You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/06/27 01:30:38 UTC
(skywalking-horizon-ui) 05/18: fix(profiling,logs,alarms): reviewer-found bugs + create-flow consistency
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch refactor/decompose-large-files
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit ff8d323faebf81ea4e5ebdab35ba24307e28c2e6
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Fri Jun 26 23:52:08 2026 +0800
fix(profiling,logs,alarms): reviewer-found bugs + create-flow consistency
EndpointCombo now mirrors the parent's selected prop into its input, so a parent-side clear/change resets the displayed text — Logs and Browser Errors no longer show a stale endpoint/page after a service switch; Logs also resets its endpoint search keyword on service change.
Logs "Run query" now refetches the facet sample too, not just the rows (facets carry a 30s staleTime, so level counts no longer lag the stream).
Trace profiling's create modal reuses the shared EndpointCombo (search-and-select) instead of a separate input + dropdown; network profiling's instance pick moves inside the New Task modal — the create target is chosen there with Create gated on it — instead of sitting on the page.
The alarm timeline's selection band is a neutral light tint instead of orange (which muddied the red/green firing/recovered bars), and ToolboxComponent is registered to silence ECharts' brush warning.
---
apps/ui/src/components/charts/AlarmsTimeline.vue | 4 ++-
apps/ui/src/layer/_shared/EndpointCombo.vue | 5 +++-
apps/ui/src/layer/logs/LayerLogsView.vue | 15 +++++-----
apps/ui/src/layer/logs/useLayerLogs.ts | 1 +
.../layer/profiling/LayerNetworkProfilingView.vue | 35 ++++++++++++----------
.../layer/profiling/NewTraceProfileTaskModal.vue | 19 +++++-------
6 files changed, 43 insertions(+), 36 deletions(-)
diff --git a/apps/ui/src/components/charts/AlarmsTimeline.vue b/apps/ui/src/components/charts/AlarmsTimeline.vue
index f92b148..62f347c 100644
--- a/apps/ui/src/components/charts/AlarmsTimeline.vue
+++ b/apps/ui/src/components/charts/AlarmsTimeline.vue
@@ -38,6 +38,7 @@ import {
BrushComponent,
GridComponent,
MarkLineComponent,
+ ToolboxComponent,
TooltipComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
@@ -50,6 +51,7 @@ echarts.use([
BrushComponent,
GridComponent,
MarkLineComponent,
+ ToolboxComponent,
TooltipComponent,
CanvasRenderer,
]);
@@ -161,7 +163,7 @@ function buildOption(): echarts.EChartsCoreOption {
xAxisIndex: 0,
brushType: 'lineX',
brushMode: 'single',
- brushStyle: { color: 'rgba(249,115,22,0.18)', borderColor: 'rgba(249,115,22,0.6)' },
+ brushStyle: { color: 'rgba(226,232,240,0.10)', borderColor: 'rgba(226,232,240,0.7)' },
transformable: false,
throttleType: 'debounce',
throttleDelay: 200,
diff --git a/apps/ui/src/layer/_shared/EndpointCombo.vue b/apps/ui/src/layer/_shared/EndpointCombo.vue
index 1d6a149..6ccea6d 100644
--- a/apps/ui/src/layer/_shared/EndpointCombo.vue
+++ b/apps/ui/src/layer/_shared/EndpointCombo.vue
@@ -32,7 +32,7 @@
import { watch } from 'vue';
import { useEndpointCombo } from '@/layer/_shared/useEndpointCombo';
-withDefaults(
+const props = withDefaults(
defineProps<{
endpoints: ReadonlyArray<{ id: string; name: string }>;
selected: string | null;
@@ -51,6 +51,9 @@ const emit = defineEmits<{
const combo = useEndpointCombo();
watch(combo.query, (q) => emit('update:query', q));
+// Mirror the parent's selection into the input so a parent-side clear/change
+// resets the displayed text (setDisplay skips the search debounce).
+watch(() => props.selected, (sel) => combo.setDisplay(sel ?? ''));
function pick(name: string): void {
combo.setDisplay(name);
diff --git a/apps/ui/src/layer/logs/LayerLogsView.vue b/apps/ui/src/layer/logs/LayerLogsView.vue
index a3d9a7d..228ac3d 100644
--- a/apps/ui/src/layer/logs/LayerLogsView.vue
+++ b/apps/ui/src/layer/logs/LayerLogsView.vue
@@ -150,8 +150,9 @@ const { endpoints: endpointList, isFetching: endpointsLoading } = useLayerEndpoi
// No endpoint auto-pick on Logs either — same reasoning as the
// instance picker above. Default is `All`; operator narrows by hand.
watch(serviceName, (next, prev) => {
- if (prev !== undefined && next !== prev && selectedEndpoint.value) {
- setSelectedEndpoint(null);
+ if (prev !== undefined && next !== prev) {
+ if (selectedEndpoint.value) setSelectedEndpoint(null);
+ endpointQuery.value = '';
}
});
const selectedEndpointObj = computed(() =>
@@ -278,7 +279,7 @@ const { logs, total, isFetching, error, refetch } = useLayerLogs(layerKey, {
enabled: hasQueried,
});
-const { facets } = useLayerLogFacets(layerKey, {
+const { facets, refetch: refetchFacets } = useLayerLogFacets(layerKey, {
service: aService,
instanceId: aInstanceId,
endpointId: aEndpointId,
@@ -290,15 +291,15 @@ const { facets } = useLayerLogFacets(layerKey, {
enabled: hasQueried,
});
-// Run-query handler mirrors the trace tab: refetch both the log
-// stream + the facet sample on demand. With most filters already
-// auto-refetching, this is the operator's "I'm done editing — refresh
-// now" affordance, identical voice to `LayerTracesView#runQuery`.
+// Run query refetches BOTH the log stream and the facet sample (level
+// counts) so they never diverge — facets carry a 30s staleTime, so an
+// unchanged-condition Run query needs an explicit refetch.
function runQuery(): void {
page.value = 1;
hasQueried.value = true;
applyConditions();
void refetch();
+ void refetchFacets();
}
// ── Density histogram (60 bins). Loki/Datadog style: stacked bars
diff --git a/apps/ui/src/layer/logs/useLayerLogs.ts b/apps/ui/src/layer/logs/useLayerLogs.ts
index 5cba8f7..1ebe208 100644
--- a/apps/ui/src/layer/logs/useLayerLogs.ts
+++ b/apps/ui/src/layer/logs/useLayerLogs.ts
@@ -136,6 +136,7 @@ export function useLayerLogFacets(layerKey: Ref<string>, params: LogFacetParams)
facets: computed(() => q.data.value ?? null),
isFetching: q.isFetching,
error: q.error,
+ refetch: q.refetch,
};
}
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index 10bf4ee..47cca91 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -350,20 +350,8 @@ function fmtTime(ms: number): string {
<template>
<div class="sw-card net-shell">
- <!-- Side: instance picker + tasks -->
+ <!-- Side: tasks (the create target instance is chosen inside New Task) -->
<div class="net-side">
- <div class="side-head">
- <span>Instance</span>
- </div>
- <div class="picker">
- <select v-model="selectedInstanceId" class="sel wide" :disabled="!instances.instances.value.length">
- <option v-if="!instances.instances.value.length" :value="null">— no instances —</option>
- <option v-for="inst in instances.instances.value" :key="inst.id" :value="inst.id">
- {{ inst.name }}
- </option>
- </select>
- </div>
-
<div class="side-head between">
<span>Network tasks</span>
<div class="side-head-actions">
@@ -377,8 +365,8 @@ function fmtTime(ms: number): string {
><Icon name="refresh" :size="11" /></button>
<button
class="btn-new"
- :disabled="!selectedInstanceId"
- :title="!selectedInstanceId ? 'Pick an instance first' : 'Create a new network profile task'"
+ :disabled="!serviceId"
+ :title="!serviceId ? 'Pick a service first' : 'Create a new network profile task'"
@click="showNewTask = true"
>+ New Task</button>
</div>
@@ -497,6 +485,16 @@ function fmtTime(ms: number): string {
<button class="x" @click="showNewTask = false">×</button>
</div>
<div class="dlg-body">
+ <div class="field">
+ <label>Instance</label>
+ <select v-model="selectedInstanceId" class="sel wide" :disabled="!instances.instances.value.length">
+ <option v-if="!instances.instances.value.length" :value="null">— no instances —</option>
+ <option v-for="inst in instances.instances.value" :key="inst.id" :value="inst.id">{{ inst.name }}</option>
+ </select>
+ </div>
+ <div v-if="!instances.instances.value.length" class="banner err">
+ No instances available for this service — a network profile task cannot be created.
+ </div>
<p class="hint">
OAP captures one connection sample per matching rule. Leave URI
empty to match any request; toggle 4xx/5xx to scope by status.
@@ -533,7 +531,12 @@ function fmtTime(ms: number): string {
</div>
<div class="dlg-foot">
<button class="btn-secondary" @click="showNewTask = false">Cancel</button>
- <button class="btn-primary" @click="submitNewTask">Create task</button>
+ <button
+ class="btn-primary"
+ :disabled="!selectedInstanceId"
+ :title="selectedInstanceId ? 'Create the network profile task' : 'No instances available for this service'"
+ @click="submitNewTask"
+ >Create task</button>
</div>
</div>
</div>
diff --git a/apps/ui/src/layer/profiling/NewTraceProfileTaskModal.vue b/apps/ui/src/layer/profiling/NewTraceProfileTaskModal.vue
index 1b79741..47ad5fe 100644
--- a/apps/ui/src/layer/profiling/NewTraceProfileTaskModal.vue
+++ b/apps/ui/src/layer/profiling/NewTraceProfileTaskModal.vue
@@ -25,6 +25,7 @@
<script setup lang="ts">
import { reactive, watch } from 'vue';
import { useEscapeToClose } from '@/components/primitives/useEscapeToClose';
+import EndpointCombo from '@/layer/_shared/EndpointCombo.vue';
export interface EndpointPick {
id: string;
@@ -116,18 +117,14 @@ function submit(): void {
<div class="dlg-body">
<div class="field">
<label>Endpoint name</label>
- <input
- :value="keyword"
- placeholder="Type to search…"
- class="ti-input wide"
- @input="(ev: Event) => emit('update:keyword', (ev.target as HTMLInputElement).value)"
+ <EndpointCombo
+ :endpoints="endpoints"
+ :selected="newTask.endpointName || null"
+ placeholder="(any)"
+ @update:query="(q: string) => emit('update:keyword', q)"
+ @pick="(name: string) => (newTask.endpointName = name)"
+ @clear="newTask.endpointName = ''"
/>
- <select v-model="newTask.endpointName" class="sel wide">
- <option value="">(any)</option>
- <option v-for="e in endpoints" :key="e.id" :value="e.name">
- {{ e.name }}
- </option>
- </select>
</div>
<div class="field-row">
<div class="field">