This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch feat/explore in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 67200c829c280ed6915cd7fe11f692d14d0cfe67 Author: Wu Sheng <[email protected]> AuthorDate: Tue Jun 23 21:19:49 2026 +0800 fix(explore): brush-select traces + keep results on screen The distribution's drag-to-select emitted a brush event that Trace inspect never handled, so dragging a box did nothing. Wire it like the per-layer tab: brushing highlights those dots and filters the list to them, with a clear affordance; single-click still opens one trace. Also bound the distribution square (it stretched to the full form height, pushing the result list/detail off-screen) so results stay visible below the strip. --- .../src/features/operate/explore/ExploreView.vue | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/ui/src/features/operate/explore/ExploreView.vue b/apps/ui/src/features/operate/explore/ExploreView.vue index 269df27..dc0c466 100644 --- a/apps/ui/src/features/operate/explore/ExploreView.vue +++ b/apps/ui/src/features/operate/explore/ExploreView.vue @@ -375,6 +375,7 @@ async function runQuery(): Promise<void> { hasQueried.value = true; errorMsg.value = null; closeDetail(); + brushedKeys.value = []; const zipkin = traceSource.value === 'zipkin'; const req = zipkin ? buildZipkinRequest() : buildNativeRequest(); try { @@ -423,6 +424,16 @@ const rows = computed<NativeTraceListRow[]>(() => { }); const maxDuration = computed(() => (rows.value.length === 0 ? 0 : Math.max(...rows.value.map((r) => r.duration)))); +// Brushing a region of the distribution highlights those dots and filters +// the list to them; a single click still opens one trace's detail. +const brushedKeys = ref<string[]>([]); +const brushedSet = computed(() => new Set(brushedKeys.value)); +const displayRows = computed<NativeTraceListRow[]>(() => + brushedKeys.value.length > 0 ? rows.value.filter((r) => brushedSet.value.has(r.key)) : rows.value, +); +function onScatterBrush(keys: string[]): void { brushedKeys.value = keys; } +function clearBrush(): void { brushedKeys.value = []; } + // Which OAP query answered. `queryBasicTraces` (Trace Query v1 API) // returns trace SEGMENTS; `queryTraces` (v2, BanyanDB only) returns whole // traces with spans inline. The banner states the API so operators always @@ -662,7 +673,9 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onPageKeyDown, true) :rows="rows" :max-duration="maxDuration" :selected-key="selectedRowKey" + :highlight-keys="brushedKeys" @select="openRow" + @brush="onScatterBrush" /> </section> </div> @@ -686,10 +699,11 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onPageKeyDown, true) <article v-else-if="!selectedTraceId" class="iq-list-card sw-card"> <header class="iq-list-head"> <h4>{{ isSegmentList ? t('Segments') : t('Traces') }}</h4> - <span class="hint">{{ rows.length }} {{ isSegmentList ? t('segments') : t('traces') }}</span> + <span class="hint">{{ displayRows.length }}<template v-if="brushedKeys.length"> / {{ rows.length }}</template> {{ isSegmentList ? t('segments') : t('traces') }}</span> + <button v-if="brushedKeys.length" type="button" class="iq-brush-clear" @click="clearBrush">{{ t('clear') }}</button> </header> <TraceListPanel - :rows="rows" + :rows="displayRows" :selected-key="selectedRowKey" :max-duration="maxDuration" @select="openRow" @@ -700,11 +714,11 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onPageKeyDown, true) <TraceListPanel foldable :rail-open="railOpen" - :rows="rows" + :rows="displayRows" :selected-key="selectedRowKey" :max-duration="maxDuration" :title="isSegmentList ? t('Segments') : t('Traces')" - :count-hint="rows.length" + :count-hint="displayRows.length" @select="openRow" @toggle-rail="railOpen = !railOpen" /> @@ -795,12 +809,14 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onPageKeyDown, true) .iq-resolved-tog .dim { color: var(--sw-fg-4); margin-left: 6px; } .iq-resolved-body { margin: 0; padding: 8px 12px; font-family: var(--sw-mono); font-size: 11px; color: var(--sw-fg-2); background: var(--sw-bg-0); overflow: auto; max-height: 160px; border: 1px solid var(--sw-line); border-radius: 5px; } -.iq-top-strip { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: stretch; } +.iq-top-strip { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 360px); gap: 12px; align-items: start; } @media (max-width: 1100px) { .iq-top-strip { grid-template-columns: 1fr; } } .iq-form { display: flex; flex-direction: column; gap: 10px; min-width: 0; } .iq-conditions { display: flex; flex-direction: column; gap: 10px; min-width: 0; } -.iq-scatter { padding: 6px 10px 8px; display: flex; flex-direction: column; min-height: 0; margin: 0; height: 100%; aspect-ratio: 1; } +.iq-scatter { padding: 6px 10px 8px; display: flex; flex-direction: column; min-height: 0; margin: 0; aspect-ratio: 1; } +.iq-brush-clear { margin-left: 8px; background: none; border: 1px solid var(--sw-line-2); color: var(--sw-fg-2); border-radius: 4px; padding: 1px 8px; font: inherit; font-size: 11px; cursor: pointer; } +.iq-brush-clear:hover { color: var(--sw-fg-0); border-color: var(--sw-fg-3); } @media (max-width: 1100px) { .iq-scatter { aspect-ratio: auto; height: 320px; } } .iq-scatter-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 2px; flex: 0 0 auto; } .iq-scatter-head .legend { margin-left: auto; font-size: 10.5px; color: var(--sw-fg-3); display: inline-flex; gap: 10px; align-items: center; }
