From 2abcf3480e540820c32bfe37e0827efce7ed0b4a Mon Sep 17 00:00:00 2001 From: Amir Raminfar Date: Tue, 2 Jun 2026 09:36:28 -0700 Subject: [PATCH] feat: search progress and completion indicator (#4769) (#4775) Co-authored-by: Claude Opus 4.8 (1M context) --- assets/auto-imports.d.ts | 2 +- assets/components.d.ts | 1 + .../components/LogViewer/EventSource.spec.ts | 27 +++++ assets/components/LogViewer/EventSource.vue | 11 +- .../components/LogViewer/SearchStatus.spec.ts | 108 ++++++++++++++++++ assets/components/LogViewer/SearchStatus.vue | 69 +++++++++++ assets/composable/eventStreams.ts | 27 +++++ assets/composable/historicalLogs.ts | 3 + internal/web/logs.go | 43 ++++++- internal/web/logs_test.go | 88 ++++++++++++++ locales/da.yml | 6 + locales/de.yml | 6 + locales/en.yml | 6 + locales/es.yml | 6 + locales/fr.yml | 6 + locales/id.yml | 6 + locales/it.yml | 6 + locales/ko.yml | 6 + locales/nl.yml | 6 + locales/pl.yml | 6 + locales/pt.yml | 6 + locales/ru.yml | 6 + locales/sl.yml | 6 + locales/tr.yml | 6 + locales/zh-tw.yml | 6 + locales/zh.yml | 6 + plans/search-progress-indicator.md | 108 ++++++++++++++++++ 27 files changed, 578 insertions(+), 5 deletions(-) create mode 100644 assets/components/LogViewer/SearchStatus.spec.ts create mode 100644 assets/components/LogViewer/SearchStatus.vue create mode 100644 plans/search-progress-indicator.md diff --git a/assets/auto-imports.d.ts b/assets/auto-imports.d.ts index 9784eb32..c6b6db12 100644 --- a/assets/auto-imports.d.ts +++ b/assets/auto-imports.d.ts @@ -420,7 +420,7 @@ declare global { export type { DrawerWidth } from './composable/drawer' import('./composable/drawer') // @ts-ignore - export type { LogStreamSource } from './composable/eventStreams' + export type { SearchStatus, LogStreamSource } from './composable/eventStreams' import('./composable/eventStreams') // @ts-ignore export type { ExprEditorOptions } from './composable/exprEditor' diff --git a/assets/components.d.ts b/assets/components.d.ts index 6f570ce2..8e54aceb 100644 --- a/assets/components.d.ts +++ b/assets/components.d.ts @@ -182,6 +182,7 @@ declare module 'vue' { ScrollableView: typeof import('./components/ScrollableView.vue')['default'] ScrollProgress: typeof import('./components/ScrollProgress.vue')['default'] Search: typeof import('./components/Search.vue')['default'] + SearchStatus: typeof import('./components/LogViewer/SearchStatus.vue')['default'] ServiceLog: typeof import('./components/ServiceViewer/ServiceLog.vue')['default'] SideDrawer: typeof import('./components/common/SideDrawer.vue')['default'] SideMenu: typeof import('./components/SideMenu.vue')['default'] diff --git a/assets/components/LogViewer/EventSource.spec.ts b/assets/components/LogViewer/EventSource.spec.ts index ccb67d2c..2a855a10 100644 --- a/assets/components/LogViewer/EventSource.spec.ts +++ b/assets/components/LogViewer/EventSource.spec.ts @@ -9,6 +9,7 @@ import { computed, nextTick } from "vue"; import { createI18n } from "vue-i18n"; import { createRouter, createWebHistory } from "vue-router"; import { default as Component } from "./EventSource.vue"; +import SearchStatus from "./SearchStatus.vue"; import LogViewer from "@/components/LogViewer/LogViewer.vue"; import { Container } from "@/models/Container"; import { Level } from "@/models/LogEntry"; @@ -172,6 +173,32 @@ describe("", () => { expect(message).toMatchSnapshot(); }); + describe("search status", () => { + test("shows no-logs when not searching and the stream is empty", async () => { + const wrapper = createLogEventSource(); + sources[sourceUrl].emitOpen(); + + await vi.advanceTimersByTimeAsync(3500); + await nextTick(); + + expect(wrapper.find('[data-testid="no-logs"]').exists()).toBe(true); + }); + + test("suppresses no-logs while a search is still running", async () => { + const wrapper = createLogEventSource(); + sources[sourceUrl].emitOpen(); + sources[sourceUrl].emit("search-status", { + data: JSON.stringify({ scannedTo: "2026-06-01T14:31:00Z", matches: 0, done: false }), + }); + + vi.advanceTimersByTime(3000); + await nextTick(); + + expect(wrapper.find('[data-testid="no-logs"]').exists()).toBe(false); + expect(wrapper.findComponent(SearchStatus).exists()).toBe(true); + }); + }); + describe("render html correctly", () => { test("should render messages", async () => { const wrapper = createLogEventSource(); diff --git a/assets/components/LogViewer/EventSource.vue b/assets/components/LogViewer/EventSource.vue index 9161c1c2..f7783215 100644 --- a/assets/components/LogViewer/EventSource.vue +++ b/assets/components/LogViewer/EventSource.vue @@ -1,12 +1,13 @@