generated from coulomb/repo-seed
Add graph explorer bubble help
This commit is contained in:
@@ -67,6 +67,42 @@ def graph_explorer_page() -> str:
|
|||||||
}
|
}
|
||||||
button.primary { background: var(--accent); border-color: var(--accent); color: #ffffff; }
|
button.primary { background: var(--accent); border-color: var(--accent); color: #ffffff; }
|
||||||
button:disabled { color: #98a2b3; cursor: default; }
|
button:disabled { color: #98a2b3; cursor: default; }
|
||||||
|
.field-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.help-tip {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.help-tip:hover, .help-tip:focus-visible {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.help-popup {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 30;
|
||||||
|
display: none;
|
||||||
|
max-width: 300px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 16px 40px rgba(23, 32, 51, .16);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.help-popup strong { display: block; margin-bottom: 4px; }
|
||||||
|
.help-popup p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.35; }
|
||||||
.filter-menu {
|
.filter-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -122,7 +158,10 @@ def graph_explorer_page() -> str:
|
|||||||
z-index: 3;
|
z-index: 3;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
max-width: min(460px, calc(100% - 24px));
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -220,48 +259,76 @@ def graph_explorer_page() -> str:
|
|||||||
}
|
}
|
||||||
.popup strong { display: block; margin-bottom: 4px; }
|
.popup strong { display: block; margin-bottom: 4px; }
|
||||||
.notice { color: var(--warn); padding: 14px; }
|
.notice { color: var(--warn); padding: 14px; }
|
||||||
|
.canvas-notice {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.shell { min-height: 760px; }
|
.shell { min-height: 760px; }
|
||||||
.toolbar { grid-template-columns: 1fr 1fr; }
|
.toolbar { grid-template-columns: 1fr 1fr; }
|
||||||
.main { grid-template-columns: 1fr; grid-template-rows: minmax(420px, 1fr) 330px; }
|
.main { grid-template-columns: 1fr; grid-template-rows: minmax(420px, 1fr) 330px; }
|
||||||
.side { border-left: 0; border-top: 1px solid var(--line); }
|
.side { border-left: 0; border-top: 1px solid var(--line); }
|
||||||
|
.map-controls { top: 8px; right: 8px; max-width: calc(100% - 16px); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section class="shell">
|
<section class="shell">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<label>Search <input id="search" autocomplete="off"></label>
|
<div class="field">
|
||||||
<label>Mode <select id="mode-select"></select></label>
|
<span class="field-label">Search <button type="button" class="help-tip" aria-label="Search help" data-help-title="Search" data-help="Search matches labels, ids, repos, node types, edge types, and descriptions. It hides non-matching nodes and edges without redrawing the layout.">?</button></span>
|
||||||
<label>Layout <select id="layout-select">
|
<input id="search" autocomplete="off" aria-label="Search graph">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="field-label">Mode <button type="button" class="help-tip" aria-label="Mode help" data-help-title="Mode" data-help="Modes are predefined map views. Some modes use the selected item as context; changing modes changes which graph entities are visible, but keeps the layout controls separate.">?</button></span>
|
||||||
|
<select id="mode-select" aria-label="Graph mode"></select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="field-label">Layout <button type="button" class="help-tip" aria-label="Layout help" data-help-title="Layout" data-help="Layout redraws the map arrangement. Cose uses relationship strength and repo affinity; circle, grid, concentric, and breadthfirst are simpler alternate arrangements.">?</button></span>
|
||||||
|
<select id="layout-select" aria-label="Graph layout">
|
||||||
<option value="cose">Cose</option>
|
<option value="cose">Cose</option>
|
||||||
<option value="concentric">Concentric</option>
|
<option value="concentric">Concentric</option>
|
||||||
<option value="circle">Circle</option>
|
<option value="circle">Circle</option>
|
||||||
<option value="grid">Grid</option>
|
<option value="grid">Grid</option>
|
||||||
<option value="breadthfirst">Breadthfirst</option>
|
<option value="breadthfirst">Breadthfirst</option>
|
||||||
</select></label>
|
</select>
|
||||||
<div class="field"><span>Node Types</span>
|
</div>
|
||||||
|
<div class="field"><span class="field-label">Node Types <button type="button" class="help-tip" aria-label="Node types help" data-help-title="Node Types" data-help="Nodes are the entities drawn on the map: repositories, services, deployments, servers, capabilities, interfaces, dependencies, bindings, and libraries. This filter hides unchecked node types while preserving the current layout.">?</button></span>
|
||||||
<details id="node-type-menu" class="filter-menu">
|
<details id="node-type-menu" class="filter-menu">
|
||||||
<summary id="node-type-summary">All node types</summary>
|
<summary id="node-type-summary">All node types</summary>
|
||||||
<div id="node-type-filter" class="check-list"></div>
|
<div id="node-type-filter" class="check-list"></div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><span>Edge Types</span>
|
<div class="field"><span class="field-label">Edge Types <button type="button" class="help-tip" aria-label="Edge types help" data-help-title="Edge Types" data-help="Edges are relationships between nodes, such as provides, consumes, runs_on, or binds. This filter hides unchecked relationship types while preserving the current layout.">?</button></span>
|
||||||
<details id="edge-type-menu" class="filter-menu">
|
<details id="edge-type-menu" class="filter-menu">
|
||||||
<summary id="edge-type-summary">All edge types</summary>
|
<summary id="edge-type-summary">All edge types</summary>
|
||||||
<div id="edge-type-filter" class="check-list"></div>
|
<div id="edge-type-filter" class="check-list"></div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<label>Review <select id="review-filter"><option value="">Any</option><option>accepted</option><option>candidate</option></select></label>
|
<div class="field">
|
||||||
<label>Unresolved <select id="unresolved-filter"><option value="">Any</option><option value="true">Only</option></select></label>
|
<span class="field-label">Review <button type="button" class="help-tip" aria-label="Review help" data-help-title="Review" data-help="Review state applies only to entities that carry review metadata. Accepted means trusted data; candidate means registered or inferred data that still needs context.">?</button></span>
|
||||||
<label>Profile <select id="profile-select" disabled><option>Unsaved exploration</option></select></label>
|
<select id="review-filter" aria-label="Review state filter"><option value="">Any</option><option>accepted</option><option>candidate</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="field-label">Unresolved <button type="button" class="help-tip" aria-label="Unresolved help" data-help-title="Unresolved" data-help="Unresolved marks missing provider bindings or registered repositories without accepted graph snapshots. It only applies to node and edge types that can express those gaps.">?</button></span>
|
||||||
|
<select id="unresolved-filter" aria-label="Unresolved filter"><option value="">Any</option><option value="true">Only</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="field-label">Profile <button type="button" class="help-tip" aria-label="Saved views help" data-help-title="Saved Views" data-help="Saved views store the current search, mode, layout, label density, filters, and manual visibility overrides in this browser. Copy State creates a shareable URL for the current view.">?</button></span>
|
||||||
|
<select id="profile-select" aria-label="Saved view profile" disabled><option>Unsaved exploration</option></select>
|
||||||
|
</div>
|
||||||
<span id="counts" class="meta"></span>
|
<span id="counts" class="meta"></span>
|
||||||
</div>
|
</div>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="canvas-wrap">
|
<div class="canvas-wrap">
|
||||||
<div id="graph-canvas" role="img" aria-label="Interactive Fabric graph"></div>
|
<div id="graph-canvas" role="img" aria-label="Interactive Fabric graph" aria-live="polite"><p class="canvas-notice">Loading graph...</p></div>
|
||||||
<div class="map-controls" aria-label="Map navigation controls">
|
<div class="map-controls" aria-label="Map navigation controls">
|
||||||
<label class="map-control-field">Labels
|
<label class="map-control-field"><span class="field-label">Labels <button type="button" class="help-tip" aria-label="Labels help" data-help-title="Labels" data-help="Label density changes only text visibility. Auto hides low-priority labels when the map is dense; Key keeps repositories, services, deployments, servers, and issue markers visible.">?</button></span>
|
||||||
<select id="label-select" title="Control node label density">
|
<select id="label-select" title="Control node label density">
|
||||||
<option value="auto">Auto</option>
|
<option value="auto">Auto</option>
|
||||||
<option value="key">Key</option>
|
<option value="key">Key</option>
|
||||||
@@ -269,14 +336,15 @@ def graph_explorer_page() -> str:
|
|||||||
<option value="none">None</option>
|
<option value="none">None</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button type="button" data-action="fit" title="Fit graph to view">Fit</button>
|
<button type="button" data-action="fit" title="Fit graph to view" data-help-title="Fit" data-help="Fit pans and zooms the current visible graph into the map viewport. It does not change filters, labels, or layout.">Fit</button>
|
||||||
<button type="button" data-action="focus" title="Focus selected neighborhood">Focus</button>
|
<button type="button" data-action="focus" title="Focus selected neighborhood" data-help-title="Focus" data-help="Focus narrows the map to the selected entity and its nearby context. It changes visibility, not the underlying registry data.">Focus</button>
|
||||||
<button type="button" data-action="clear" class="primary" title="Reset map controls">Reset</button>
|
<button type="button" data-action="clear" class="primary" title="Reset map controls" data-help-title="Reset" data-help="Reset clears search, mode, node and edge type filters, label density, unresolved/review filters, focus, and manual overrides.">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="selection-anchor" class="selection-anchor" aria-hidden="true">
|
<div id="selection-anchor" class="selection-anchor" aria-hidden="true">
|
||||||
<span id="selection-anchor-label" class="selection-anchor-label"></span>
|
<span id="selection-anchor-label" class="selection-anchor-label"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="popup" class="popup"></div>
|
<div id="popup" class="popup"></div>
|
||||||
|
<div id="help-popup" class="help-popup" role="tooltip"></div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="side">
|
<aside class="side">
|
||||||
<section class="section">
|
<section class="section">
|
||||||
@@ -291,24 +359,27 @@ def graph_explorer_page() -> str:
|
|||||||
</section>
|
</section>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button type="button" data-override="show">Show</button>
|
<button type="button" data-override="show" data-help-title="Show" data-help="Show pins the selected entity visible even when other filters would hide it. It does not redraw the layout.">Show</button>
|
||||||
<button type="button" data-override="blur">Blur</button>
|
<button type="button" data-override="blur" data-help-title="Blur" data-help="Blur keeps the selected entity in place but reduces emphasis. It is useful for context that should stay visible without competing for attention.">Blur</button>
|
||||||
<button type="button" data-override="hide">Hide</button>
|
<button type="button" data-override="hide" data-help-title="Hide" data-help="Hide makes the selected entity invisible while preserving layout context. Remove is reserved for the upcoming rule panel and will redraw the graph without the entity.">Hide</button>
|
||||||
<button type="button" data-action="reset-overrides">Clear Overrides</button>
|
<button type="button" data-action="reset-overrides" data-help-title="Clear Overrides" data-help="Clear Overrides removes manual Show, Blur, and Hide choices. Filters, mode, layout, and label density stay as they are.">Clear Overrides</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<p id="profile-summary" class="meta">Profile persistence unavailable for this host.</p>
|
<p id="profile-summary" class="meta">Profile persistence unavailable for this host.</p>
|
||||||
<label>Profile Name <input id="profile-name" autocomplete="off" disabled></label>
|
<div class="field">
|
||||||
|
<span class="field-label">Profile Name <button type="button" class="help-tip" aria-label="Profile name help" data-help-title="Profile Name" data-help="Profile Name labels a saved browser-local map view. It does not rename graph entities or registry data.">?</button></span>
|
||||||
|
<input id="profile-name" autocomplete="off" aria-label="Profile name" disabled>
|
||||||
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button type="button" data-profile-action="save" disabled>Save</button>
|
<button type="button" data-profile-action="save" data-help-title="Save" data-help="Save stores the current browser-local view, including filters, layout, label density, and manual overrides." disabled>Save</button>
|
||||||
<button type="button" data-profile-action="duplicate" disabled>Duplicate</button>
|
<button type="button" data-profile-action="duplicate" data-help-title="Duplicate" data-help="Duplicate creates a new saved view from the currently loaded profile and current map state." disabled>Duplicate</button>
|
||||||
<button type="button" data-profile-action="delete" disabled>Delete</button>
|
<button type="button" data-profile-action="delete" data-help-title="Delete" data-help="Delete removes the selected browser-local saved view. It does not delete graph data." disabled>Delete</button>
|
||||||
<button type="button" data-profile-action="copy" disabled>Copy State</button>
|
<button type="button" data-profile-action="copy" data-help-title="Copy State" data-help="Copy State copies a URL that can restore this map state, including filters and manual overrides." disabled>Copy State</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<p id="hidden-summary" class="meta">Hidden 0</p>
|
<p class="meta"><span id="hidden-summary">Hidden 0</span> <button type="button" class="help-tip" aria-label="Hidden entities help" data-help-title="Hidden Entities" data-help="Hidden counts entities currently not shown because of filters, mode, or manual Hide overrides. Hidden keeps layout context; Remove will redraw without the entity in the rule panel.">?</button></p>
|
||||||
<div id="legend"></div>
|
<div id="legend"></div>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -321,6 +392,7 @@ def graph_explorer_page() -> str:
|
|||||||
const graphUrl = "/exports/graph-explorer";
|
const graphUrl = "/exports/graph-explorer";
|
||||||
const canvas = document.getElementById("graph-canvas");
|
const canvas = document.getElementById("graph-canvas");
|
||||||
const popup = document.getElementById("popup");
|
const popup = document.getElementById("popup");
|
||||||
|
const helpPopup = document.getElementById("help-popup");
|
||||||
const selectionAnchor = document.getElementById("selection-anchor");
|
const selectionAnchor = document.getElementById("selection-anchor");
|
||||||
const selectionAnchorLabel = document.getElementById("selection-anchor-label");
|
const selectionAnchorLabel = document.getElementById("selection-anchor-label");
|
||||||
const searchInput = document.getElementById("search");
|
const searchInput = document.getElementById("search");
|
||||||
@@ -365,6 +437,26 @@ def graph_explorer_page() -> str:
|
|||||||
.replaceAll("&", "&").replaceAll("<", "<")
|
.replaceAll("&", "&").replaceAll("<", "<")
|
||||||
.replaceAll(">", ">").replaceAll('"', """);
|
.replaceAll(">", ">").replaceAll('"', """);
|
||||||
|
|
||||||
|
const showHelp = (target) => {
|
||||||
|
if (!helpPopup || !target?.dataset?.help) return;
|
||||||
|
const bounds = target.getBoundingClientRect();
|
||||||
|
const title = target.dataset.helpTitle || target.textContent || "Help";
|
||||||
|
helpPopup.innerHTML = `<strong>${escapeHtml(title)}</strong><p>${escapeHtml(target.dataset.help)}</p>`;
|
||||||
|
helpPopup.style.display = "block";
|
||||||
|
const width = Math.min(helpPopup.offsetWidth || 300, 300);
|
||||||
|
const left = Math.min(Math.max(12, bounds.left), window.innerWidth - width - 12);
|
||||||
|
const top = bounds.bottom + 8 < window.innerHeight - 80
|
||||||
|
? bounds.bottom + 8
|
||||||
|
: Math.max(12, bounds.top - helpPopup.offsetHeight - 8);
|
||||||
|
helpPopup.style.left = `${left}px`;
|
||||||
|
helpPopup.style.top = `${top}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideHelp = () => {
|
||||||
|
if (!helpPopup) return;
|
||||||
|
helpPopup.style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
const elementText = (data) => [
|
const elementText = (data) => [
|
||||||
data.id, data.stableKey, data.label, data.name, data.description,
|
data.id, data.stableKey, data.label, data.name, data.description,
|
||||||
data.repo, data.domain, data.kind, data.layer, data.edgeType
|
data.repo, data.domain, data.kind, data.layer, data.edgeType
|
||||||
@@ -960,6 +1052,13 @@ def graph_explorer_page() -> str:
|
|||||||
...element,
|
...element,
|
||||||
data: {...element.data, color: layerColors[element.data.layer] || "#667085"}
|
data: {...element.data, color: layerColors[element.data.layer] || "#667085"}
|
||||||
}));
|
}));
|
||||||
|
if (elements.length === 0) {
|
||||||
|
canvas.innerHTML = "<p class='canvas-notice'>No graph entities were returned by the registry.</p>";
|
||||||
|
counts.textContent = "0 nodes / 0 edges";
|
||||||
|
hiddenSummary.textContent = "Hidden 0";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.innerHTML = "";
|
||||||
cy = cytoscape({
|
cy = cytoscape({
|
||||||
container: canvas,
|
container: canvas,
|
||||||
elements,
|
elements,
|
||||||
@@ -1158,12 +1257,21 @@ def graph_explorer_page() -> str:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll("[data-help]").forEach((target) => {
|
||||||
|
target.addEventListener("mouseenter", () => showHelp(target));
|
||||||
|
target.addEventListener("focus", () => showHelp(target));
|
||||||
|
target.addEventListener("mouseleave", hideHelp);
|
||||||
|
target.addEventListener("blur", hideHelp);
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") hideHelp();
|
||||||
|
});
|
||||||
if (!window.cytoscape) {
|
if (!window.cytoscape) {
|
||||||
canvas.innerHTML = "<p class='notice'>Cytoscape.js could not be loaded.</p>";
|
canvas.innerHTML = "<p class='notice'>Cytoscape.js could not be loaded.</p>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boot().catch((error) => {
|
boot().catch((error) => {
|
||||||
canvas.innerHTML = `<p class='notice'>${escapeHtml(error.message)}</p>`;
|
canvas.innerHTML = `<p class='canvas-notice'>Could not load graph explorer data: ${escapeHtml(error.message)}</p>`;
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -210,11 +210,19 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
|||||||
assert 'id="node-type-filter"' in page
|
assert 'id="node-type-filter"' in page
|
||||||
assert 'id="edge-type-filter"' in page
|
assert 'id="edge-type-filter"' in page
|
||||||
assert 'id="selection-anchor"' in page
|
assert 'id="selection-anchor"' in page
|
||||||
|
assert 'id="help-popup"' in page
|
||||||
assert "updateSelectionAnchor" in page
|
assert "updateSelectionAnchor" in page
|
||||||
assert "updateLabelVisibility" in page
|
assert "updateLabelVisibility" in page
|
||||||
|
assert "showHelp" in page
|
||||||
assert "label-hidden" in page
|
assert "label-hidden" in page
|
||||||
|
assert "Loading graph..." in page
|
||||||
|
assert "No graph entities were returned by the registry." in page
|
||||||
|
assert "Could not load graph explorer data" in page
|
||||||
assert "Node Types" in page
|
assert "Node Types" in page
|
||||||
assert "Edge Types" in page
|
assert "Edge Types" in page
|
||||||
|
assert 'data-help-title="Node Types"' in page
|
||||||
|
assert 'data-help-title="Saved Views"' in page
|
||||||
|
assert "Remove will redraw" in page
|
||||||
assert "Registered only" in page
|
assert "Registered only" in page
|
||||||
assert 'id="layer-filter"' not in page
|
assert 'id="layer-filter"' not in page
|
||||||
assert '"border-width": 4' not in page
|
assert '"border-width": 4' not in page
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ Acceptance notes:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0009-T03
|
id: RAIL-FAB-WP-0009-T03
|
||||||
status: in_progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "3e60397c-c833-4bd7-ba1b-b754b203dade"
|
state_hub_task_id: "3e60397c-c833-4bd7-ba1b-b754b203dade"
|
||||||
```
|
```
|
||||||
@@ -176,7 +176,7 @@ Acceptance notes:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0009-T05
|
id: RAIL-FAB-WP-0009-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "67a9cbc9-ebaa-4cb1-bec9-46bf250faf41"
|
state_hub_task_id: "67a9cbc9-ebaa-4cb1-bec9-46bf250faf41"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user