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 578cd8002db3ab0fb8d4a56771e54513545daae9
Author: Wu Sheng <[email protected]>
AuthorDate: Tue Jun 23 15:35:15 2026 +0800

    refactor(explore): two menus (Trace/Log inspect), optional entity, wider 
form
    
    Address review feedback:
    - Two sidebar menus + routes — /operate/trace-inspect (traces:read) and
      /operate/log-inspect (logs:read) — instead of one "Explore"; one
      component driven by a `kind` prop.
    - Entity is OPTIONAL: leave the target blank to query every service in
      the window (OAP's serviceId is nullable). Validated cross-service.
    - Layer picker lists all active layers (serviceCount > 0), not just the
      few that declare a native-traces capability.
    - Form adopts the Traces page's .cf grid vocab (full-width inputs in a
      4-col grid) so fields no longer truncate.
---
 apps/bff/src/http/query/explore.ts                 |   9 +-
 .../src/features/operate/explore/ExploreView.vue   | 474 ++++++++++-----------
 apps/ui/src/shell/AppSidebar.vue                   |   3 +-
 apps/ui/src/shell/router/index.ts                  |  23 +-
 packages/api-client/src/explore.ts                 |   4 +-
 5 files changed, 242 insertions(+), 271 deletions(-)

diff --git a/apps/bff/src/http/query/explore.ts 
b/apps/bff/src/http/query/explore.ts
index f2d8e14..ee78f4c 100644
--- a/apps/bff/src/http/query/explore.ts
+++ b/apps/bff/src/http/query/explore.ts
@@ -105,10 +105,9 @@ export function registerExploreRoutes(app: 
FastifyInstance, deps: ExploreRouteDe
         };
 
         if (traceSource === 'native') {
-          const ids = resolveNativeEntity(body.entity);
-          if (!ids.serviceId && !body.traceId) {
-            return reply.code(400).send({ error: 'entity_required' });
-          }
+          // Entity is optional — no service means "all services in the
+          // window" (OAP's TraceQueryCondition.serviceId is nullable).
+          const ids = body.entity ? resolveNativeEntity(body.entity) : {};
           const native = await fetchNativeList(
             opts,
             { ...base, ...ids },
@@ -141,7 +140,7 @@ export function registerExploreRoutes(app: FastifyInstance, 
deps: ExploreRouteDe
 
         // zipkin: a raw service name (no OAP id). remoteService/span/
         // annotation enrichment lands with the zipkin form increment.
-        const service = body.entity.serviceName;
+        const service = body.entity?.serviceName;
         const zipkin = await fetchZipkinList(opts, { ...base, service }, 
maxTraces);
         const resolved: ExploreResolved = {
           kind: 'trace',
diff --git a/apps/ui/src/features/operate/explore/ExploreView.vue 
b/apps/ui/src/features/operate/explore/ExploreView.vue
index e1ee616..06c836f 100644
--- a/apps/ui/src/features/operate/explore/ExploreView.vue
+++ b/apps/ui/src/features/operate/explore/ExploreView.vue
@@ -15,17 +15,18 @@
   limitations under the License.
 -->
 <!--
-  Explore — cross-layer trace/log query power-tool.
-
-  Layer-less by design: the operator names an entity by PICKING it (a
-  layer-filtered dropdown) or TYPING it (service name + the real/normal
-  flag, which the BFF encodes into the OAP id). One query → one result,
-  rendered with the same waterfall the per-layer Traces tab uses. The
-  resolved-query panel surfaces the exact condition the BFF ran — the
-  trace/log analog of Metrics-inspect showing the bare MQE.
-
-  This pass implements Trace · native end to end. Trace · zipkin and the
-  Log kinds (raw / browser-error) are wired into the same spine next.
+  Trace inspect / Log inspect — cross-layer trace/log query power-tools
+  (the `kind` prop picks which; two sidebar menus, one component).
+
+  Layer-less by design: the entity is OPTIONAL. Name a service by PICKING
+  it (a layer-filtered dropdown), TYPING it (service name + the real/
+  normal flag, which the BFF encodes into the OAP id), or leave it blank
+  to query every service in the window. One query → one result, rendered
+  with the same waterfall the per-layer Traces tab uses; the resolved-
+  query panel surfaces the exact condition the BFF ran.
+
+  This pass implements Trace · native. Trace · zipkin and the Log kinds
+  (raw / browser-error) land next on the same spine.
 -->
 <script setup lang="ts">
 import { computed, reactive, ref, watch } from 'vue';
@@ -35,7 +36,6 @@ import type {
   ExploreEntity,
   ExploreRequest,
   ExploreResolved,
-  LayerDef,
   NativeSpan,
   NativeTraceListResponse,
   NativeTraceListRow,
@@ -47,25 +47,20 @@ import { useTraceDetail } from 
'@/layer/traces/useLayerTraces';
 import NativeTraceWaterfall, { type WaterfallSpan } from 
'@/layer/traces/NativeTraceWaterfall.vue';
 import TypeaheadSelect from '@/components/primitives/TypeaheadSelect.vue';
 
+const props = defineProps<{ kind: 'trace' | 'log' }>();
 const { t } = useI18n();
-const { layers } = useLayers();
+const { availableLayers } = useLayers();
+
+const title = computed(() => (props.kind === 'trace' ? t('Trace inspect') : 
t('Log inspect')));
 
-// ── kind + source (zipkin / log land in the next increments) ──────────
-type Kind = 'trace' | 'log';
+// ── source (zipkin / log sources land in the next increments) ─────────
 type TraceSource = 'native' | 'zipkin';
-const kind = ref<Kind>('trace');
 const traceSource = ref<TraceSource>('native');
 
-// ── entity: pick (layer-filtered) vs type (name + real flag) ──────────
+// ── entity (OPTIONAL): pick (layer-filtered) vs type (name + real) ────
 type EntityMode = 'pick' | 'type';
 const entityMode = ref<EntityMode>('pick');
 
-// Layers that actually serve native traces.
-const traceLayers = computed<LayerDef[]>(() =>
-  layers.value.filter((l) => l.caps?.traces && (l.traces?.source ?? 'native') 
!== 'zipkin'),
-);
-
-// pick mode
 const pickLayer = ref<string>('');
 const pickServiceId = ref<string>('');
 const pickInstanceId = ref<string>('');
@@ -133,6 +128,7 @@ watch(pickServiceId, () => {
 });
 watch(endpointQuery, () => void loadEndpoints());
 
+const layerOptions = computed(() => availableLayers.value.map((l) => ({ value: 
l.key, label: l.name || l.key })));
 const serviceOptions = computed(() =>
   services.value.map((s) => ({ value: s.id, label: s.name, hint: s.normal === 
false ? 'virtual' : undefined })),
 );
@@ -147,7 +143,6 @@ const endpointOptions = computed(() => [
 const instanceSel = computed<string>({ get: () => pickInstanceId.value, set: 
(v) => (pickInstanceId.value = v ?? '') });
 const endpointSel = computed<string>({ get: () => pickEndpointId.value, set: 
(v) => (pickEndpointId.value = v ?? '') });
 
-// type mode
 const typeService = ref<string>('');
 const typeReal = ref<boolean>(true);
 const typeInstance = ref<string>('');
@@ -219,20 +214,19 @@ const native = ref<NativeTraceListResponse | null>(null);
 const resolved = ref<ExploreResolved | null>(null);
 const showResolved = ref(false);
 
-const canRun = computed(() => kind.value === 'trace' && traceSource.value === 
'native' && currentEntity() !== null);
+const canRun = computed(() => props.kind === 'trace' && traceSource.value === 
'native');
 
 async function runQuery(): Promise<void> {
-  const entity = currentEntity();
-  if (!entity) return;
   running.value = true;
   hasQueried.value = true;
   errorMsg.value = null;
   selectedTraceId.value = null;
   embeddedSpans.value = null;
+  const entity = currentEntity();
   const req: ExploreRequest = {
     kind: 'trace',
     traceSource: 'native',
-    entity,
+    ...(entity ? { entity } : {}),
     window: { windowMinutes: cond.windowMinutes },
     pageSize: cond.limit,
     traceId: cond.traceId.trim() || undefined,
@@ -283,253 +277,219 @@ function fmtTime(start: string): string {
 </script>
 
 <template>
-  <div class="ex">
-    <header class="ex-head">
-      <h1>{{ t('Explore') }}</h1>
-      <p class="ex-sub">{{ t('Cross-layer trace and log query. Pick any 
service, or type its name.') }}</p>
+  <div class="iq">
+    <header class="iq-head">
+      <h1>{{ title }}</h1>
+      <p class="iq-sub">{{ t('Query any service across layers — pick it, type 
its name, or leave it blank.') }}</p>
     </header>
 
-    <div class="ex-bar">
-      <div class="ex-seg">
-        <button :class="{ on: kind === 'trace' }" @click="kind = 'trace'">{{ 
t('Trace') }}</button>
-        <button class="muted" disabled :title="t('coming next')">{{ t('Log') 
}}</button>
-      </div>
-      <div class="ex-seg" v-if="kind === 'trace'">
-        <button :class="{ on: traceSource === 'native' }" @click="traceSource 
= 'native'">{{ t('Native') }}</button>
-        <button class="muted" disabled :title="t('coming 
next')">Zipkin</button>
+    <div v-if="props.kind === 'log'" class="iq-coming">{{ t('Log inspect — 
coming in the next pass.') }}</div>
+
+    <template v-else>
+      <div class="iq-bar">
+        <span class="iq-bar-l">{{ t('Source') }}</span>
+        <div class="seg">
+          <button :class="{ on: traceSource === 'native' }" 
@click="traceSource = 'native'">{{ t('Native') }}</button>
+          <button class="muted" disabled :title="t('coming 
next')">Zipkin</button>
+        </div>
       </div>
-    </div>
 
-    <div class="ex-form">
-      <div class="ex-seg sm">
-        <button :class="{ on: entityMode === 'pick' }" @click="entityMode = 
'pick'">{{ t('Pick') }}</button>
-        <button :class="{ on: entityMode === 'type' }" @click="entityMode = 
'type'">{{ t('Type') }}</button>
+      <div class="iq-target">
+        <div class="iq-target-h">
+          <span>{{ t('Target') }} <small class="dim">{{ t('optional — blank 
queries all services') }}</small></span>
+          <div class="seg sm">
+            <button :class="{ on: entityMode === 'pick' }" @click="entityMode 
= 'pick'">{{ t('Pick') }}</button>
+            <button :class="{ on: entityMode === 'type' }" @click="entityMode 
= 'type'">{{ t('Type') }}</button>
+          </div>
+          <button v-if="entityMode === 'pick'" class="iq-link" 
:disabled="!pickServiceId" @click="seedTypeFromPick">
+            {{ t('→ edit as text') }}
+          </button>
+        </div>
+
+        <div class="iq-grid" v-if="entityMode === 'pick'">
+          <label class="cf">
+            <span>{{ t('Layer') }}</span>
+            <TypeaheadSelect v-model="pickLayer" :aria-label="t('Layer')" 
:options="layerOptions" :placeholder="t('Any layer')" class="cf-tas" />
+          </label>
+          <label class="cf">
+            <span>{{ t('Service') }}</span>
+            <TypeaheadSelect
+              v-model="pickServiceId" :aria-label="t('Service')" 
:options="serviceOptions" :disabled="!pickLayer || servicesLoading"
+              :placeholder="servicesLoading ? t('Reading…') : t('Any 
service')" class="cf-tas"
+            />
+          </label>
+          <label class="cf">
+            <span>{{ t('Instance') }}</span>
+            <TypeaheadSelect v-model="instanceSel" :aria-label="t('Instance')" 
:options="instanceOptions" :disabled="!pickServiceId" :placeholder="t('All 
instances')" class="cf-tas" />
+          </label>
+          <label class="cf">
+            <span>{{ t('Endpoint') }}</span>
+            <input v-model="endpointQuery" class="cf-input" type="text" 
:disabled="!pickServiceId" :placeholder="t('search…')" />
+            <TypeaheadSelect v-model="endpointSel" :aria-label="t('Endpoint')" 
:options="endpointOptions" :disabled="!pickServiceId" :placeholder="t('All 
endpoints')" class="cf-tas" />
+          </label>
+        </div>
+
+        <div class="iq-grid" v-else>
+          <label class="cf">
+            <span>{{ t('Service name') }}</span>
+            <input v-model="typeService" class="cf-input mono" type="text" 
:placeholder="t('e.g. agent::checkout')" />
+          </label>
+          <label class="cf cf-chk">
+            <span>{{ t('Real') }}</span>
+            <span class="iq-chk"><input v-model="typeReal" type="checkbox" /> 
<small class="dim">{{ t('off = virtual / peer') }}</small></span>
+          </label>
+          <label class="cf">
+            <span>{{ t('Instance') }}</span>
+            <input v-model="typeInstance" class="cf-input" type="text" 
:placeholder="t('optional')" />
+          </label>
+          <label class="cf">
+            <span>{{ t('Endpoint') }}</span>
+            <input v-model="typeEndpoint" class="cf-input" type="text" 
:placeholder="t('optional')" />
+          </label>
+        </div>
       </div>
 
-      <!-- Pick: layer-filtered dropdowns -->
-      <template v-if="entityMode === 'pick'">
-        <label class="ex-f">
-          <span>{{ t('Layer') }}</span>
-          <TypeaheadSelect
-            v-model="pickLayer"
-            :aria-label="t('Layer')"
-            :options="traceLayers.map((l) => ({ value: l.key, label: l.name || 
l.key }))"
-            :placeholder="t('Select a layer')"
-          />
+      <div class="iq-grid">
+        <label class="cf">
+          <span>{{ t('Trace ID') }}</span>
+          <input v-model="cond.traceId" class="cf-input mono" type="text" 
:placeholder="t('paste a trace id')" />
         </label>
-        <label class="ex-f">
-          <span>{{ t('Service') }}</span>
-          <TypeaheadSelect
-            v-model="pickServiceId"
-            :aria-label="t('Service')"
-            :options="serviceOptions"
-            :disabled="!pickLayer || servicesLoading"
-            :placeholder="servicesLoading ? t('Reading…') : t('Select a 
service')"
-          />
+        <label class="cf">
+          <span>{{ t('Status') }}</span>
+          <select v-model="cond.traceState" class="cf-input">
+            <option value="ALL">{{ t('All') }}</option>
+            <option value="SUCCESS">{{ t('Success') }}</option>
+            <option value="ERROR">{{ t('Error') }}</option>
+          </select>
         </label>
-        <label class="ex-f">
-          <span>{{ t('Instance') }}</span>
-          <TypeaheadSelect
-            v-model="instanceSel"
-            :aria-label="t('Instance')"
-            :options="instanceOptions"
-            :disabled="!pickServiceId"
-            :placeholder="t('All instances')"
-          />
+        <label class="cf">
+          <span>{{ t('Order') }}</span>
+          <select v-model="cond.queryOrder" class="cf-input">
+            <option value="BY_START_TIME">{{ t('Newest') }}</option>
+            <option value="BY_DURATION">{{ t('Slowest') }}</option>
+          </select>
         </label>
-        <label class="ex-f cf-wide">
-          <span>{{ t('Endpoint') }}</span>
-          <input
-            v-model="endpointQuery"
-            class="ex-input"
-            type="text"
-            :disabled="!pickServiceId"
-            :placeholder="t('Search endpoints…')"
-          />
-          <TypeaheadSelect
-            v-model="endpointSel"
-            :aria-label="t('Endpoint')"
-            :options="endpointOptions"
-            :disabled="!pickServiceId"
-            :placeholder="t('All endpoints')"
-          />
+        <label class="cf">
+          <span>{{ t('Duration (ms)') }}</span>
+          <span class="cf-range">
+            <input v-model="cond.minDuration" class="cf-input cf-range-num" 
type="number" min="0" :placeholder="t('min')" />
+            <span class="cf-range-sep">–</span>
+            <input v-model="cond.maxDuration" class="cf-input cf-range-num" 
type="number" min="0" :placeholder="t('max')" />
+          </span>
         </label>
-        <button class="ex-link" :disabled="!pickServiceId" 
@click="seedTypeFromPick">{{ t('→ edit as text') }}</button>
-      </template>
-
-      <!-- Type: name + real flag (no layer) -->
-      <template v-else>
-        <label class="ex-f">
-          <span>{{ t('Service name') }}</span>
-          <input v-model="typeService" class="ex-input" type="text" 
:placeholder="t('e.g. agent::checkout')" />
+        <label class="cf cf-wide">
+          <span>{{ t('Tags') }}</span>
+          <input v-model="cond.tags" class="cf-input mono" type="text" 
:placeholder="t('http.status_code=500, …')" />
         </label>
-        <label class="ex-f chk">
-          <input v-model="typeReal" type="checkbox" />
-          <span>{{ t('real') }}</span>
-          <small class="dim">{{ t('agent-reported (off = virtual/peer)') 
}}</small>
+        <label class="cf">
+          <span>{{ t('Time') }}</span>
+          <select v-model.number="cond.windowMinutes" class="cf-input">
+            <option v-for="w in WINDOWS" :key="w" :value="w">{{ w < 60 ? 
`${w}m` : `${w / 60}h` }}</option>
+          </select>
         </label>
-        <label class="ex-f">
-          <span>{{ t('Instance') }}</span>
-          <input v-model="typeInstance" class="ex-input" type="text" 
:placeholder="t('optional')" />
+        <label class="cf">
+          <span>{{ t('Limit') }}</span>
+          <select v-model.number="cond.limit" class="cf-input">
+            <option v-for="l in LIMITS" :key="l" :value="l">{{ l }}</option>
+          </select>
         </label>
-        <label class="ex-f">
-          <span>{{ t('Endpoint') }}</span>
-          <input v-model="typeEndpoint" class="ex-input" type="text" 
:placeholder="t('optional')" />
-        </label>
-      </template>
-    </div>
-
-    <div class="ex-form">
-      <label class="ex-f">
-        <span>{{ t('Trace ID') }}</span>
-        <input v-model="cond.traceId" class="ex-input mono" type="text" 
:placeholder="t('paste a trace id')" />
-      </label>
-      <label class="ex-f">
-        <span>{{ t('Status') }}</span>
-        <select v-model="cond.traceState" class="ex-input">
-          <option value="ALL">{{ t('All') }}</option>
-          <option value="SUCCESS">{{ t('Success') }}</option>
-          <option value="ERROR">{{ t('Error') }}</option>
-        </select>
-      </label>
-      <label class="ex-f">
-        <span>{{ t('Order') }}</span>
-        <select v-model="cond.queryOrder" class="ex-input">
-          <option value="BY_START_TIME">{{ t('Newest') }}</option>
-          <option value="BY_DURATION">{{ t('Slowest') }}</option>
-        </select>
-      </label>
-      <label class="ex-f sm">
-        <span>{{ t('Duration (ms)') }}</span>
-        <span class="ex-dur">
-          <input v-model="cond.minDuration" class="ex-input" type="number" 
min="0" :placeholder="t('min')" />
-          <input v-model="cond.maxDuration" class="ex-input" type="number" 
min="0" :placeholder="t('max')" />
-        </span>
-      </label>
-      <label class="ex-f cf-wide">
-        <span>{{ t('Tags') }}</span>
-        <input v-model="cond.tags" class="ex-input mono" type="text" 
:placeholder="t('http.status_code=500, …')" />
-      </label>
-      <label class="ex-f">
-        <span>{{ t('Time') }}</span>
-        <select v-model.number="cond.windowMinutes" class="ex-input">
-          <option v-for="w in WINDOWS" :key="w" :value="w">{{ w < 60 ? `${w}m` 
: `${w / 60}h` }}</option>
-        </select>
-      </label>
-      <label class="ex-f">
-        <span>{{ t('Limit') }}</span>
-        <select v-model.number="cond.limit" class="ex-input">
-          <option v-for="l in LIMITS" :key="l" :value="l">{{ l }}</option>
-        </select>
-      </label>
-      <button class="ex-run" :disabled="!canRun || running" @click="runQuery">
-        {{ running ? t('Running…') : t('Run query') }}
-      </button>
-    </div>
-
-    <div v-if="resolved" class="ex-resolved">
-      <button class="ex-resolved-tog" @click="showResolved = !showResolved">
-        {{ showResolved ? '▾' : '▸' }} {{ t('Resolved query') }}
-        <span class="dim">{{ resolved.source }}{{ resolved.backend ? ` · 
${resolved.backend}` : '' }}</span>
-      </button>
-      <pre v-if="showResolved" class="ex-resolved-body">{{ 
JSON.stringify(resolved.condition, null, 2) }}</pre>
-    </div>
-
-    <div class="ex-result">
-      <div v-if="!hasQueried" class="ex-empty">{{ t('Pick or type an entity, 
then Run query.') }}</div>
-      <div v-else-if="errorMsg" class="ex-err">{{ errorMsg }}</div>
-      <div v-else-if="rows.length === 0" class="ex-empty">{{ t('No traces in 
this window.') }}</div>
-      <div v-else class="ex-split">
-        <ul class="ex-list">
-          <li
-            v-for="(row, i) in rows"
-            :key="row.key || i"
-            class="ex-row"
-            :class="{ on: selectedTraceId === (row.traceIds[0] ?? ''), err: 
row.isError }"
-            @click="openRow(row)"
-          >
-            <span class="ex-flag" :class="{ err: row.isError }"></span>
-            <span class="ex-ep">{{ row.endpointNames[0] || row.traceIds[0] || 
row.key }}</span>
-            <span class="ex-dur-v">{{ fmtDur(row.duration) }}</span>
-            <span class="ex-time">{{ fmtTime(row.start) }}</span>
-          </li>
-        </ul>
-        <div class="ex-detail">
-          <div v-if="!selectedTraceId" class="ex-empty sm">{{ t('Select a 
trace.') }}</div>
-          <div v-else-if="detailFetching && waterfallSpans.length === 0" 
class="ex-empty sm">{{ t('Reading trace…') }}</div>
-          <div v-else-if="waterfallSpans.length === 0" class="ex-empty sm">{{ 
t('No spans (older than the detail window).') }}</div>
-          <NativeTraceWaterfall
-            v-else
-            :spans="waterfallSpans"
-            :selected-span="selectedSpan"
-            @select-span="selectedSpan = $event"
-          />
+      </div>
+
+      <div class="iq-run-row">
+        <button class="iq-run" :disabled="!canRun || running" 
@click="runQuery">
+          {{ running ? t('Running…') : t('Run query') }}
+        </button>
+        <button v-if="resolved" class="iq-resolved-tog" @click="showResolved = 
!showResolved">
+          {{ showResolved ? '▾' : '▸' }} {{ t('Resolved query') }}
+          <span class="dim">{{ resolved.source }}{{ resolved.backend ? ` · 
${resolved.backend}` : '' }}</span>
+        </button>
+      </div>
+      <pre v-if="resolved && showResolved" class="iq-resolved-body">{{ 
JSON.stringify(resolved.condition, null, 2) }}</pre>
+
+      <div class="iq-result">
+        <div v-if="!hasQueried" class="iq-empty">{{ t('Run a query — name a 
service or leave it blank.') }}</div>
+        <div v-else-if="errorMsg" class="iq-err">{{ errorMsg }}</div>
+        <div v-else-if="rows.length === 0" class="iq-empty">{{ t('No traces in 
this window.') }}</div>
+        <div v-else class="iq-split">
+          <ul class="iq-list">
+            <li
+              v-for="(row, i) in rows" :key="row.key || i" class="iq-row"
+              :class="{ on: selectedTraceId === (row.traceIds[0] ?? ''), err: 
row.isError }" @click="openRow(row)"
+            >
+              <span class="iq-flag" :class="{ err: row.isError }"></span>
+              <span class="iq-ep">{{ row.endpointNames[0] || row.traceIds[0] 
|| row.key }}</span>
+              <span class="iq-dur">{{ fmtDur(row.duration) }}</span>
+              <span class="iq-time">{{ fmtTime(row.start) }}</span>
+            </li>
+          </ul>
+          <div class="iq-detail">
+            <div v-if="!selectedTraceId" class="iq-empty sm">{{ t('Select a 
trace.') }}</div>
+            <div v-else-if="detailFetching && waterfallSpans.length === 0" 
class="iq-empty sm">{{ t('Reading trace…') }}</div>
+            <div v-else-if="waterfallSpans.length === 0" class="iq-empty 
sm">{{ t('No spans (older than the detail window).') }}</div>
+            <NativeTraceWaterfall v-else :spans="waterfallSpans" 
:selected-span="selectedSpan" @select-span="selectedSpan = $event" />
+          </div>
         </div>
       </div>
-    </div>
+    </template>
   </div>
 </template>
 
 <style scoped>
-.ex { display: flex; flex-direction: column; gap: 10px; height: 100%; 
min-height: 0; padding: 14px 16px; }
-.ex-head h1 { font-size: 16px; margin: 0; }
-.ex-sub { color: var(--sw-fg-3); font-size: 12px; margin: 2px 0 0; }
-.ex-bar { display: flex; gap: 12px; align-items: center; }
-.ex-seg { display: inline-flex; border: 1px solid var(--sw-line-2); 
border-radius: 5px; overflow: hidden; }
-.ex-seg.sm button { padding: 3px 10px; }
-.ex-seg button {
-  background: var(--sw-bg-2); color: var(--sw-fg-2); border: none; padding: 
4px 14px;
-  font: inherit; font-size: 12px; cursor: pointer;
-}
-.ex-seg button + button { border-left: 1px solid var(--sw-line-2); }
-.ex-seg button.on { background: var(--sw-accent); color: #fff; }
-.ex-seg button.muted { opacity: 0.45; cursor: not-allowed; }
-.ex-form { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: 
flex-end; }
-.ex-f { display: flex; flex-direction: column; gap: 3px; font-size: 11px; 
color: var(--sw-fg-3); }
-.ex-f.cf-wide { min-width: 240px; flex: 1 1 240px; }
-.ex-f.chk { flex-direction: row; align-items: center; gap: 6px; 
padding-bottom: 4px; }
-.ex-f.chk .dim { color: var(--sw-fg-4); }
-.ex-input {
-  background: var(--sw-bg-2); color: var(--sw-fg-1); border: 1px solid 
var(--sw-line-2);
-  border-radius: 4px; padding: 3px 8px; font: inherit; font-size: 12px; 
min-width: 140px;
-}
-.ex-input.mono { font-family: var(--sw-mono); }
-.ex-dur { display: inline-flex; gap: 6px; }
-.ex-dur .ex-input { min-width: 70px; width: 80px; }
-.ex-link { background: none; border: none; color: var(--sw-accent); font-size: 
11px; cursor: pointer; padding: 0 0 4px; }
-.ex-link:disabled { color: var(--sw-fg-4); cursor: not-allowed; }
-.ex-run {
-  background: var(--sw-accent); color: #fff; border: none; border-radius: 4px;
-  padding: 5px 16px; font: inherit; font-size: 12px; cursor: pointer; height: 
28px;
-}
-.ex-run:disabled { opacity: 0.5; cursor: not-allowed; }
-.ex-resolved { border: 1px solid var(--sw-line); border-radius: 5px; }
-.ex-resolved-tog {
-  width: 100%; text-align: left; background: var(--sw-bg-1); color: 
var(--sw-fg-2);
-  border: none; padding: 5px 10px; font: inherit; font-size: 11px; cursor: 
pointer;
-}
-.ex-resolved-tog .dim { color: var(--sw-fg-4); margin-left: 8px; }
-.ex-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: 180px;
-  border-top: 1px solid var(--sw-line);
-}
-.ex-result { flex: 1; min-height: 0; border: 1px solid var(--sw-line); 
border-radius: 5px; overflow: hidden; }
-.ex-empty, .ex-err { padding: 24px; text-align: center; color: var(--sw-fg-3); 
font-size: 12px; }
-.ex-empty.sm { padding: 14px; }
-.ex-err { color: var(--sw-err); }
-.ex-split { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; 
height: 100%; min-height: 0; }
-.ex-list { list-style: none; margin: 0; padding: 0; overflow: auto; 
border-right: 1px solid var(--sw-line); }
-.ex-row {
-  display: grid; grid-template-columns: 10px 1fr auto auto; gap: 8px; 
align-items: center;
-  padding: 5px 10px; cursor: pointer; font-size: 12px; border-bottom: 1px 
solid var(--sw-line);
+.iq { display: flex; flex-direction: column; gap: 10px; height: 100%; 
min-height: 0; padding: 14px 16px; }
+.iq-head h1 { font-size: 16px; margin: 0; }
+.iq-sub { color: var(--sw-fg-3); font-size: 12px; margin: 2px 0 0; }
+.iq-coming { padding: 40px; text-align: center; color: var(--sw-fg-3); 
font-size: 13px; }
+.iq-bar { display: flex; align-items: center; gap: 10px; }
+.iq-bar-l { font-size: 11px; color: var(--sw-fg-3); font-weight: 500; }
+.seg { display: inline-flex; border: 1px solid var(--sw-line-2); 
border-radius: 5px; overflow: hidden; }
+.seg button { background: var(--sw-bg-2); color: var(--sw-fg-2); border: none; 
padding: 4px 14px; font: inherit; font-size: 12px; cursor: pointer; }
+.seg.sm button { padding: 3px 10px; }
+.seg button + button { border-left: 1px solid var(--sw-line-2); }
+.seg button.on { background: var(--sw-accent); color: #fff; }
+.seg button.muted { opacity: 0.45; cursor: not-allowed; }
+.iq-target { border: 1px solid var(--sw-line); border-radius: 6px; padding: 
8px 10px; }
+.iq-target-h { display: flex; align-items: center; gap: 12px; margin-bottom: 
8px; font-size: 11px; color: var(--sw-fg-2); font-weight: 600; }
+.iq-target-h .dim { color: var(--sw-fg-4); font-weight: 400; }
+.iq-link { background: none; border: none; color: var(--sw-accent); font-size: 
11px; cursor: pointer; padding: 0; margin-left: auto; }
+.iq-link:disabled { color: var(--sw-fg-4); cursor: not-allowed; }
+.iq-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); 
gap: 8px 10px; }
+@media (max-width: 900px) { .iq-grid { grid-template-columns: repeat(2, 
minmax(0, 1fr)); } }
+.cf { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: 
var(--sw-fg-3); font-weight: 500; min-width: 0; }
+.cf.cf-wide { grid-column: span 2; }
+.cf.cf-chk { justify-content: flex-end; }
+.iq-chk { display: inline-flex; align-items: center; gap: 6px; height: 28px; }
+.iq-chk .dim { color: var(--sw-fg-4); }
+.cf-input {
+  height: 28px; padding: 0 8px; background: var(--sw-bg-2); border: 1px solid 
var(--sw-line-2);
+  border-radius: 4px; color: var(--sw-fg-0); font: inherit; font-size: 11px; 
width: 100%; box-sizing: border-box;
 }
-.ex-row:hover { background: var(--sw-bg-2); }
-.ex-row.on { background: var(--sw-bg-3); }
-.ex-flag { width: 8px; height: 8px; border-radius: 50%; background: 
var(--sw-ok, #3a7); }
-.ex-flag.err { background: var(--sw-err); }
-.ex-ep { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.ex-dur-v { font-family: var(--sw-mono); font-size: 11px; color: 
var(--sw-fg-2); }
-.ex-time { font-size: 11px; color: var(--sw-fg-3); }
-.ex-detail { overflow: auto; min-width: 0; }
+.cf-input.mono { font-family: var(--sw-mono); }
+.cf-input:disabled { opacity: 0.5; cursor: not-allowed; }
+.cf-input + .cf-tas { margin-top: 3px; }
+.cf-tas { display: block; width: 100%; }
+.cf-tas :deep(.tas__trigger) { width: 100%; max-width: none; min-width: 0; 
height: 28px; padding: 0 8px; font-size: 11px; background: var(--sw-bg-2); 
border-radius: 4px; }
+.cf-range { display: flex; align-items: center; gap: 4px; }
+.cf-range-num { flex: 1; min-width: 0; }
+.cf-range-sep { color: var(--sw-fg-3); font-size: 12px; flex: 0 0 auto; }
+.iq-run-row { display: flex; align-items: center; gap: 12px; }
+.iq-run { background: var(--sw-accent); color: #fff; border: none; 
border-radius: 4px; padding: 5px 18px; font: inherit; font-size: 12px; cursor: 
pointer; height: 28px; }
+.iq-run:disabled { opacity: 0.5; cursor: not-allowed; }
+.iq-resolved-tog { background: none; border: none; color: var(--sw-fg-3); 
font-size: 11px; cursor: pointer; }
+.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-result { flex: 1; min-height: 0; border: 1px solid var(--sw-line); 
border-radius: 6px; overflow: hidden; }
+.iq-empty, .iq-err { padding: 24px; text-align: center; color: var(--sw-fg-3); 
font-size: 12px; }
+.iq-empty.sm { padding: 14px; }
+.iq-err { color: var(--sw-err); }
+.iq-split { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; 
height: 100%; min-height: 0; }
+.iq-list { list-style: none; margin: 0; padding: 0; overflow: auto; 
border-right: 1px solid var(--sw-line); }
+.iq-row { display: grid; grid-template-columns: 10px 1fr auto auto; gap: 8px; 
align-items: center; padding: 5px 10px; cursor: pointer; font-size: 12px; 
border-bottom: 1px solid var(--sw-line); }
+.iq-row:hover { background: var(--sw-bg-2); }
+.iq-row.on { background: var(--sw-bg-3); }
+.iq-flag { width: 8px; height: 8px; border-radius: 50%; background: 
var(--sw-ok, #3a7); }
+.iq-flag.err { background: var(--sw-err); }
+.iq-ep { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.iq-dur { font-family: var(--sw-mono); font-size: 11px; color: var(--sw-fg-2); 
}
+.iq-time { font-size: 11px; color: var(--sw-fg-3); }
+.iq-detail { overflow: auto; min-width: 0; }
 </style>
diff --git a/apps/ui/src/shell/AppSidebar.vue b/apps/ui/src/shell/AppSidebar.vue
index 69c574a..bca3b92 100644
--- a/apps/ui/src/shell/AppSidebar.vue
+++ b/apps/ui/src/shell/AppSidebar.vue
@@ -272,7 +272,8 @@ const sections = computed<NavSection[]>(() => [
       },
       { icon: 'event', label: t('Capture history'), to: 
'/operate/live-debug/history', verb: 'live-debug:read' },
       { icon: 'metric', label: t('Metrics inspect'), to: '/operate/inspect', 
verb: 'inspect:read' },
-      { icon: 'trace', label: t('Explore'), to: '/operate/explore', verb: 
'explore:read' },
+      { icon: 'trace', label: t('Trace inspect'), to: 
'/operate/trace-inspect', verb: 'traces:read' },
+      { icon: 'log', label: t('Log inspect'), to: '/operate/log-inspect', 
verb: 'logs:read' },
     ],
   },
   {
diff --git a/apps/ui/src/shell/router/index.ts 
b/apps/ui/src/shell/router/index.ts
index ef9c763..f8124e1 100644
--- a/apps/ui/src/shell/router/index.ts
+++ b/apps/ui/src/shell/router/index.ts
@@ -211,15 +211,24 @@ const shellRoutes: RouteRecordRaw[] = [
     component: () => import('@/features/operate/inspect/InspectView.vue'),
     meta: { verb: 'inspect:read' },
   },
-  // Explore — cross-layer trace/log power-tool. Layer-less: the entity is
-  // picked or typed (name + real flag); dispatches by source via
-  // /api/explore/query. Uses the standard query-protocol (always on),
-  // so it is NOT gated on the inspect module like Metrics inspect.
+  // Trace inspect / Log inspect — cross-layer trace/log query power-tools.
+  // Layer-less: the entity is picked or typed (name + real flag) or
+  // omitted; dispatched by source via /api/explore/query. They use the
+  // standard query-protocol (always on), so — unlike Metrics inspect —
+  // they are NOT gated on the OAP inspect module.
   {
-    path: 'operate/explore',
-    name: 'explore',
+    path: 'operate/trace-inspect',
+    name: 'trace-inspect',
     component: () => import('@/features/operate/explore/ExploreView.vue'),
-    meta: { verb: 'explore:read' },
+    props: { kind: 'trace' },
+    meta: { verb: 'traces:read' },
+  },
+  {
+    path: 'operate/log-inspect',
+    name: 'log-inspect',
+    component: () => import('@/features/operate/explore/ExploreView.vue'),
+    props: { kind: 'log' },
+    meta: { verb: 'logs:read' },
   },
   // Data retention (TTL) — query-port read of getRecordsTTL/getMetricsTTL.
   {
diff --git a/packages/api-client/src/explore.ts 
b/packages/api-client/src/explore.ts
index 39061b9..0f3f333 100644
--- a/packages/api-client/src/explore.ts
+++ b/packages/api-client/src/explore.ts
@@ -74,7 +74,9 @@ export interface ExploreRequest {
   traceSource?: ExploreTraceSource;
   /** when kind === 'log' */
   logSource?: ExploreLogSource;
-  entity: ExploreEntity;
+  /** Optional — a trace/log query needs no entity (query all services in
+   *  the window, or filter by trace id / conditions only). */
+  entity?: ExploreEntity;
   window: ExploreWindow;
   pageNum?: number;
   pageSize?: number;


Reply via email to