This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit 2d5d2cc2a7c17e5da2d24c4ad47c8eab676a8081
Author: Logic <[email protected]>
AuthorDate: Sat Aug 29 16:12:03 2026 +0800

    Align Explore workbench with Greptime patterns
---
 web-app/src/assets/i18n/explore/en-us.json         |  9 +++
 web-app/src/assets/i18n/explore/ja-jp.json         |  9 +++
 web-app/src/assets/i18n/explore/pt-br.json         |  9 +++
 web-app/src/assets/i18n/explore/zh-cn.json         |  9 +++
 web-app/src/assets/i18n/explore/zh-tw.json         |  9 +++
 .../components/explore-active-filters.test.tsx     | 77 ++++++++++++++++++
 .../explore/components/explore-active-filters.tsx  | 18 ++++-
 .../components/explore-advanced-filters.tsx        | 33 +++++++-
 ...plore-investigation-responsive-contract.test.ts | 60 +++++++++++++-
 .../explore-investigation-view-primitives.tsx      | 15 +++-
 .../explore-investigation-view.module.css          | 19 ++++-
 .../components/explore-investigation-view.test.tsx | 26 ++++++
 .../components/explore-log-investigation-view.tsx  |  3 +-
 .../explore/components/explore-log-statistics.tsx  |  5 +-
 .../components/explore-query-bar.module.css        | 44 +++++++++--
 .../explore/components/explore-query-bar.tsx       | 90 ++++++++++++++++-----
 .../explore/components/explore-trace-filters.tsx   |  2 +-
 .../explore-trace-investigation-view.tsx           |  3 +-
 .../components/explore-workbench.module.css        | 18 +----
 .../explore/components/explore-workbench.test.tsx  | 92 +++++++++++++++++-----
 .../explore/components/explore-workbench.tsx       | 87 +-------------------
 .../explore/components/log-result.module.css       | 22 +++---
 .../explore/components/log-result.test.tsx         | 11 ++-
 .../components/signal-result-frame.module.css      |  4 +
 .../pages/explore-focused-investigation.test.tsx   | 14 +++-
 .../pages/explore-focused-investigation.tsx        | 18 +----
 .../features/explore/pages/explore-page.test.tsx   | 30 ++++++-
 .../src/features/explore/pages/explore-page.tsx    | 11 +--
 .../explore/pages/explore-state-i18n.test.ts       | 11 +++
 web-app/src/layout/basic/basic-layout.test.tsx     | 67 +++++++++++++++-
 web-app/src/layout/shell/hertzbeat-shell.tsx       | 31 +++++++-
 web-app/src/layout/shell/shell-navigation.tsx      | 21 ++---
 .../runtime/hertzbeat-perses-primitive-frame.tsx   | 10 ++-
 .../runtime/hertzbeat-perses-primitives.module.css | 54 ++++++++++++-
 .../runtime/hertzbeat-perses-primitives.test.tsx   | 22 +++++-
 35 files changed, 745 insertions(+), 218 deletions(-)

diff --git a/web-app/src/assets/i18n/explore/en-us.json 
b/web-app/src/assets/i18n/explore/en-us.json
index 116dfb0994..ebf4a23d37 100644
--- a/web-app/src/assets/i18n/explore/en-us.json
+++ b/web-app/src/assets/i18n/explore/en-us.json
@@ -1,5 +1,6 @@
 {
   "exploreTrace": {
+    "attributeFilter": "Trace attributes, key:value",
     "closeDetail": "Close detail"
   },
   "exploreInvestigation": {
@@ -70,6 +71,14 @@
   },
   "explore": {
     "exactWindow": "Fixed time window",
+    "queryToolbar": "Explore query controls",
+    "filterContext": "{{label}}: {{value}}",
+    "addFilters": "Add filters",
+    "queryLabels": {
+      "metrics": "Metrics query",
+      "logs": "Logs query",
+      "traces": "Traces query"
+    },
     "instanceId": "Service instance ID",
     "httpRouteTemplate": "HTTP route template, for example /checkout",
     "instanceContext": "Instance: {{value}}",
diff --git a/web-app/src/assets/i18n/explore/ja-jp.json 
b/web-app/src/assets/i18n/explore/ja-jp.json
index e4b5ee13a4..3e35e6dee5 100644
--- a/web-app/src/assets/i18n/explore/ja-jp.json
+++ b/web-app/src/assets/i18n/explore/ja-jp.json
@@ -1,5 +1,6 @@
 {
   "exploreTrace": {
+    "attributeFilter": "トレース属性、key:value",
     "closeDetail": "詳細を閉じる"
   },
   "exploreInvestigation": {
@@ -70,6 +71,14 @@
   },
   "explore": {
     "exactWindow": "固定時間範囲",
+    "queryToolbar": "Explore クエリコントロール",
+    "filterContext": "{{label}}: {{value}}",
+    "addFilters": "フィルターを追加",
+    "queryLabels": {
+      "metrics": "メトリクスクエリ",
+      "logs": "ログクエリ",
+      "traces": "トレースクエリ"
+    },
     "instanceId": "サービスインスタンス ID",
     "httpRouteTemplate": "HTTP ルートテンプレート(例: /checkout)",
     "instanceContext": "インスタンス: {{value}}",
diff --git a/web-app/src/assets/i18n/explore/pt-br.json 
b/web-app/src/assets/i18n/explore/pt-br.json
index 6327909208..4b3c130d06 100644
--- a/web-app/src/assets/i18n/explore/pt-br.json
+++ b/web-app/src/assets/i18n/explore/pt-br.json
@@ -1,5 +1,6 @@
 {
   "exploreTrace": {
+    "attributeFilter": "Atributos de trace, chave:valor",
     "closeDetail": "Fechar detalhes"
   },
   "exploreInvestigation": {
@@ -70,6 +71,14 @@
   },
   "explore": {
     "exactWindow": "Intervalo de tempo fixo",
+    "queryToolbar": "Controles de consulta do Explore",
+    "filterContext": "{{label}}: {{value}}",
+    "addFilters": "Adicionar filtros",
+    "queryLabels": {
+      "metrics": "Consulta de métricas",
+      "logs": "Consulta de logs",
+      "traces": "Consulta de traces"
+    },
     "instanceId": "ID da instância do serviço",
     "httpRouteTemplate": "Modelo de rota HTTP, por exemplo /checkout",
     "instanceContext": "Instância: {{value}}",
diff --git a/web-app/src/assets/i18n/explore/zh-cn.json 
b/web-app/src/assets/i18n/explore/zh-cn.json
index 8a77874089..5ee4014d12 100644
--- a/web-app/src/assets/i18n/explore/zh-cn.json
+++ b/web-app/src/assets/i18n/explore/zh-cn.json
@@ -1,5 +1,6 @@
 {
   "exploreTrace": {
+    "attributeFilter": "Trace 属性,key:value",
     "closeDetail": "关闭详情"
   },
   "exploreInvestigation": {
@@ -70,6 +71,14 @@
   },
   "explore": {
     "exactWindow": "固定时间范围",
+    "queryToolbar": "探索查询控件",
+    "filterContext": "{{label}}:{{value}}",
+    "addFilters": "添加筛选条件",
+    "queryLabels": {
+      "metrics": "指标查询",
+      "logs": "日志查询",
+      "traces": "链路查询"
+    },
     "instanceId": "服务实例 ID",
     "httpRouteTemplate": "HTTP 路由模板,例如 /checkout",
     "instanceContext": "实例:{{value}}",
diff --git a/web-app/src/assets/i18n/explore/zh-tw.json 
b/web-app/src/assets/i18n/explore/zh-tw.json
index 0bc5e0eee2..18372544d9 100644
--- a/web-app/src/assets/i18n/explore/zh-tw.json
+++ b/web-app/src/assets/i18n/explore/zh-tw.json
@@ -1,5 +1,6 @@
 {
   "exploreTrace": {
+    "attributeFilter": "Trace 屬性,key:value",
     "closeDetail": "關閉詳情"
   },
   "exploreInvestigation": {
@@ -70,6 +71,14 @@
   },
   "explore": {
     "exactWindow": "固定時間範圍",
+    "queryToolbar": "探索查詢控制項",
+    "filterContext": "{{label}}:{{value}}",
+    "addFilters": "新增篩選條件",
+    "queryLabels": {
+      "metrics": "指標查詢",
+      "logs": "日誌查詢",
+      "traces": "追蹤查詢"
+    },
     "instanceId": "服務執行個體 ID",
     "httpRouteTemplate": "HTTP 路由範本,例如 /checkout",
     "instanceContext": "執行個體:{{value}}",
diff --git 
a/web-app/src/features/explore/components/explore-active-filters.test.tsx 
b/web-app/src/features/explore/components/explore-active-filters.test.tsx
index 0b7b4bc04f..0017ec6537 100644
--- a/web-app/src/features/explore/components/explore-active-filters.test.tsx
+++ b/web-app/src/features/explore/components/explore-active-filters.test.tsx
@@ -22,6 +22,7 @@ import { i18n, initializeI18n, loadLocale } from 
'@/core/i18n/i18n';
 import { QUERY_CONTEXT_FIELDS } from '@/shared/query-context';
 
 import { ExploreActiveFilters } from './explore-active-filters';
+import type { ExploreQuery } from '../model/explore-model';
 
 describe('Explore active filters', () => {
   beforeAll(async () => {
@@ -134,6 +135,82 @@ describe('Explore active filters', () => {
     expect(removeFilter).toHaveBeenCalledWith('hideNoise');
     expect(updateQuery).not.toHaveBeenCalled();
   });
+
+  it.each([
+    [
+      'metric label filter',
+      { signal: 'metrics', timeRange: 'last-30m', metricFilter: 'method=GET' },
+      'Label filter, key=value: method=GET',
+      'metricFilter'
+    ],
+    [
+      'metric group by',
+      { signal: 'metrics', timeRange: 'last-30m', groupBy: 'service.name' },
+      'Group by label: service.name',
+      'groupBy'
+    ],
+    [
+      'metric aggregation',
+      { signal: 'metrics', timeRange: 'last-30m', aggregation: 'sum' },
+      'Aggregation: sum',
+      'aggregation'
+    ],
+    [
+      'metric step',
+      { signal: 'metrics', timeRange: 'last-30m', step: '60' },
+      'Step in seconds, for example 60: 60',
+      'step'
+    ],
+    [
+      'log resource filter',
+      { signal: 'logs', timeRange: 'last-30m', resourceFilter: 
'service.name=checkout' },
+      'Resource attributes, key=value: service.name=checkout',
+      'resourceFilter'
+    ],
+    [
+      'log attribute filter',
+      { signal: 'logs', timeRange: 'last-30m', attributeFilter: 
'http.status_code:500' },
+      'Log attributes, key:value: http.status_code:500',
+      'attributeFilter'
+    ],
+    [
+      'trace resource filter',
+      { signal: 'traces', timeRange: 'last-30m', resourceFilter: 
'service.name=checkout' },
+      'Resource attributes, key=value: service.name=checkout',
+      'resourceFilter'
+    ],
+    [
+      'trace attribute filter',
+      { signal: 'traces', timeRange: 'last-30m', attributeFilter: 
'http.route:/checkout' },
+      'Trace attributes, key:value: http.route:/checkout',
+      'attributeFilter'
+    ],
+    [
+      'trace minimum duration',
+      { signal: 'traces', timeRange: 'last-30m', minDurationMs: 25 },
+      'Minimum duration (ms): 25',
+      'minDurationMs'
+    ],
+    [
+      'trace maximum duration',
+      { signal: 'traces', timeRange: 'last-30m', maxDurationMs: 800 },
+      'Maximum duration (ms): 800',
+      'maxDurationMs'
+    ]
+  ] as const)('shows and removes the applied %s chip', (_name, query, label, 
key) => {
+    const removeFilter = vi.fn(() => true);
+    render(
+      <ExploreActiveFilters
+        query={query as ExploreQuery}
+        t={i18n.t}
+        updateQuery={vi.fn()}
+        removeFilter={removeFilter}
+      />
+    );
+
+    closeFilter(label);
+    expect(removeFilter).toHaveBeenCalledWith(key);
+  });
 });
 
 function closeFilter(label: string) {
diff --git a/web-app/src/features/explore/components/explore-active-filters.tsx 
b/web-app/src/features/explore/components/explore-active-filters.tsx
index 7fe58b4251..cee2c3582a 100644
--- a/web-app/src/features/explore/components/explore-active-filters.tsx
+++ b/web-app/src/features/explore/components/explore-active-filters.tsx
@@ -73,7 +73,7 @@ export function ExploreActiveFilters({ query, t, updateQuery, 
removeFilter }: Pr
 }
 
 function activeFilter(value: unknown, key: keyof ExploreQueryPatch, label: 
string): ActiveFilter[] {
-  return value ? [{ key, label }] : [];
+  return value != null && value !== '' && value !== false ? [{ key, label }] : 
[];
 }
 
 function signalActiveFilters(query: ExploreQuery, t: TFunction): 
ActiveFilter[] {
@@ -84,13 +84,17 @@ function signalActiveFilters(query: ExploreQuery, t: 
TFunction): ActiveFilter[]
         'operationName',
         t('explore.operationContext', { value: query.operationName })
       ),
+      ...contextFilter(query.metricFilter, 'metricFilter', 
t('exploreMetric.filter'), t),
+      ...contextFilter(query.groupBy, 'groupBy', t('exploreMetric.groupBy'), 
t),
+      ...contextFilter(query.aggregation, 'aggregation', 
t('exploreMetric.aggregation'), t),
       ...activeFilter(
         query.temporalAggregation,
         'temporalAggregation',
         t('exploreMetric.temporalAggregationContext', {
           value: 
t(`exploreMetric.temporalAggregationValues.${query.temporalAggregation}`)
         })
-      )
+      ),
+      ...contextFilter(query.step, 'step', t('exploreMetric.step'), t)
     ];
   }
   const trace = activeFilter(query.traceId, 'traceId', 
t('explore.traceIdContext', { value: query.traceId }));
@@ -99,12 +103,18 @@ function signalActiveFilters(query: ExploreQuery, t: 
TFunction): ActiveFilter[]
       ...activeFilter(query.severityText, 'severityText', 
`${t('explore.severity')}: ${query.severityText}`),
       ...trace,
       ...activeFilter(query.spanId, 'spanId', t('explore.spanIdContext', { 
value: query.spanId })),
+      ...contextFilter(query.resourceFilter, 'resourceFilter', 
t('exploreLog.resourceFilter'), t),
+      ...contextFilter(query.attributeFilter, 'attributeFilter', 
t('exploreLog.attributeFilter'), t),
       ...activeFilter(query.hideInternal, 'hideInternal', 
t('exploreLog.hideInternal')),
       ...activeFilter(query.hideNoise, 'hideNoise', t('exploreLog.hideNoise'))
     ];
   }
   return [
     ...trace,
+    ...contextFilter(query.resourceFilter, 'resourceFilter', 
t('exploreLog.resourceFilter'), t),
+    ...contextFilter(query.attributeFilter, 'attributeFilter', 
t('exploreTrace.attributeFilter'), t),
+    ...contextFilter(query.minDurationMs, 'minDurationMs', 
t('exploreTrace.minDuration'), t),
+    ...contextFilter(query.maxDurationMs, 'maxDurationMs', 
t('exploreTrace.maxDuration'), t),
     ...activeFilter(query.errorOnly, 'errorOnly', t('exploreTrace.errorOnly')),
     ...activeFilter(
       query.spanScope,
@@ -116,3 +126,7 @@ function signalActiveFilters(query: ExploreQuery, t: 
TFunction): ActiveFilter[]
     ...activeFilter(query.hideInternal, 'hideInternal', 
t('exploreTrace.hideInternal'))
   ];
 }
+
+function contextFilter(value: unknown, key: keyof ExploreQueryPatch, label: 
string, t: TFunction) {
+  return activeFilter(value, key, t('explore.filterContext', { label, value 
}));
+}
diff --git 
a/web-app/src/features/explore/components/explore-advanced-filters.tsx 
b/web-app/src/features/explore/components/explore-advanced-filters.tsx
index bd0da64764..5634c433b3 100644
--- a/web-app/src/features/explore/components/explore-advanced-filters.tsx
+++ b/web-app/src/features/explore/components/explore-advanced-filters.tsx
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 
+import { FilterOutlined } from '@ant-design/icons';
 import { Input } from 'antd';
 import type { TFunction } from 'i18next';
 
@@ -27,11 +28,37 @@ import { ExploreTraceFilters } from 
'./explore-trace-filters';
 import styles from './explore-query-bar.module.css';
 
 type Props = Pick<ExploreSubmissionViewModel, 'draft' | 'errors' | 
'updateField'> & { t: TFunction };
+type GuidedProps = Pick<ExploreSubmissionViewModel, 'draft' | 'updateField'> & 
{ t: TFunction };
+
+export function ExploreGuidedFilters({ draft, t, updateField }: GuidedProps) {
+  return (
+    <details className={styles.guided} open={hasGuidedFilter(draft) || 
undefined}>
+      <summary aria-label={t('explore.addFilters')}>
+        <FilterOutlined aria-hidden />
+        <span>{t('explore.addFilters')}</span>
+      </summary>
+      <div className={styles.guidedFields}>
+        <Input
+          value={draft.serviceName}
+          aria-label={t('explore.serviceName')}
+          onChange={event => updateField({ field: 'serviceName', value: 
event.target.value })}
+          placeholder={t('explore.serviceName')}
+        />
+        <Input
+          value={draft.environment}
+          aria-label={t('explore.environment')}
+          onChange={event => updateField({ field: 'environment', value: 
event.target.value })}
+          placeholder={t('explore.environment')}
+        />
+      </div>
+    </details>
+  );
+}
 
 export function ExploreAdvancedFilters({ draft, errors, t, updateField }: 
Props) {
   return (
     <details className={styles.advanced} open={hasAdvancedFilter(draft) || 
undefined}>
-      <summary>{t('explore.advancedFilters')}</summary>
+      <summary 
aria-label={t('explore.advancedFilters')}>{t('explore.advancedFilters')}</summary>
       <div className={styles.advancedFields}>
         <Input
           value={draft.instance}
@@ -55,6 +82,10 @@ export function ExploreAdvancedFilters({ draft, errors, t, 
updateField }: Props)
   );
 }
 
+function hasGuidedFilter(draft: ExploreSubmissionViewModel['draft']) {
+  return [draft.serviceName, draft.environment].some(value => value != null && 
value !== '');
+}
+
 function hasAdvancedFilter(draft: ExploreSubmissionViewModel['draft']) {
   return [draft.instance, draft.endpoint, ...signalFilters(draft)].some(
     value => value != null && value !== false && value !== ''
diff --git 
a/web-app/src/features/explore/components/explore-investigation-responsive-contract.test.ts
 
b/web-app/src/features/explore/components/explore-investigation-responsive-contract.test.ts
index 4c039b20ac..4d4fbfacd4 100644
--- 
a/web-app/src/features/explore/components/explore-investigation-responsive-contract.test.ts
+++ 
b/web-app/src/features/explore/components/explore-investigation-responsive-contract.test.ts
@@ -7,7 +7,11 @@ import appStyles from '@/app/styles.css?raw';
 import traceStyles from './explore-investigation-trace.module.css?raw';
 import viewStyles from './explore-investigation-view.module.css?raw';
 import queryStyles from './explore-query-bar.module.css?raw';
+import historyStyles from './explore-history-result.module.css?raw';
+import logStyles from './log-result.module.css?raw';
+import resultFrameStyles from './signal-result-frame.module.css?raw';
 import workspaceStyles from './explore-workbench.module.css?raw';
+import persesStyles from 
'@/platform/perses/runtime/hertzbeat-perses-primitives.module.css?raw';
 
 describe('Explore investigation responsive contract', () => {
   it('keeps the Gantt readable at ordinary desktop widths and contains narrow 
runtime overflow', () => {
@@ -27,20 +31,20 @@ describe('Explore investigation responsive contract', () => 
{
   it('releases the shared desktop body floor only while a narrow focused 
investigation is mounted', () => {
     expect(appStyles).toMatch(/body\s*\{[^}]*min-width:\s*1024px/s);
     expect(viewStyles).toMatch(
-      /@media 
\(max-width:\s*700px\)[\s\S]*:global\(body:has\(\[data-explore-investigation='true'\]\)\)\s*\{[^}]*min-width:\s*0/s
+      /@media 
\(max-width:\s*768px\)[\s\S]*:global\(body:has\(\[data-explore-investigation='true'\]\)\)\s*\{[^}]*min-width:\s*0/s
     );
   });
 
   it('removes secondary shell summaries only from a narrow focused 
investigation header', () => {
     expect(viewStyles).toMatch(
-      /@media 
\(max-width:\s*700px\)[\s\S]*body:has\(\[data-explore-investigation='true'\]\)[\s\S]*\[data-testid='shell-time-policy'\][\s\S]*\[data-testid='shell-status-greptime'\][\s\S]*\[data-testid='shell-status-collector'\][\s\S]*\{[^}]*display:\s*none/s
+      /@media 
\(max-width:\s*768px\)[\s\S]*body:has\(\[data-explore-investigation='true'\]\)[\s\S]*\[data-testid='shell-time-policy'\][\s\S]*\[data-testid='shell-status-greptime'\][\s\S]*\[data-testid='shell-status-collector'\][\s\S]*\{[^}]*display:\s*none/s
     );
     expect(viewStyles).not.toMatch(/\[data-testid='shell-status-server'\]/);
   });
 
-  it('contains ordinary Explore at 700px without changing the desktop shell 
floor', () => {
+  it('contains ordinary Explore through tablet width without changing the 
desktop shell floor', () => {
     expect(workspaceStyles).toMatch(
-      /@media 
\(max-width:\s*700px\)[\s\S]*body:has\(\[data-explore-workspace='true'\]\)[\s\S]*min-width:\s*0/s
+      /@media 
\(max-width:\s*768px\)[\s\S]*body:has\(\[data-explore-workspace='true'\]\)[\s\S]*min-width:\s*0/s
     );
     expect(workspaceStyles).toMatch(
       
/body:has\(\[data-explore-workspace='true'\]\)[\s\S]*\[data-testid='shell-time-policy'\][\s\S]*\[data-testid='shell-status-greptime'\][\s\S]*\[data-testid='shell-status-collector'\][\s\S]*display:\s*none/s
@@ -49,4 +53,52 @@ describe('Explore investigation responsive contract', () => {
     expect(queryStyles).toMatch(/@media 
\(max-width:\s*700px\)[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\)/s);
     expect(appStyles).toMatch(/body\s*\{[^}]*min-width:\s*1024px/s);
   });
+
+  it('keeps Logs overview, trend, and result on flat full-width workbench 
surfaces', () => {
+    
expect(logStyles).toMatch(/\.statistics\s*\{[^}]*flex-direction:\s*column/s);
+    
expect(logStyles).toMatch(/\.statistics\s*>\s*section\s*\{[^}]*border-bottom:\s*1px
 solid/s);
+    
expect(logStyles).not.toMatch(/\.statistics\s*>\s*section\s*\{[^}]*border-radius:/s);
+    
expect(logStyles).not.toMatch(/\.statistics\s*\{[^}]*grid-template-columns:/s);
+  });
+
+  it('flattens embedded Perses cards and keeps narrow host actions on one 
scrollable line', () => {
+    
expect(persesStyles).toMatch(/\.runtime\s+:global\(\.MuiCard-root\)[\s\S]*border-radius:\s*0/s);
+    
expect(persesStyles).toMatch(/\.runtime\s+:global\(\.MuiPaper-root\)[\s\S]*box-shadow:\s*none/s);
+    
expect(persesStyles).toMatch(/\.runtime\s+:global\(\.MuiTableContainer-root\)[^}]*overflow-x:\s*auto/s);
+    expect(persesStyles).toMatch(
+      
/\.runtime\s+:global\(\.MuiCardContent-root\s*>\s*\.MuiBox-root\)[^}]*box-shadow:\s*none/s
+    );
+    
expect(persesStyles).toMatch(/\.interactions\s+li\s*>\s*div\s*\{[^}]*overflow-x:\s*auto/s);
+    
expect(persesStyles).toMatch(/\.interactions\s+button\s*\{[^}]*white-space:\s*nowrap/s);
+  });
+
+  it('keeps narrow Logs readable with stable content width and runtime-local 
scrolling', () => {
+    
expect(historyStyles).toMatch(/\.persesFrame\s*\{[^}]*min-width:\s*0[^}]*overflow:\s*hidden/s);
+    expect(persesStyles).toMatch(
+      
/\.runtime:has\(>\s*:global\(\[data-perses-primitive='logs-table'\]\)\)\s*\{[^}]*overflow-x:\s*auto/s
+    );
+    expect(persesStyles).toMatch(
+      
/\.runtime\s*>\s*:global\(\[data-perses-primitive='logs-table'\]\)\s*\{[^}]*min-width:\s*640px/s
+    );
+  });
+
+  it('gives runtime, completeness, and interactions independent grid rows', () 
=> {
+    
expect(persesStyles).toMatch(/\.primitive\s*\{[^}]*grid-template-rows:\s*360px\s+auto\s+auto/s);
+    expect(persesStyles).toMatch(
+      
/\.primitive\[data-variant='compact'\]\s*\{[^}]*min-height:\s*220px[^}]*grid-template-rows:\s*220px\s+auto\s+auto/s
+    );
+  });
+
+  it('keeps focused signal facts flat inside their single outer surface', () 
=> {
+    expect(viewStyles).toMatch(/\.signalSection\s*\{[^}]*border:\s*1px 
solid[^}]*border-radius:/s);
+    expect(viewStyles).toMatch(/\.fact\s*\{[^}]*border-bottom:\s*1px solid/s);
+    expect(viewStyles).not.toMatch(/\.fact\s*\{[^}]*border:\s*1px solid/s);
+    expect(viewStyles).not.toMatch(/\.fact\s*\{[^}]*border-radius:/s);
+  });
+
+  it('uses tabular figures for ordinary Logs overview and result counts', () 
=> {
+    
expect(logStyles).toMatch(/\.overviewStats\s+dd\s*\{[^}]*font-variant-numeric:\s*tabular-nums/s);
+    expect(queryStyles).toMatch(/\.commandFields\s*\{[^}]*min-width:\s*0/s);
+    
expect(resultFrameStyles).toMatch(/\.identity\s+span\s*\{[^}]*font-variant-numeric:\s*tabular-nums/s);
+  });
 });
diff --git 
a/web-app/src/features/explore/components/explore-investigation-view-primitives.tsx
 
b/web-app/src/features/explore/components/explore-investigation-view-primitives.tsx
index 228ee9e5fd..bccf580b49 100644
--- 
a/web-app/src/features/explore/components/explore-investigation-view-primitives.tsx
+++ 
b/web-app/src/features/explore/components/explore-investigation-view-primitives.tsx
@@ -14,7 +14,15 @@ export type AvailabilityItem = {
   state: InvestigationEvidenceState;
 };
 
-export function InvestigationContextBand({ window, onBack }: { window: 
InvestigationTimeWindow; onBack: () => void }) {
+export function InvestigationContextBand({
+  window,
+  onBack,
+  onRefresh
+}: {
+  window: InvestigationTimeWindow;
+  onBack: () => void;
+  onRefresh: () => void;
+}) {
   const { t } = useInvestigationTranslation();
   const formatter = new Intl.DateTimeFormat(undefined, {
     dateStyle: 'short',
@@ -28,7 +36,10 @@ export function InvestigationContextBand({ window, onBack }: 
{ window: Investiga
         <span>{t('exploreInvestigation.exactWindow')}</span>
         <time>{`${formatter.format(window.from)} – 
${formatter.format(window.to)}`}</time>
       </div>
-      <Button 
onClick={onBack}>{t('exploreInvestigation.actions.backToResults')}</Button>
+      <div className={styles.contextActions}>
+        <Button 
onClick={onBack}>{t('exploreInvestigation.actions.backToResults')}</Button>
+        <Button onClick={onRefresh}>{t('common.refresh')}</Button>
+      </div>
     </header>
   );
 }
diff --git 
a/web-app/src/features/explore/components/explore-investigation-view.module.css 
b/web-app/src/features/explore/components/explore-investigation-view.module.css
index 0f8f5ab5ef..34a49a9819 100644
--- 
a/web-app/src/features/explore/components/explore-investigation-view.module.css
+++ 
b/web-app/src/features/explore/components/explore-investigation-view.module.css
@@ -31,6 +31,12 @@
   color: var(--hb-text-secondary);
 }
 
+.contextActions {
+  display: flex;
+  flex: 0 0 auto;
+  gap: var(--hb-space-2);
+}
+
 .contextBand time {
   overflow-wrap: anywhere;
   font-variant-numeric: tabular-nums;
@@ -103,8 +109,11 @@
   min-width: 0;
   gap: var(--hb-space-1);
   padding: var(--hb-space-3);
-  border: 1px solid var(--hb-border-subtle);
-  border-radius: var(--hb-radius-sm);
+  border-bottom: 1px solid var(--hb-border-subtle);
+}
+
+.fact:last-child {
+  border-bottom: 0;
 }
 
 .fact dt {
@@ -118,7 +127,7 @@
   font-variant-numeric: tabular-nums;
 }
 
-@media (max-width: 700px) {
+@media (max-width: 768px) {
   :global(body:has([data-explore-investigation='true'])) {
     min-width: 0;
   }
@@ -136,6 +145,10 @@
     flex-direction: column;
   }
 
+  .contextActions {
+    flex-wrap: wrap;
+  }
+
   .capabilityGrid,
   .capabilityGrid[data-count='3'],
   .capabilityGrid[data-count='5'] {
diff --git 
a/web-app/src/features/explore/components/explore-investigation-view.test.tsx 
b/web-app/src/features/explore/components/explore-investigation-view.test.tsx
index 95f3a1a617..9d332f0920 100644
--- 
a/web-app/src/features/explore/components/explore-investigation-view.test.tsx
+++ 
b/web-app/src/features/explore/components/explore-investigation-view.test.tsx
@@ -147,6 +147,30 @@ describe('focused Explore investigation presentation', () 
=> {
     expect(screen.getByRole('button', { name: 
i18n.t('exploreInvestigation.actions.backToResults') })).toBeEnabled();
   });
 
+  it('keeps manual refresh beside Back in the exact-window context band', () 
=> {
+    const refresh = vi.fn();
+    renderTrace(traceReady(), { onRefresh: refresh });
+
+    const refreshButton = screen.getByRole('button', { name: 
i18n.t('common.refresh') });
+    const context = refreshButton.closest('header');
+    expect(context).not.toBeNull();
+    fireEvent.click(refreshButton);
+    expect(refresh).toHaveBeenCalledOnce();
+    expect(
+      within(context as HTMLElement).getByRole('button', {
+        name: i18n.t('exploreInvestigation.actions.backToResults')
+      })
+    ).toBeInTheDocument();
+  });
+
+  it('renders facts as one semantic list inside the outer signal surface', () 
=> {
+    renderTrace(traceReady());
+
+    const traces = screen.getByRole('region', { name: 
i18n.t('exploreInvestigation.sections.traces') });
+    expect(within(traces).getAllByRole('term').length).toBeGreaterThan(0);
+    
expect(within(traces).getAllByRole('definition').length).toBeGreaterThan(0);
+  });
+
   it('does not let ready RED evidence mask unavailable service metrics', () => 
{
     const ready = traceReady();
     renderTrace({
@@ -211,6 +235,7 @@ function renderTrace(
         state={state}
         evidenceCurrent
         onBack={vi.fn()}
+        onRefresh={vi.fn()}
         onSelectSpan={vi.fn()}
         onOpenLogs={vi.fn()}
         onOpenMetrics={vi.fn()}
@@ -228,6 +253,7 @@ function renderLog(state: ReadyLog, overrides: 
Partial<React.ComponentProps<type
         state={state}
         evidenceCurrent
         onBack={vi.fn()}
+        onRefresh={vi.fn()}
         onFocusTrace={vi.fn()}
         onOpenMetrics={vi.fn()}
         onOpenTopology={vi.fn()}
diff --git 
a/web-app/src/features/explore/components/explore-log-investigation-view.tsx 
b/web-app/src/features/explore/components/explore-log-investigation-view.tsx
index 0b1cedcc7c..118a2532e6 100644
--- a/web-app/src/features/explore/components/explore-log-investigation-view.tsx
+++ b/web-app/src/features/explore/components/explore-log-investigation-view.tsx
@@ -28,6 +28,7 @@ type Props = {
   state: ReadyState;
   evidenceCurrent: boolean;
   onBack: () => void;
+  onRefresh: () => void;
   onFocusTrace: () => void;
   onOpenMetrics?: (() => void) | undefined;
   onOpenTopology?: (() => void) | undefined;
@@ -47,7 +48,7 @@ export function ExploreLogInvestigationView(props: Props) {
       data-explore-investigation="true"
       aria-label={t('exploreInvestigation.title')}
     >
-      <InvestigationContextBand window={state.route.window} 
onBack={props.onBack} />
+      <InvestigationContextBand window={state.route.window} 
onBack={props.onBack} onRefresh={props.onRefresh} />
       <LogAvailability
         logs={snapshot.selectedLog.state}
         traces={traceEvidenceState(state)}
diff --git a/web-app/src/features/explore/components/explore-log-statistics.tsx 
b/web-app/src/features/explore/components/explore-log-statistics.tsx
index d0e4b25de6..2ecd34b001 100644
--- a/web-app/src/features/explore/components/explore-log-statistics.tsx
+++ b/web-app/src/features/explore/components/explore-log-statistics.tsx
@@ -32,7 +32,7 @@ export function ExploreLogStatistics({
 
 function Overview({ statistics, t }: { statistics: Pick<LogHistoryEvidence, 
'overview'>; t: TFunction }) {
   return (
-    <section aria-label={t('exploreLog.overview')}>
+    <section className={styles.overview} aria-label={t('exploreLog.overview')}>
       <h3>{t('exploreLog.overview')}</h3>
       {statistics.overview.kind === 'error' ? (
         <Alert type="warning" showIcon 
message={t('exploreLog.statisticsUnavailable')} />
@@ -63,7 +63,7 @@ function Trend({
 }) {
   const rows = statistics.trend.kind === 'ready' ? 
Object.keys(statistics.trend.data.hourlyStats) : [];
   return (
-    <section aria-label={t('exploreLog.trend')}>
+    <section className={styles.trend} aria-label={t('exploreLog.trend')}>
       <h3>{t('exploreLog.trend')}</h3>
       {statistics.trend.kind === 'error' ? (
         <Alert type="warning" showIcon 
message={t('exploreLog.statisticsUnavailable')} />
@@ -97,6 +97,7 @@ function TrendResult({
       outcome={result.outcome}
       runtimeIdentity={result.runtimeIdentity}
       messages={explorePersesMessages(t)}
+      variant="compact"
     />
   );
 }
diff --git 
a/web-app/src/features/explore/components/explore-query-bar.module.css 
b/web-app/src/features/explore/components/explore-query-bar.module.css
index 731b27523a..5638c63112 100644
--- a/web-app/src/features/explore/components/explore-query-bar.module.css
+++ b/web-app/src/features/explore/components/explore-query-bar.module.css
@@ -19,10 +19,27 @@
   min-width: 0;
 }
 
-.primaryRow {
-  display: grid;
-  grid-template-columns: minmax(280px, 1.6fr) minmax(180px, 0.8fr) 
minmax(160px, 0.7fr);
+.commandFields {
+  display: flex;
+  min-width: 0;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.queryInput {
+  min-width: 240px;
+  flex: 1 1 360px;
+}
+
+.timeRange {
+  width: 176px;
+  flex: 0 0 176px;
+}
+
+.commandActions {
+  display: flex;
   gap: 8px;
+  white-space: nowrap;
 }
 
 .queryInput :global(input) {
@@ -33,10 +50,12 @@
   font-weight: 600;
 }
 
+.guided,
 .advanced {
   margin-top: 10px;
 }
 
+.guided summary,
 .advanced summary {
   width: max-content;
   color: var(--hb-text-muted);
@@ -45,11 +64,13 @@
   user-select: none;
 }
 
+.guided[open] summary,
 .advanced[open] summary {
   margin-bottom: 10px;
   color: var(--hb-text-secondary);
 }
 
+.guidedFields,
 .advancedFields {
   display: grid;
   grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -88,14 +109,27 @@
 }
 
 @media (max-width: 880px) {
-  .primaryRow,
+  .guidedFields,
   .advancedFields {
     grid-template-columns: repeat(2, minmax(180px, 1fr));
   }
 }
 
 @media (max-width: 700px) {
-  .primaryRow,
+  .commandFields,
+  .commandActions {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .queryInput,
+  .timeRange {
+    width: 100%;
+    min-width: 0;
+    flex-basis: auto;
+  }
+
+  .guidedFields,
   .advancedFields {
     grid-template-columns: minmax(0, 1fr);
   }
diff --git a/web-app/src/features/explore/components/explore-query-bar.tsx 
b/web-app/src/features/explore/components/explore-query-bar.tsx
index ae23bb438c..733f66cfeb 100644
--- a/web-app/src/features/explore/components/explore-query-bar.tsx
+++ b/web-app/src/features/explore/components/explore-query-bar.tsx
@@ -15,29 +15,43 @@
  * limitations under the License.
  */
 
-import { Button, Input } from 'antd';
+import { Button, Input, Select } from 'antd';
 import type { TFunction } from 'i18next';
 
 import { OperationalCommandBar } from '@/shared/operational-page';
+import { globalAutoRefreshValues, type SharedTimeValue } from '@/shared/time';
 
-import type { ExploreQuery, ExploreQueryPatch } from '../model/explore-model';
+import {
+  EXPLORE_TIME_RANGES,
+  exploreUsesExactWindow,
+  presetTimeRangePatch,
+  type ExploreQuery,
+  type ExploreQueryPatch,
+  type ExploreTimeRange
+} from '../model/explore-model';
 import type { ExploreSubmissionViewModel } from 
'../model/explore-submission-model';
 import { ExploreActiveFilters } from './explore-active-filters';
-import { ExploreAdvancedFilters } from './explore-advanced-filters';
+import { ExploreAdvancedFilters, ExploreGuidedFilters } from 
'./explore-advanced-filters';
 import styles from './explore-query-bar.module.css';
 
 type Props = {
   query: ExploreQuery;
   t: TFunction;
   updateQuery: (changes: ExploreQueryPatch) => void;
+  updateScope: (changes: ExploreQueryPatch) => void;
+  refresh: () => Promise<void>;
+  time: SharedTimeValue | null | undefined;
   submission: ExploreSubmissionViewModel;
 };
 
-export function ExploreQueryBar({ query, t, updateQuery, submission }: Props) {
+const EXACT_WINDOW_OPTION = 'exact-window';
+
+export function ExploreQueryBar({ query, t, updateQuery, updateScope, refresh, 
time, submission }: Props) {
   const { draft, errors, updateField } = submission;
   return (
     <form
       className={styles.form}
+      aria-label={t('explore.queryToolbar')}
       onSubmit={event => {
         event.preventDefault();
         submission.submit();
@@ -45,33 +59,73 @@ export function ExploreQueryBar({ query, t, updateQuery, 
submission }: Props) {
     >
       <OperationalCommandBar
         primary={
-          <div className={styles.primaryRow}>
+          <div className={styles.commandFields}>
+            <ExploreTimeRange query={query} t={t} updateScope={updateScope} />
             <Input
               className={styles.queryInput ?? ''}
               value={draft.query}
+              aria-label={t(`explore.queryLabels.${query.signal}`)}
               onChange={event => updateField({ field: 'query', value: 
event.target.value })}
               placeholder={t(`explore.queryPlaceholders.${query.signal}`)}
             />
-            <Input
-              value={draft.serviceName}
-              onChange={event => updateField({ field: 'serviceName', value: 
event.target.value })}
-              placeholder={t('explore.serviceName')}
-            />
-            <Input
-              value={draft.environment}
-              onChange={event => updateField({ field: 'environment', value: 
event.target.value })}
-              placeholder={t('explore.environment')}
-            />
+            <ExploreAutoRefresh query={query} t={t} time={time} />
           </div>
         }
         secondary={
-          <Button className={styles.run ?? ''} type="primary" 
htmlType="submit">
-            {t('common.query')}
-          </Button>
+          <div className={styles.commandActions}>
+            <Button className={styles.run ?? ''} type="primary" 
htmlType="submit">
+              {t('common.query')}
+            </Button>
+            <Button onClick={() => void 
refresh()}>{t('common.refresh')}</Button>
+          </div>
         }
       />
+      <ExploreGuidedFilters draft={draft} t={t} updateField={updateField} />
       <ExploreAdvancedFilters draft={draft} errors={errors} t={t} 
updateField={updateField} />
       <ExploreActiveFilters query={query} t={t} updateQuery={updateQuery} 
removeFilter={submission.removeFilter} />
     </form>
   );
 }
+
+function ExploreTimeRange({ query, t, updateScope }: Pick<Props, 'query' | 't' 
| 'updateScope'>) {
+  const exactWindow = exploreUsesExactWindow(query);
+  const exactOption = exactWindow
+    ? [{ value: EXACT_WINDOW_OPTION, label: t('explore.exactWindow'), 
disabled: true }]
+    : [];
+  return (
+    <Select<string>
+      className={styles.timeRange ?? ''}
+      aria-label={t('explore.timeRange')}
+      value={exactWindow ? EXACT_WINDOW_OPTION : query.timeRange}
+      options={[
+        ...exactOption,
+        ...EXPLORE_TIME_RANGES.map(value => ({ value, label: 
t(`explore.timeRanges.${value}`) }))
+      ]}
+      onChange={value => updateTimeRange(query, value, updateScope)}
+    />
+  );
+}
+
+function ExploreAutoRefresh({ query, t, time }: Pick<Props, 'query' | 't' | 
'time'>) {
+  const fixedWindowFields = query.start != null || query.end != null;
+  if (fixedWindowFields || !time) return null;
+  return (
+    <Select<number>
+      className={styles.timeRange ?? ''}
+      aria-label={autoRefreshLabel(time.autoRefreshMs, t)}
+      value={time.autoRefreshMs}
+      options={globalAutoRefreshValues.map(interval => ({ value: interval, 
label: autoRefreshLabel(interval, t) }))}
+      onChange={interval => time.setAutoRefresh(interval)}
+    />
+  );
+}
+
+function updateTimeRange(query: ExploreQuery, value: string, updateScope: 
Props['updateScope']) {
+  if (!EXPLORE_TIME_RANGES.includes(value as ExploreTimeRange)) return;
+  updateScope(presetTimeRangePatch(query, value as ExploreTimeRange));
+}
+
+function autoRefreshLabel(interval: number, t: TFunction) {
+  if (interval === 0) return t('shell.time.autoRefreshOff');
+  return t('shell.time.autoRefreshSeconds', { seconds: interval / 1_000 });
+}
diff --git a/web-app/src/features/explore/components/explore-trace-filters.tsx 
b/web-app/src/features/explore/components/explore-trace-filters.tsx
index 8aab575b2b..c222979ee4 100644
--- a/web-app/src/features/explore/components/explore-trace-filters.tsx
+++ b/web-app/src/features/explore/components/explore-trace-filters.tsx
@@ -65,7 +65,7 @@ export function ExploreTraceFilters({ draft, errors, t, 
updateField }: Props) {
       <Input
         value={draft.attributeFilter}
         onChange={event => updateField({ field: 'attributeFilter', value: 
event.target.value })}
-        placeholder={t('exploreLog.attributeFilter')}
+        placeholder={t('exploreTrace.attributeFilter')}
       />
       <TraceScopeFilters draft={draft} errors={errors} t={t} 
updateField={updateField} />
       <Checkbox
diff --git 
a/web-app/src/features/explore/components/explore-trace-investigation-view.tsx 
b/web-app/src/features/explore/components/explore-trace-investigation-view.tsx
index 7a85fa4abc..2009fa1ea3 100644
--- 
a/web-app/src/features/explore/components/explore-trace-investigation-view.tsx
+++ 
b/web-app/src/features/explore/components/explore-trace-investigation-view.tsx
@@ -24,6 +24,7 @@ type Props = {
   state: ReadyState;
   evidenceCurrent: boolean;
   onBack: () => void;
+  onRefresh: () => void;
   onSelectSpan: (spanId: string) => void;
   onOpenLogs: () => void;
   onOpenMetrics?: (() => void) | undefined;
@@ -44,7 +45,7 @@ export function ExploreTraceInvestigationView(props: Props) {
       data-explore-investigation="true"
       aria-label={t('exploreInvestigation.title')}
     >
-      <InvestigationContextBand window={state.route.window} 
onBack={props.onBack} />
+      <InvestigationContextBand window={state.route.window} 
onBack={props.onBack} onRefresh={props.onRefresh} />
       <TraceAvailability
         gantt={ganttState}
         logs={logsState}
diff --git 
a/web-app/src/features/explore/components/explore-workbench.module.css 
b/web-app/src/features/explore/components/explore-workbench.module.css
index b0f03d807c..545a354738 100644
--- a/web-app/src/features/explore/components/explore-workbench.module.css
+++ b/web-app/src/features/explore/components/explore-workbench.module.css
@@ -15,17 +15,6 @@
  * limitations under the License.
  */
 
-.scope {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
-  padding-top: 2px;
-}
-
-.timeRange {
-  width: 176px;
-}
-
 .navigationRow {
   display: flex;
   align-items: flex-end;
@@ -82,7 +71,7 @@
   padding-inline: 12px;
 }
 
-@media (max-width: 700px) {
+@media (max-width: 768px) {
   :global(body:has([data-explore-workspace='true'])) {
     min-width: 0;
   }
@@ -93,16 +82,11 @@
     display: none;
   }
 
-  .scope,
   .navigationRow {
     align-items: stretch;
     flex-direction: column;
   }
 
-  .timeRange {
-    width: 100%;
-  }
-
   .signalNavigation {
     justify-content: space-between;
     gap: 12px;
diff --git a/web-app/src/features/explore/components/explore-workbench.test.tsx 
b/web-app/src/features/explore/components/explore-workbench.test.tsx
index 2103b38d2e..a8bfea48cc 100644
--- a/web-app/src/features/explore/components/explore-workbench.test.tsx
+++ b/web-app/src/features/explore/components/explore-workbench.test.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { cleanup, fireEvent, render, screen, within } from 
'@testing-library/react';
 import { I18nextProvider, useTranslation } from 'react-i18next';
 import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
 
@@ -39,7 +39,7 @@ describe('Explore workbench', () => {
     vi.useRealTimers();
   });
 
-  it('keeps signal navigation and shared time scope visible', () => {
+  it('keeps one accessible query command region with query, time, refresh, and 
Run controls', () => {
     const updateQuery = vi.fn();
     render(
       <I18nextProvider i18n={i18n}>
@@ -47,10 +47,15 @@ describe('Explore workbench', () => {
       </I18nextProvider>
     );
 
-    expect(screen.getByRole('combobox', { name: 'Time range' 
})).toBeInTheDocument();
-    expect(screen.getByRole('combobox', { name: /Auto refresh/u 
})).toBeInTheDocument();
-    expect(screen.getByText('Last 30 minutes')).toBeInTheDocument();
-    
expect(screen.getByRole('banner')).toHaveAttribute('data-hb-operational-page-header');
+    const command = screen.getByRole('form', { name: 'Explore query controls' 
});
+    expect(within(command).getByRole('textbox', { name: 'Metrics query' 
})).toBeInTheDocument();
+    expect(within(command).getByRole('combobox', { name: 'Time range' 
})).toBeInTheDocument();
+    expect(within(command).getByRole('combobox', { name: /Auto refresh/u 
})).toBeInTheDocument();
+    expect(within(command).getByRole('button', { name: 'Refresh' 
})).toBeInTheDocument();
+    expect(within(command).getByRole('button', { name: 'Query' 
})).toBeInTheDocument();
+    expect(screen.getByRole('banner')).not.toContainElement(
+      document.querySelector('[data-hb-operational-page-actions]')
+    );
     fireEvent.click(screen.getByRole('tab', { name: 'Metrics' }));
     expect(updateQuery).not.toHaveBeenCalled();
     fireEvent.click(screen.getByRole('tab', { name: 'Logs' }));
@@ -70,7 +75,11 @@ describe('Explore workbench', () => {
       </I18nextProvider>
     );
 
-    fireEvent.mouseDown(screen.getByRole('combobox', { name: /Auto refresh/u 
}));
+    fireEvent.mouseDown(
+      within(screen.getByRole('form', { name: 'Explore query controls' 
})).getByRole('combobox', {
+        name: /Auto refresh/u
+      })
+    );
     fireEvent.click(screen.getByText('Auto refresh 30s'));
     expect(time.setAutoRefresh).toHaveBeenCalledWith(30_000);
   });
@@ -82,6 +91,7 @@ describe('Explore workbench', () => {
       </I18nextProvider>
     );
     expect(screen.getByText('Advanced 
filters').closest('details')).not.toHaveAttribute('open');
+    expect(screen.getByText('Add 
filters').closest('summary')?.querySelector('.anticon-filter')).not.toBeNull();
   });
 
   it('edits optional QueryContext v1 dimensions as an instance and HTTP route 
template', () => {
@@ -97,6 +107,9 @@ describe('Explore workbench', () => {
           query={query}
           t={i18n.t}
           updateQuery={vi.fn()}
+          updateScope={vi.fn()}
+          refresh={vi.fn().mockResolvedValue(undefined)}
+          time={sharedTime()}
           submission={{
             draft: draftFromQuery(query),
             errors: {},
@@ -132,8 +145,35 @@ describe('Explore workbench', () => {
           }}
           t={i18n.t}
           updateQuery={updateQuery}
+        />
+        <ExploreQueryBar
+          query={{
+            signal: 'metrics',
+            timeRange: 'last-30m',
+            serviceName: 'checkout-api',
+            serviceNamespace: 'commerce',
+            environment: 'prod',
+            collectorId: 'collector-east',
+            start: 1_710_000_000_000,
+            end: 1_710_000_005_000
+          }}
+          t={i18n.t}
+          updateQuery={vi.fn()}
+          updateScope={updateQuery}
           refresh={refresh}
           time={sharedTime({ autoRefreshMs: 0 })}
+          submission={{
+            draft: draftFromQuery({
+              signal: 'metrics',
+              timeRange: 'last-30m',
+              start: 1_710_000_000_000,
+              end: 1_710_000_005_000
+            }),
+            errors: {},
+            updateField: vi.fn(),
+            submit: vi.fn(),
+            removeFilter: vi.fn()
+          }}
         />
       </I18nextProvider>
     );
@@ -149,8 +189,6 @@ describe('Explore workbench', () => {
           query={{ signal: 'metrics', timeRange: 'last-30m', collectorId: 
'collector-east', start: 2_000, end: 1_000 }}
           t={i18n.t}
           updateQuery={vi.fn()}
-          refresh={vi.fn().mockResolvedValue(undefined)}
-          time={sharedTime()}
         />
       </I18nextProvider>
     );
@@ -168,13 +206,19 @@ function WorkbenchSubject({
 }) {
   const { t } = useTranslation();
   return (
-    <ExploreWorkbench
-      query={{ signal: 'metrics', timeRange: 'last-30m', query: 
'http_requests_total' }}
-      t={t}
-      updateQuery={updateQuery}
-      refresh={vi.fn().mockResolvedValue(undefined)}
-      time={time}
-    />
+    <>
+      <ExploreWorkbench
+        query={{ signal: 'metrics', timeRange: 'last-30m', query: 
'http_requests_total' }}
+        t={t}
+        updateQuery={updateQuery}
+      />
+      <QuerySubject
+        query={{ signal: 'metrics', timeRange: 'last-30m', query: 
'http_requests_total' }}
+        time={time}
+        updateScope={updateQuery}
+        refresh={vi.fn().mockResolvedValue(undefined)}
+      />
+    </>
   );
 }
 
@@ -196,14 +240,26 @@ function sharedTime(override: Partial<SharedTimeValue> = 
{}): SharedTimeValue {
   };
 }
 
-function QuerySubject() {
+function QuerySubject({
+  query = { signal: 'logs', timeRange: 'last-30m' },
+  time = sharedTime(),
+  updateScope = vi.fn(),
+  refresh = vi.fn().mockResolvedValue(undefined)
+}: {
+  query?: Parameters<typeof draftFromQuery>[0];
+  time?: SharedTimeValue;
+  updateScope?: (changes: ExploreQueryPatch) => void;
+  refresh?: () => Promise<void>;
+} = {}) {
   const { t } = useTranslation();
-  const query = { signal: 'logs', timeRange: 'last-30m' } as const;
   return (
     <ExploreQueryBar
       query={query}
       t={t}
       updateQuery={vi.fn()}
+      updateScope={updateScope}
+      refresh={refresh}
+      time={time}
       submission={{
         draft: draftFromQuery(query),
         errors: {},
diff --git a/web-app/src/features/explore/components/explore-workbench.tsx 
b/web-app/src/features/explore/components/explore-workbench.tsx
index 87a800866a..c9d811bde4 100644
--- a/web-app/src/features/explore/components/explore-workbench.tsx
+++ b/web-app/src/features/explore/components/explore-workbench.tsx
@@ -15,122 +15,43 @@
  * limitations under the License.
  */
 
-import { Button, Select } from 'antd';
+import { Button } from 'antd';
 import type { TFunction } from 'i18next';
 
 import { OperationalPageHeader, OperationalStatePanel } from 
'@/shared/operational-page';
-import { globalAutoRefreshValues, type SharedTimeValue } from '@/shared/time';
 
 import {
-  EXPLORE_TIME_RANGES,
   exploreHandoffState,
-  exploreUsesExactWindow,
-  presetTimeRangePatch,
   signalSelectionPatch,
   type ExploreQuery,
   type ExploreQueryPatch,
-  type ExploreSignal,
-  type ExploreTimeRange
+  type ExploreSignal
 } from '../model/explore-model';
 import styles from './explore-workbench.module.css';
 
 const signalKeys: ExploreSignal[] = ['metrics', 'logs', 'traces'];
-const EXACT_WINDOW_OPTION = 'exact-window';
 
 type Props = {
   query: ExploreQuery;
   t: TFunction;
   updateQuery: (changes: ExploreQueryPatch) => void;
-  refresh: () => Promise<void>;
-  time: SharedTimeValue | null | undefined;
 };
 
-export function ExploreWorkbench({ query, t, updateQuery, refresh, time }: 
Props) {
+export function ExploreWorkbench({ query, t, updateQuery }: Props) {
   const handoffState = exploreHandoffState(query);
-  const exactWindow = exploreUsesExactWindow(query);
-  const fixedWindowFields = query.start != null || query.end != null;
-  const updateTimeRange = (value: string) => {
-    if (!EXPLORE_TIME_RANGES.includes(value as ExploreTimeRange)) return;
-    const timeRange = value as ExploreTimeRange;
-    updateQuery(presetTimeRangePatch(query, timeRange));
-  };
   const selectSignal = (signal: ExploreSignal) => {
     if (query.signal === signal) return;
     updateQuery(signalSelectionPatch(signal));
   };
   return (
     <>
-      <ExploreHeader
-        exactWindow={exactWindow}
-        fixedWindowFields={fixedWindowFields}
-        query={query}
-        refresh={refresh}
-        t={t}
-        time={time}
-        updateTimeRange={updateTimeRange}
-      />
+      <OperationalPageHeader title={t('explore.title')} 
description={t('explore.description')} />
       {handoffState === 'invalid' && <OperationalStatePanel kind="error" 
title={t('explore.handoffInvalid')} />}
       <ExploreSignalNavigation query={query} selectSignal={selectSignal} t={t} 
updateQuery={updateQuery} />
     </>
   );
 }
 
-function ExploreHeader({
-  exactWindow,
-  fixedWindowFields,
-  query,
-  refresh,
-  t,
-  time,
-  updateTimeRange
-}: Pick<Props, 'query' | 'refresh' | 't' | 'time'> & {
-  exactWindow: boolean;
-  fixedWindowFields: boolean;
-  updateTimeRange: (value: string) => void;
-}) {
-  const exactOption = exactWindow
-    ? [{ value: EXACT_WINDOW_OPTION, label: t('explore.exactWindow'), 
disabled: true }]
-    : [];
-  return (
-    <OperationalPageHeader
-      title={t('explore.title')}
-      description={t('explore.description')}
-      actions={
-        <div className={styles.scope} aria-label={t('explore.context')}>
-          <Select<string>
-            className={styles.timeRange ?? ''}
-            aria-label={t('explore.timeRange')}
-            value={exactWindow ? EXACT_WINDOW_OPTION : query.timeRange}
-            options={[
-              ...exactOption,
-              ...EXPLORE_TIME_RANGES.map(value => ({ value, label: 
t(`explore.timeRanges.${value}`) }))
-            ]}
-            onChange={updateTimeRange}
-          />
-          {!fixedWindowFields && time && (
-            <Select<number>
-              className={styles.timeRange ?? ''}
-              aria-label={autoRefreshLabel(time.autoRefreshMs, t)}
-              value={time.autoRefreshMs}
-              options={globalAutoRefreshValues.map(interval => ({
-                value: interval,
-                label: autoRefreshLabel(interval, t)
-              }))}
-              onChange={interval => time.setAutoRefresh(interval)}
-            />
-          )}
-          <Button onClick={() => void refresh()}>{t('common.refresh')}</Button>
-        </div>
-      }
-    />
-  );
-}
-
-function autoRefreshLabel(interval: number, t: TFunction) {
-  if (interval === 0) return t('shell.time.autoRefreshOff');
-  return t('shell.time.autoRefreshSeconds', { seconds: interval / 1_000 });
-}
-
 function ExploreSignalNavigation({
   query,
   selectSignal,
diff --git a/web-app/src/features/explore/components/log-result.module.css 
b/web-app/src/features/explore/components/log-result.module.css
index 2e6faa1573..2593b2b6e3 100644
--- a/web-app/src/features/explore/components/log-result.module.css
+++ b/web-app/src/features/explore/components/log-result.module.css
@@ -48,16 +48,20 @@
 }
 
 .statistics {
-  display: grid;
-  grid-template-columns: minmax(0, 2fr) minmax(240px, 1fr);
-  gap: 12px;
+  display: flex;
+  min-width: 0;
+  flex-direction: column;
   margin-bottom: 12px;
 }
 
 .statistics > section {
-  padding: 12px;
-  border: 1px solid var(--ant-color-border-secondary);
-  border-radius: var(--ant-border-radius);
+  min-width: 0;
+  padding: 12px 0;
+  border-bottom: 1px solid var(--hb-border-subtle);
+}
+
+.statistics > .overview {
+  padding-top: 8px;
 }
 
 .statistics h3 {
@@ -85,19 +89,15 @@
 .overviewStats dd {
   margin: 2px 0 0;
   font-weight: 600;
+  font-variant-numeric: tabular-nums;
 }
 
 .trendChart {
   width: 100%;
-  height: 220px;
   min-width: 0;
 }
 
 @media (width <= 900px) {
-  .statistics {
-    grid-template-columns: 1fr;
-  }
-
   .overviewStats {
     grid-template-columns: repeat(4, minmax(0, 1fr));
   }
diff --git a/web-app/src/features/explore/components/log-result.test.tsx 
b/web-app/src/features/explore/components/log-result.test.tsx
index 5ef84a1642..e5d32d44f3 100644
--- a/web-app/src/features/explore/components/log-result.test.tsx
+++ b/web-app/src/features/explore/components/log-result.test.tsx
@@ -22,6 +22,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } 
from 'vitest';
 
 import { i18n, initializeI18n, loadLocale } from '@/core/i18n/i18n';
 
+import { ExploreLogStatistics } from './explore-log-statistics';
 import { LogResult } from './log-result';
 
 describe('LogResult', () => {
@@ -168,8 +169,7 @@ describe('LogResult', () => {
   it('renders non-empty hourly evidence as an accessible time-series chart 
instead of row-by-row history', () => {
     render(
       <I18nextProvider i18n={i18n}>
-        <LogResult
-          data={{ content: [], totalElements: 0, totalPages: 0, number: 0, 
size: 20 }}
+        <ExploreLogStatistics
           statistics={{
             overview: {
               kind: 'ready',
@@ -188,14 +188,17 @@ describe('LogResult', () => {
               data: { hourlyStats: { '2026-08-06 10:00': 4, '2026-08-06 
11:00': 8 } }
             }
           }}
-          query={{ signal: 'logs', timeRange: 'last-30m' }}
+          timeWindow={{ from: 1_754_467_200_000, to: 1_754_474_400_000 }}
+          runtimeIdentity="logs-trend:revision-1"
           t={i18n.t}
-          navigate={vi.fn()}
         />
       </I18nextProvider>
     );
 
     const trend = screen.getByRole('region', { name: 
i18n.t('exploreLog.trend') });
+    const runtime = 
trend.querySelector('[data-visualization-runtime="perses"]');
+    expect(runtime).toHaveAttribute('data-variant', 'compact');
+    expect(runtime?.firstElementChild).not.toHaveAttribute('style');
     expect(within(trend).getByRole('img', { name: i18n.t('exploreLog.trend') 
})).toBeInTheDocument();
     expect(within(trend).queryByRole('list')).not.toBeInTheDocument();
   });
diff --git 
a/web-app/src/features/explore/components/signal-result-frame.module.css 
b/web-app/src/features/explore/components/signal-result-frame.module.css
index d7bbafc3a4..d620c764d2 100644
--- a/web-app/src/features/explore/components/signal-result-frame.module.css
+++ b/web-app/src/features/explore/components/signal-result-frame.module.css
@@ -47,6 +47,10 @@
   font-size: 12px;
 }
 
+.identity span {
+  font-variant-numeric: tabular-nums;
+}
+
 .meta {
   display: flex;
   gap: 22px;
diff --git 
a/web-app/src/features/explore/pages/explore-focused-investigation.test.tsx 
b/web-app/src/features/explore/pages/explore-focused-investigation.test.tsx
index 24e36cff68..cb8b5f90e4 100644
--- a/web-app/src/features/explore/pages/explore-focused-investigation.test.tsx
+++ b/web-app/src/features/explore/pages/explore-focused-investigation.test.tsx
@@ -20,6 +20,7 @@ vi.mock('../controller/use-log-investigation-controller', () 
=> ({
 vi.mock('../components/explore-trace-investigation-view', () => ({
   ExploreTraceInvestigationView: (props: {
     onBack: () => void;
+    onRefresh: () => void;
     onSelectSpan: (spanId: string) => void;
     onOpenLogs: () => void;
     onOpenMetrics?: (() => void) | undefined;
@@ -27,6 +28,7 @@ vi.mock('../components/explore-trace-investigation-view', () 
=> ({
   }) => (
     <div>
       <button onClick={props.onBack}>trace-back</button>
+      <button onClick={props.onRefresh}>trace-refresh</button>
       <button onClick={() => 
props.onSelectSpan('fedcba9876543210')}>select-span</button>
       <button onClick={props.onOpenLogs}>trace-logs</button>
       {props.onOpenMetrics ? <button 
onClick={props.onOpenMetrics}>trace-metrics</button> : null}
@@ -37,11 +39,13 @@ vi.mock('../components/explore-trace-investigation-view', 
() => ({
 vi.mock('../components/explore-log-investigation-view', () => ({
   ExploreLogInvestigationView: (props: {
     onBack: () => void;
+    onRefresh: () => void;
     onFocusTrace: () => void;
     onOpenTopology?: (() => void) | undefined;
   }) => (
     <div>
       <button onClick={props.onBack}>log-back</button>
+      <button onClick={props.onRefresh}>log-refresh</button>
       <button onClick={props.onFocusTrace}>log-trace</button>
       {props.onOpenTopology ? <button 
onClick={props.onOpenTopology}>log-topology</button> : null}
     </div>
@@ -60,7 +64,8 @@ describe('Explore focused investigation page wiring', () => {
   });
 
   it('uses route builders for Trace actions and returns without focused 
identities', () => {
-    controllers.trace.mockReturnValue({ state: traceReady(), refetch: 
vi.fn().mockResolvedValue(undefined) });
+    const refetch = vi.fn().mockResolvedValue(undefined);
+    controllers.trace.mockReturnValue({ state: traceReady(), refetch });
     const openPath = vi.fn();
     renderSubject(
       <ExploreFocusedTracePage
@@ -96,10 +101,13 @@ describe('Explore focused investigation page wiring', () 
=> {
     fireEvent.click(screen.getByText('trace-back'));
     expect(pathParams(lastPath(openPath))).not.toHaveProperty('traceId');
     expect(pathParams(lastPath(openPath))).not.toHaveProperty('spanId');
+    fireEvent.click(screen.getByText('trace-refresh'));
+    expect(refetch).toHaveBeenCalledOnce();
   });
 
   it('hands a selected Log to its exact Trace and clears the Log anchor on 
return', () => {
-    controllers.log.mockReturnValue({ state: logReady(), refetch: 
vi.fn().mockResolvedValue(undefined) });
+    const refetch = vi.fn().mockResolvedValue(undefined);
+    controllers.log.mockReturnValue({ state: logReady(), refetch });
     const openPath = vi.fn();
     renderSubject(
       <ExploreFocusedLogPage query={logQuery()} t={i18n.t} 
updateQuery={vi.fn()} time={undefined} openPath={openPath} />
@@ -122,6 +130,8 @@ describe('Explore focused investigation page wiring', () => 
{
     });
     fireEvent.click(screen.getByText('log-back'));
     expect(pathParams(lastPath(openPath))).not.toHaveProperty('logRecordUid');
+    fireEvent.click(screen.getByText('log-refresh'));
+    expect(refetch).toHaveBeenCalledOnce();
   });
 
   it('does not offer Log Topology navigation from a trace fallback identity', 
() => {
diff --git 
a/web-app/src/features/explore/pages/explore-focused-investigation.tsx 
b/web-app/src/features/explore/pages/explore-focused-investigation.tsx
index 72864126e9..d8cca71ed1 100644
--- a/web-app/src/features/explore/pages/explore-focused-investigation.tsx
+++ b/web-app/src/features/explore/pages/explore-focused-investigation.tsx
@@ -39,19 +39,14 @@ export function ExploreFocusedTracePage({ query, ...common 
}: CommonProps & { qu
   const topologyPath = state.kind === 'ready' ? 
buildTraceInvestigationTopologyPath(state.snapshot) : undefined;
   return (
     <>
-      <ExploreWorkbench
-        query={query}
-        t={common.t}
-        updateQuery={common.updateQuery}
-        refresh={investigation.refetch}
-        time={common.time}
-      />
+      <ExploreWorkbench query={query} t={common.t} 
updateQuery={common.updateQuery} />
       <OperationalResultRegion>
         {state.kind === 'ready' ? (
           <ExploreTraceInvestigationView
             state={state}
             evidenceCurrent
             onBack={() => common.openPath(backToResultsPath(query))}
+            onRefresh={() => void investigation.refetch()}
             onSelectSpan={spanId => 
common.openPath(buildExplorePath(mergeExploreQuery(query, { spanId })))}
             onOpenLogs={() =>
               common.openPath(
@@ -79,19 +74,14 @@ export function ExploreFocusedLogPage({ query, ...common }: 
CommonProps & { quer
   const topologyPath = state.kind === 'ready' ? 
buildLogInvestigationTopologyPath(state.snapshot) : undefined;
   return (
     <>
-      <ExploreWorkbench
-        query={query}
-        t={common.t}
-        updateQuery={common.updateQuery}
-        refresh={investigation.refetch}
-        time={common.time}
-      />
+      <ExploreWorkbench query={query} t={common.t} 
updateQuery={common.updateQuery} />
       <OperationalResultRegion>
         {state.kind === 'ready' ? (
           <ExploreLogInvestigationView
             state={state}
             evidenceCurrent
             onBack={() => common.openPath(backToResultsPath(query))}
+            onRefresh={() => void investigation.refetch()}
             onFocusTrace={() => common.openPath(logTracePath(query, 
state.snapshot.selectedLog.log))}
             {...(metricsPath ? { onOpenMetrics: () => 
common.openPath(metricsPath) } : {})}
             {...(topologyPath ? { onOpenTopology: () => 
common.openPath(topologyPath) } : {})}
diff --git a/web-app/src/features/explore/pages/explore-page.test.tsx 
b/web-app/src/features/explore/pages/explore-page.test.tsx
index 31809f8a9e..22b45ef09f 100644
--- a/web-app/src/features/explore/pages/explore-page.test.tsx
+++ b/web-app/src/features/explore/pages/explore-page.test.tsx
@@ -87,6 +87,33 @@ describe('ExplorePage instrumentation context boundary', () 
=> {
     );
   });
 
+  it('keeps the default query surface focused and submits guided filters from 
a disclosure', async () => {
+    renderPage('/explore?signal=metrics');
+
+    const primaryQuery = screen.getByRole('textbox', { name: 
i18n.t('explore.queryLabels.metrics') });
+    expect(screen.getByRole('textbox', { name: en.explore.serviceName 
})).not.toBeVisible();
+    expect(screen.getByRole('textbox', { name: en.explore.environment 
})).not.toBeVisible();
+    expect(screen.getByText(en.explore.advancedFilters)).toBeInTheDocument();
+
+    fireEvent.click(screen.getByText(i18n.t('explore.addFilters')));
+    const serviceName = screen.getByRole('textbox', { name: 
en.explore.serviceName });
+    const environment = screen.getByRole('textbox', { name: 
en.explore.environment });
+    fireEvent.change(primaryQuery, { target: { value: 
'http_request_duration_seconds' } });
+    fireEvent.change(serviceName, { target: { value: 'checkout' } });
+    fireEvent.change(environment, { target: { value: 'prod' } });
+    fireEvent.click(querySubmitButton());
+
+    await waitFor(() =>
+      expect(locationParams()).toEqual(
+        expect.objectContaining({
+          query: 'http_request_duration_seconds',
+          serviceName: 'checkout',
+          environment: 'prod'
+        })
+      )
+    );
+  });
+
   it('does not widen partial or reversed instrumentation scope into any signal 
query or SSE stream', async () => {
     const invalidEntries = [
       
'/explore?signal=metrics&intakeProfileId=primary-ingress&serviceName=checkout&serviceNamespace=commerce&start=1000&end=2000',
@@ -318,7 +345,7 @@ describe('ExplorePage instrumentation context boundary', () 
=> {
     cleanup();
     renderPage('/explore?signal=traces');
     fireEvent.click(screen.getByText(en.explore.advancedFilters));
-    const attributeFilter = 
screen.getByPlaceholderText(en.exploreLog.attributeFilter);
+    const attributeFilter = 
screen.getByPlaceholderText(i18n.t('exploreTrace.attributeFilter'));
     fireEvent.change(attributeFilter, { target: { value: 
'http.route=/checkout' } });
     fireEvent.click(screen.getByRole('checkbox', { name: 
en.exploreTrace.errorOnly }));
     expect(locationParams()).not.toHaveProperty('errorOnly');
@@ -345,6 +372,7 @@ describe('ExplorePage instrumentation context boundary', () 
=> {
     await selectOption(spanScope, en.exploreTrace.spanScopeValues.root);
     
expect(screen.getAllByText(en.exploreTrace.spanScopeValues.root).length).toBeGreaterThan(0);
     expect(screen.getByRole('checkbox', { name: en.exploreTrace.hideInternal 
})).toBeInTheDocument();
+    
expect(screen.getByPlaceholderText(i18n.t('exploreTrace.attributeFilter'))).toBeInTheDocument();
 
     cleanup();
     renderPage('/explore?signal=logs');
diff --git a/web-app/src/features/explore/pages/explore-page.tsx 
b/web-app/src/features/explore/pages/explore-page.tsx
index 73387af29b..fdb1cb5a1f 100644
--- a/web-app/src/features/explore/pages/explore-page.tsx
+++ b/web-app/src/features/explore/pages/explore-page.tsx
@@ -68,13 +68,7 @@ function ExploreHistoricalWorkspace({
   return (
     <OperationalPage mode="workspace">
       <div data-explore-workspace="true">
-        <ExploreWorkbench
-          query={controller.query}
-          t={t}
-          updateQuery={controller.updateQuery}
-          refresh={controller.refresh}
-          time={controller.time}
-        />
+        <ExploreWorkbench query={controller.query} t={t} 
updateQuery={controller.updateQuery} />
         <section
           role="tabpanel"
           id={`explore-panel-${controller.query.signal}`}
@@ -84,6 +78,9 @@ function ExploreHistoricalWorkspace({
             query={controller.query}
             t={t}
             updateQuery={controller.updateManualQuery}
+            updateScope={controller.updateQuery}
+            refresh={controller.refresh}
+            time={controller.time}
             submission={controller.submission}
           />
           <OperationalResultRegion>
diff --git a/web-app/src/features/explore/pages/explore-state-i18n.test.ts 
b/web-app/src/features/explore/pages/explore-state-i18n.test.ts
index c97de6d8e4..9ee9c6f703 100644
--- a/web-app/src/features/explore/pages/explore-state-i18n.test.ts
+++ b/web-app/src/features/explore/pages/explore-state-i18n.test.ts
@@ -32,19 +32,30 @@ const persesKeys = [
   'investigateTrace',
   'notRecorded'
 ] as const;
+const queryLabelKeys = ['metrics', 'logs', 'traces'] as const;
 
 describe('Explore result state locale contract', () => {
   it('keeps every honest result state available in all runtime locales', () => 
{
     for (const locale of [en, ja, pt, zhCn, zhTw] as LocaleRoot[]) {
       for (const key of stateKeys) 
expect(locale.explore.states[key]).toEqual(expect.any(String));
       for (const key of persesKeys) 
expect(locale.explore.perses[key]).toEqual(expect.any(String));
+      expect(locale.explore.addFilters).toEqual(expect.any(String));
+      expect(locale.explore.queryToolbar).toEqual(expect.any(String));
+      expect(locale.explore.filterContext).toEqual(expect.any(String));
+      expect(locale.exploreTrace.attributeFilter).toEqual(expect.any(String));
+      for (const key of queryLabelKeys) 
expect(locale.explore.queryLabels[key]).toEqual(expect.any(String));
     }
   });
 });
 
 type LocaleRoot = {
+  exploreTrace: { attributeFilter: string };
   explore: {
     states: Record<(typeof stateKeys)[number], string>;
     perses: Record<(typeof persesKeys)[number], string>;
+    addFilters: string;
+    queryToolbar: string;
+    filterContext: string;
+    queryLabels: Record<(typeof queryLabelKeys)[number], string>;
   };
 };
diff --git a/web-app/src/layout/basic/basic-layout.test.tsx 
b/web-app/src/layout/basic/basic-layout.test.tsx
index 81be8217a7..2f409a5679 100644
--- a/web-app/src/layout/basic/basic-layout.test.tsx
+++ b/web-app/src/layout/basic/basic-layout.test.tsx
@@ -17,9 +17,9 @@
 
 import { Refine } from '@refinedev/core';
 import routerProvider from '@refinedev/react-router';
-import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { act, cleanup, fireEvent, render, screen } from 
'@testing-library/react';
 import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
-import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { Link, MemoryRouter, Route, Routes } from 'react-router-dom';
 
 import { AppProviders } from '@/app/providers';
 import { refineResources, shellAccessControlProvider } from 
'@/app/refine/refine-resource-registry';
@@ -53,7 +53,10 @@ describe('BasicLayout shell', () => {
     await loadLocale('en-US');
   });
 
-  afterEach(cleanup);
+  afterEach(() => {
+    cleanup();
+    vi.unstubAllGlobals();
+  });
 
   it('renders the official logo as one constrained accessible brand identity', 
() => {
     renderLayout();
@@ -124,8 +127,65 @@ describe('BasicLayout shell', () => {
     fireEvent.click(screen.getByRole('button', { name: 'Collapse navigation' 
}));
     
expect(screen.getByTestId('shell-navigation')).toHaveAttribute('data-collapsed',
 'true');
   });
+
+  it('uses the real collapsed navigation on a narrow Explore route without 
overwriting manual state', () => {
+    const viewport = controlledExploreViewport(true);
+    vi.stubGlobal('matchMedia', viewport.matchMedia);
+    renderLayout('/explore', <RouteLinks />);
+
+    
expect(screen.getByTestId('shell-navigation')).toHaveAttribute('data-collapsed',
 'true');
+    expect(screen.queryByRole('button', { name: 'Expand navigation' 
})).not.toBeInTheDocument();
+    act(() => viewport.setNarrow(false));
+    
expect(screen.getByTestId('shell-navigation')).toHaveAttribute('data-collapsed',
 'false');
+    expect(screen.getByRole('button', { name: 'Collapse navigation' 
})).toBeInTheDocument();
+    act(() => viewport.setNarrow(true));
+    fireEvent.click(screen.getByRole('link', { name: 'Leave Explore test 
route' }));
+    
expect(screen.getByTestId('shell-navigation')).toHaveAttribute('data-collapsed',
 'false');
+
+    fireEvent.click(screen.getByRole('button', { name: 'Collapse navigation' 
}));
+    fireEvent.click(screen.getByRole('link', { name: 'Open Explore test route' 
}));
+    act(() => viewport.setNarrow(false));
+    fireEvent.click(screen.getByRole('link', { name: 'Leave Explore test 
route' }));
+    
expect(screen.getByTestId('shell-navigation')).toHaveAttribute('data-collapsed',
 'true');
+  });
 });
 
+function RouteLinks() {
+  return (
+    <>
+      <Link to="/explore">Open Explore test route</Link>
+      <Link to="/alerts">Leave Explore test route</Link>
+    </>
+  );
+}
+
+function controlledExploreViewport(initialNarrow: boolean) {
+  let narrow = initialNarrow;
+  const listeners = new Set<(event: MediaQueryListEvent) => void>();
+  const exploreMedia = {
+    get matches() {
+      return narrow;
+    },
+    media: '(max-width: 768px)',
+    onchange: null,
+    addListener: () => undefined,
+    removeListener: () => undefined,
+    addEventListener: (_type: string, listener: (event: MediaQueryListEvent) 
=> void) => listeners.add(listener),
+    removeEventListener: (_type: string, listener: (event: 
MediaQueryListEvent) => void) => listeners.delete(listener),
+    dispatchEvent: () => false
+  } as MediaQueryList;
+  return {
+    matchMedia: vi.fn((query: string) =>
+      query === exploreMedia.media ? exploreMedia : { ...exploreMedia, 
matches: false, media: query }
+    ),
+    setNarrow(value: boolean) {
+      narrow = value;
+      const event = { matches: value, media: exploreMedia.media } as 
MediaQueryListEvent;
+      listeners.forEach(listener => listener(event));
+    }
+  };
+}
+
 function renderLayout(path = '/alerts', routeElement: React.ReactNode = 
<div>Route content</div>) {
   return render(
     <AppProviders>
@@ -158,6 +218,7 @@ function renderLayout(path = '/alerts', routeElement: 
React.ReactNode = <div>Rou
                   <Route path="/monitors/:monitorId" element={routeElement} />
                   <Route path="/monitors/:monitorId/edit" 
element={routeElement} />
                   <Route path="/alerts" element={routeElement} />
+                  <Route path="/explore" element={routeElement} />
                   <Route path="/observability/integration" 
element={routeElement} />
                   <Route path="/settings/notifications/templates" 
element={routeElement} />
                 </Route>
diff --git a/web-app/src/layout/shell/hertzbeat-shell.tsx 
b/web-app/src/layout/shell/hertzbeat-shell.tsx
index d969a29c7e..26eafeda11 100644
--- a/web-app/src/layout/shell/hertzbeat-shell.tsx
+++ b/web-app/src/layout/shell/hertzbeat-shell.tsx
@@ -5,7 +5,7 @@
  * The ASF licenses this file to You under the Apache License, Version 2.0.
  */
 
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import { useResourceParams } from '@refinedev/core';
 import { Outlet, useLocation } from 'react-router-dom';
 
@@ -19,6 +19,8 @@ import { ShellNavigation } from './shell-navigation';
 import { readShellResourceMeta, resolveShellTimePolicy } from 
'./shell-navigation-model';
 import styles from './hertzbeat-shell.module.css';
 
+const NARROW_EXPLORE_QUERY = '(max-width: 768px)';
+
 export function HertzBeatShell() {
   return (
     <QueryContextProvider>
@@ -30,8 +32,10 @@ export function HertzBeatShell() {
 }
 
 function RouteOwnedShell() {
-  const [collapsed, setCollapsed] = useState(false);
+  const [manualCollapsed, setManualCollapsed] = useState(false);
   const location = useLocation();
+  const routeCollapsed = useNarrowExploreNavigation(location.pathname);
+  const collapsed = manualCollapsed || routeCollapsed;
   const { action, resource } = useResourceParams();
   const policy: TimeOwnership = 
resolveShellTimePolicy(readShellResourceMeta(resource?.meta?.shell), action);
   return (
@@ -44,7 +48,11 @@ function RouteOwnedShell() {
         <div className={`${styles.shell} ${collapsed ? styles.shellCollapsed : 
''}`}>
           <ShellHeader />
           <div className={styles.shellBody}>
-            <ShellNavigation collapsed={collapsed} 
onCollapsedChange={setCollapsed} />
+            <ShellNavigation
+              collapsed={collapsed}
+              collapseLocked={routeCollapsed}
+              onCollapsedChange={setManualCollapsed}
+            />
             <main className={styles.content}>
               <Outlet />
             </main>
@@ -54,3 +62,20 @@ function RouteOwnedShell() {
     </RouteTimeProvider>
   );
 }
+
+function useNarrowExploreNavigation(pathname: string) {
+  const [narrow, setNarrow] = useState(() => matchesNarrowExplore());
+  useEffect(() => {
+    if (typeof window.matchMedia !== 'function') return;
+    const media = window.matchMedia(NARROW_EXPLORE_QUERY);
+    const update = () => setNarrow(media.matches);
+    update();
+    media.addEventListener('change', update);
+    return () => media.removeEventListener('change', update);
+  }, []);
+  return pathname === applicationRoutePaths.explore && narrow;
+}
+
+function matchesNarrowExplore() {
+  return typeof window.matchMedia === 'function' && 
window.matchMedia(NARROW_EXPLORE_QUERY).matches;
+}
diff --git a/web-app/src/layout/shell/shell-navigation.tsx 
b/web-app/src/layout/shell/shell-navigation.tsx
index 89f44ae37a..f4f58627b0 100644
--- a/web-app/src/layout/shell/shell-navigation.tsx
+++ b/web-app/src/layout/shell/shell-navigation.tsx
@@ -20,11 +20,12 @@ import { ShellNavigationLink } from 
'./shell-navigation-link';
 
 type ShellNavigationProps = {
   collapsed: boolean;
+  collapseLocked?: boolean | undefined;
   onCollapsedChange: (collapsed: boolean) => void;
 };
 const noSessionRoles: readonly string[] = [];
 
-export function ShellNavigation({ collapsed, onCollapsedChange }: 
ShellNavigationProps) {
+export function ShellNavigation({ collapsed, collapseLocked = false, 
onCollapsedChange }: ShellNavigationProps) {
   const { t } = useTranslation();
   const { session } = useSession();
   const location = useLocation();
@@ -73,14 +74,16 @@ export function ShellNavigation({ collapsed, 
onCollapsedChange }: ShellNavigatio
           onToggle={toggle}
         />
       </nav>
-      <button
-        className={styles.collapseButton}
-        type="button"
-        aria-label={t(collapsed ? 'shell.navigation.expand' : 
'shell.navigation.collapse')}
-        onClick={() => onCollapsedChange(!collapsed)}
-      >
-        {collapsed ? <RightOutlined /> : <LeftOutlined />}
-      </button>
+      {collapseLocked ? null : (
+        <button
+          className={styles.collapseButton}
+          type="button"
+          aria-label={t(collapsed ? 'shell.navigation.expand' : 
'shell.navigation.collapse')}
+          onClick={() => onCollapsedChange(!collapsed)}
+        >
+          {collapsed ? <RightOutlined /> : <LeftOutlined />}
+        </button>
+      )}
     </aside>
   );
 }
diff --git 
a/web-app/src/platform/perses/runtime/hertzbeat-perses-primitive-frame.tsx 
b/web-app/src/platform/perses/runtime/hertzbeat-perses-primitive-frame.tsx
index 96d98b8ce2..c1daa31c94 100644
--- a/web-app/src/platform/perses/runtime/hertzbeat-perses-primitive-frame.tsx
+++ b/web-app/src/platform/perses/runtime/hertzbeat-perses-primitive-frame.tsx
@@ -31,6 +31,7 @@ export type SharedPrimitiveProps = {
   className?: string | undefined;
   runtimeIdentity?: string | undefined;
   interactions?: HertzBeatPersesTableInteraction[] | undefined;
+  variant?: 'default' | 'compact' | undefined;
 };
 
 export type PrimitiveState<T> =
@@ -80,7 +81,7 @@ export function HertzBeatPrimitiveFrame<T>({ state, 
toRuntimeProps, ...props }:
   }
   const runtimeRole = runtimeProps.kind === 'metric-time-series' ? 'img' : 
'region';
   return (
-    <div className={className} data-visualization-runtime="perses">
+    <div className={className} data-visualization-runtime="perses" 
data-variant={props.variant ?? 'default'}>
       <PersesPrimitiveErrorBoundary
         ariaLabel={props.ariaLabel}
         fallback={props.messages.runtimeError}
@@ -105,7 +106,12 @@ export function HertzBeatPrimitiveFrame<T>({ state, 
toRuntimeProps, ...props }:
 function PrimitiveStateFrame(props: SharedPrimitiveProps & { alert?: boolean; 
children: ReactNode }) {
   const className = [styles.primitive, props.className].filter(Boolean).join(' 
');
   return (
-    <div className={className} role={props.alert ? 'alert' : 'status'} 
aria-label={props.ariaLabel}>
+    <div
+      className={className}
+      role={props.alert ? 'alert' : 'status'}
+      aria-label={props.ariaLabel}
+      data-variant={props.variant ?? 'default'}
+    >
       <div className={styles.state}>{props.children}</div>
     </div>
   );
diff --git 
a/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.module.css 
b/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.module.css
index 7353c730bd..47202c6c34 100644
--- a/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.module.css
+++ b/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.module.css
@@ -5,13 +5,47 @@
   width: 100%;
   min-width: 0;
   min-height: 388px;
-  grid-template-rows: 360px 28px;
+  grid-template-rows: 360px auto auto;
 }
 
 .runtime {
   width: 100%;
+  min-width: 0;
   height: 360px;
   min-height: 360px;
+  overflow: hidden;
+  border-top: 1px solid var(--hb-border-subtle);
+}
+
+.runtime:has(> :global([data-perses-primitive='logs-table'])) {
+  overflow-x: auto;
+  overflow-y: hidden;
+}
+
+.runtime > :global([data-perses-primitive='logs-table']) {
+  min-width: 640px;
+}
+
+.runtime :global(.MuiCard-root),
+.runtime :global(.MuiPaper-root) {
+  border: 0 !important;
+  border-radius: 0 !important;
+  background: transparent !important;
+  box-shadow: none !important;
+}
+
+.runtime :global(.MuiTableContainer-root) {
+  max-width: 100%;
+  overflow-x: auto;
+  border-radius: 0 !important;
+  box-shadow: none !important;
+}
+
+.runtime :global(.MuiCardContent-root > .MuiBox-root) {
+  border: 0 !important;
+  border-radius: 0 !important;
+  background: transparent !important;
+  box-shadow: none !important;
 }
 
 .state {
@@ -21,6 +55,17 @@
   color: var(--hb-text-secondary);
 }
 
+.primitive[data-variant='compact'] {
+  min-height: 220px;
+  grid-template-rows: 220px auto auto;
+}
+
+.primitive[data-variant='compact'] .runtime,
+.primitive[data-variant='compact'] .state {
+  height: 220px;
+  min-height: 220px;
+}
+
 .completeness {
   display: flex;
   box-sizing: border-box;
@@ -64,6 +109,8 @@
   display: flex;
   flex: none;
   gap: 6px;
+  max-width: 100%;
+  overflow-x: auto;
 }
 
 .interactions button {
@@ -73,6 +120,7 @@
   background: var(--hb-bg-raised);
   color: var(--hb-text-primary);
   cursor: pointer;
+  white-space: nowrap;
 }
 
 .interactions button:disabled {
@@ -85,4 +133,8 @@
     align-items: flex-start;
     flex-direction: column;
   }
+
+  .interactions li > div {
+    width: 100%;
+  }
 }
diff --git 
a/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.test.tsx 
b/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.test.tsx
index 49f9a8a980..af5b323e13 100644
--- a/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.test.tsx
+++ b/web-app/src/platform/perses/runtime/hertzbeat-perses-primitives.test.tsx
@@ -20,7 +20,19 @@ vi.mock('./perses-signal-runtime', () => ({
   PersesSignalRuntime: ({ kind }: { kind: string }) => {
     if (runtimeControl.fail) throw new Error('private runtime detail');
     if (kind === 'logs-table' || kind === 'trace-table') {
-      return <table aria-label={`official ${kind}`} 
data-testid={`official-${kind}`} />;
+      return (
+        <section className="MuiCard-root">
+          <div className="MuiCardContent-root">
+            <div
+              className="MuiBox-root"
+              data-testid="official-panel-inner-surface"
+              style={{ borderRadius: 8, boxShadow: '0 2px 8px rgba(0, 0, 0, 
0.2)' }}
+            >
+              <table aria-label={`official ${kind}`} 
data-testid={`official-${kind}`} />
+            </div>
+          </div>
+        </section>
+      );
     }
     if (kind === 'tracing-gantt-chart') {
       return <button data-testid={`official-${kind}`}>Inspect span</button>;
@@ -85,7 +97,7 @@ describe('HertzBeat Perses primitives', () => {
 
     const loading = screen.getByRole('status', { name: 'Logs table' });
     expect(loading).toHaveTextContent('Loading signal');
-    expect(loading).toHaveStyle({ minHeight: '388px', gridTemplateRows: '360px 
28px' });
+    expect(loading).toHaveStyle({ minHeight: '388px', gridTemplateRows: '360px 
auto auto' });
     view.unmount();
     expect(requestSignal?.aborted).toBe(true);
   });
@@ -201,7 +213,7 @@ describe('HertzBeat Perses primitives', () => {
     expect(screen.getByRole('img', { name: 'Metric time series' 
})).toHaveStyle({ height: '360px' });
     
expect(view.container.querySelector('[data-visualization-runtime="perses"]')).toHaveStyle({
       minHeight: '388px',
-      gridTemplateRows: '360px 28px'
+      gridTemplateRows: '360px auto auto'
     });
     expect(screen.getByRole('status', { name: 'Metric time series 
completeness' })).toHaveTextContent(
       'Result completeness is unknown'
@@ -219,6 +231,7 @@ describe('HertzBeat Perses primitives', () => {
     expect(screen.getByRole('region', { name: 'Logs table' 
})).toContainElement(
       screen.getByRole('table', { name: 'official logs-table' })
     );
+    expect(screen.getByTestId('official-panel-inner-surface')).toHaveStyle({ 
borderRadius: '0', boxShadow: 'none' });
     const completeness = screen.getByRole('status', { name: 'Logs table 
completeness' });
     expect(completeness).toHaveTextContent('Results are truncated');
     expect(completeness).toHaveStyle({ height: '28px', minHeight: '28px' });
@@ -277,6 +290,9 @@ describe('HertzBeat Perses primitives', () => {
     fireEvent.click(screen.getByRole('button', { name: 'Investigate log 
checkout ready' }));
     expect(open).toHaveBeenCalledOnce();
     expect(screen.getByRole('button', { name: 'Open trace checkout ready' 
})).toBeDisabled();
+    
expect(view.container.querySelector('[data-visualization-runtime="perses"]')).toHaveStyle({
+      gridTemplateRows: '360px auto auto'
+    });
 
     view.rerender(
       <HertzBeatLogsTableResult


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to