diff --git a/scripts/translations/.translation_state.json b/scripts/translations/.translation_state.json index f540cc9bd3..cf40e1d22a 100644 --- a/scripts/translations/.translation_state.json +++ b/scripts/translations/.translation_state.json @@ -22599,6 +22599,7 @@ "customRange": "06ea254e73bbab2e", "deleteSavedView": "64d7b3bab0b50cc0", "deleteSavedViewConfirm": "e1555bb4863e8cb2", + "deleteViewNamed": "3cf8146e529f1e42", "deltaScanSize": "81d40c84284d2ec0", "downloadCSV": "eb6b42f54c42d283", "downloadJSON": "db1a21a0bc2ef8fb", @@ -22642,7 +22643,7 @@ "explainResults": "b64075059084248e", "exportLogs": "437c5e8f71a7a1f8", "failedToFetchFieldValues": "65eb66697a2df8b5", - "favoriteViews": "d2e4e43c5b91e51a", + "favoriteViewNamed": "ea8a16e59889a0d2", "fetchingResults": "4d15ef1c91fc8092", "fetchingValues": "c6837b65bbc83780", "fieldMissingInStreams": "c349f2b1b98bca04", @@ -22730,7 +22731,6 @@ "liveModeTooltip": "28bf0972f3888162", "loadingPlan": "687ba06353583216", "logicalPlan": "f9c628db537a449b", - "manageSavedViews": "b70a3b0c5b410752", "maxEventsScheduleJob": "1e33f66246307f84", "maxRecords": "209f9c68099d7e6b", "menuGroupAlerts": "f62e9267d196c761", @@ -22738,7 +22738,6 @@ "menuGroupExplain": "3b61e4e936523550", "menuGroupHistory": "0e76960093379060", "menuGroupInspect": "e0723a86a5b9408a", - "menuGroupSavedViews": "8b4b9a23dfb5e991", "menuGroupSchedule": "f4830a1dae298044", "menuGroupSetOnce": "7722b43b4b6931cf", "menuMore": "d47d7cb0e4f8fd2b", @@ -22797,6 +22796,10 @@ "occurrenceColumnHeader": "37bac4952becf21a", "of": "28391d3bc64ec15c", "original": "88d759ea02cef4b8", + "overwriteSavedView": "484c5ddb5d0abaf3", + "overwriteSavedViewConfirm": "011e1d4e19f77577", + "overwriteSavedViewNamed": "764b55e98873c80e", + "overwriteSavedViewOk": "b24963ea2cbc74b9", "patternAnomalyDetected": "d2bd334c661bf20d", "patternAnomalyLowFreq": "fb473531d3e61eaf", "patternAnomalyRare": "e75077de2370d11b", @@ -22867,9 +22870,19 @@ "runningAnalyze": "1e8f6a027cd4e5cf", "saveFunctionName": "dae596892b2dd17c", "savedFunctionNotFound": "a330ceefc8dc19c1", + "savedViewApplied": "0d3b7a579736eb6d", "savedViewName": "da7a321c73c90d88", + "savedViewsAllGroup": "20b0b5a1811f1f60", + "savedViewsEmpty": "861d00de83e6681c", + "savedViewsFavoritesGroup": "e39e59fb49c815d2", "savedViewsLabel": "edc47cfbced35fc7", - "savedViewsNotFound": "d271ae2e71620984", + "savedViewsNoMatch": "3e4ed5a972a2a1e9", + "savedViewsRowLabel": "8b4b9a23dfb5e991", + "savedViewsSearchCount": "3c0e27d0f74136b8", + "savedViewsSearchPlaceholder": "db71b188cd05760f", + "savedViewsSummary": "a831814a345ed1b1", + "savedViewsSummaryFavorites": "d53b1a67a1dfc3cc", + "savedViewsSummaryViews": "31b4c95468c67eb0", "scanSize": "b41c8c7779b8cc15", "scannedSoFar": "ff88237fc82a362e", "scheduleSearchJob": "21daedde3d118f2b", @@ -22882,7 +22895,6 @@ "searchOperationCancelled": "2429bc389adabe7f", "searchQueryEmptyOrInvalid": "7682ef6f589ed1ea", "searchSavedFunction": "49c266baaaa70981", - "searchSavedView": "db71b188cd05760f", "selectFunctionNamePlaceholder": "185f94c6eda7de48", "selectIndex": "bae161900447ff32", "selectLogStream": "a8e6de79496e8d62", @@ -22952,9 +22964,6 @@ "unknownError": "27c2ccd962c2b8dc", "unknownErrorOccurred": "43c4deec555b4c7e", "unpinFromToolbar": "c9e9cb30a9457d6e", - "updateSavedView": "f0e9e5ac3af3b640", - "updateSavedViewConfirm": "165c5f6398bd57f9", - "updateSavedViewWithCurrent": "9ec1d2751c4654c4", "usageCostHistory": "b97c1a6d83f75800", "userDefinedSchemaLabel": "ad895e214ecd7872", "viewCreatedSuccessfully": "6a620ddf5d4013a9", diff --git a/tests/ui-testing/pages/logsPages/logsPage.js b/tests/ui-testing/pages/logsPages/logsPage.js index fd88dbcfd6..56bd2e76b8 100644 --- a/tests/ui-testing/pages/logsPages/logsPage.js +++ b/tests/ui-testing/pages/logsPages/logsPage.js @@ -124,7 +124,7 @@ export class LogsPage { this.savedViewDialog = '[data-test="search-bar-store-state-saved-view-dialog"]'; this.savedViewArrow = '[data-test="logs-search-bar-utilities-menu-btn"]'; // OInput convention §4: drive the auto-derived `-field` inner native input for fill(). - this.savedViewSearchInput = '[data-test="logs-saved-views-dialog-search-field"]'; + this.savedViewSearchInput = '[data-test="logs-search-bar-menu-saved-views-search"] input'; // Matches both ConfirmDialog.vue (data-test="confirm-dialog") and SearchBar's inline // ODialog (data-test="search-bar-confirm-dialog") via substring selector. this.confirmButton = '[data-test*="confirm-dialog"] [data-test="o-dialog-primary-btn"]'; @@ -135,7 +135,6 @@ export class LogsPage { this.menuTransformEditorToggleBtn = '[data-test="logs-search-bar-menu-transform-editor-toggle-btn"]'; this.menuListSavedViewsBtn = '[data-test="logs-search-bar-menu-list-saved-views-btn"]'; this.menuCreateSavedViewBtn = '[data-test="logs-search-bar-menu-create-saved-view-btn"]'; - this.savedViewsListDialogEl = '[data-test="logs-saved-views-dialog"]'; this.menuHistogramBtn = '[data-test="logs-search-bar-menu-histogram-btn"]'; this.menuSqlModeBtn = '[data-test="logs-search-bar-menu-sql-mode-btn"]'; this.menuSqlModeBtnState = '[data-test="logs-search-bar-menu-sql-mode-btn"] [data-state]'; @@ -3430,22 +3429,7 @@ export class LogsPage { } async clickSavedViewsExpand() { - // CRITICAL: This method is often called after applying a saved view - // - // Problem: When a saved view is applied via applySavedView(): - // 1. The saved views dialog closes immediately (savedViewsListDialog = false) - // 2. applySavedView() fetches the saved view details from API (async) - // 3. It updates searchObj with the saved view's query/filters - // 4. This triggers a NEW SEARCH which updates the logs table - // 5. The search also updates searchObj.data.savedViews (the saved views list) - // - // If we try to reopen the saved views dialog while the search is still running: - // - The dialog's table will be re-rendering as data updates - // - The search input element becomes "unstable" (detached/recreated) - // - Test clicks fail with "element was detached from DOM" - // - // Solution: Wait for the search triggered by applySavedView to complete - // before opening the dialog again. This ensures the dialog opens with stable data. + // Reopening while the search triggered by applying a view is still rendering detaches the menu's elements. try { // Wait for any ongoing search to complete by checking for the logs table @@ -3472,22 +3456,15 @@ export class LogsPage { } async clickSaveViewButton() { - // Post-menu-migration: "Create saved view" moved into utilities ("More") menu. - // Close any open menus/dialogs first, then open the menu and click the item. - const listDialog = this.page.locator(this.savedViewsListDialogEl); - const isListOpen = await listDialog.isVisible({ timeout: 500 }).catch(() => false); - if (isListOpen) { - await listDialog.locator('[data-test="o-dialog-close-btn"]').click(); - await listDialog.waitFor({ state: 'hidden', timeout: 5000 }); - // The overlay has a 200ms fade-out — wait for it to fully detach so - // it doesn't intercept the utilities-menu click. Scoped by data-test. - await this.page.locator('[data-test="o-dialog-overlay"]') - .waitFor({ state: 'detached', timeout: 5000 }); - } - const createSavedViewBtn = this.page.locator('[data-test="logs-search-bar-menu-create-saved-view-btn"]'); - const isVisible = await createSavedViewBtn.isVisible({ timeout: 500 }).catch(() => false); - if (!isVisible) { - await this.page.locator(this.utilitiesMenuButton).click({ force: true }); + // Create lives in the Saved views submenu, which only renders while open. + const createSavedViewBtn = this.page.locator(this.menuCreateSavedViewBtn); + if (!(await createSavedViewBtn.isVisible({ timeout: 500 }).catch(() => false))) { + const submenuTrigger = this.page.locator(this.menuListSavedViewsBtn); + if (!(await submenuTrigger.isVisible().catch(() => false))) { + await this.page.locator(this.utilitiesMenuButton).click({ force: true }); + await submenuTrigger.waitFor({ state: 'visible', timeout: 5000 }); + } + await submenuTrigger.click({ force: true }); await createSavedViewBtn.waitFor({ state: 'visible', timeout: 5000 }); } await createSavedViewBtn.click(); @@ -3585,12 +3562,14 @@ export class LogsPage { return await searchInput.fill(text); } - // The `-apply-` prefix keeps the favourites pane (`-favorites-apply-`) out of the match. + // The `-apply-` prefix keeps the favorite toggle (`-favorite-`) out of the match. savedViewApplyLocator(name) { if (typeof name !== 'string') { throw new TypeError(`savedViewApplyLocator expects the exact view name, got ${name}`); } - return this.page.locator(`[data-test^="logs-saved-views-dialog-apply-"][data-test-view-name="${LogsPage.escapeCssAttrValue(name)}"]`); + return this.page + .locator('[data-test^="logs-search-bar-menu-saved-views-apply-"]') + .filter({ has: this.page.getByText(name, { exact: true }) }); } async clickSavedViewByTitle(title) { @@ -3601,7 +3580,7 @@ export class LogsPage { } async clickDeleteButton() { - const deleteBtn = this.page.locator('[data-test="logs-saved-views-dialog-table"] [data-test^="logs-saved-views-dialog-delete-"]:not([data-test$="-menu"])').first(); + const deleteBtn = this.page.locator('[data-test^="logs-search-bar-menu-saved-views-delete-"]').first(); await deleteBtn.waitFor({ state: 'visible', timeout: 5000 }); return await deleteBtn.click(); } @@ -3681,12 +3660,6 @@ export class LogsPage { } async clickDeleteSavedViewButton(savedViewName) { - // The caller (clickSavedViewByTitle) closes the dialog. Wait passively for it to - // reach hidden state so the re-open sequence doesn't race the close animation. - await this.page.locator(this.savedViewsListDialogEl) - .waitFor({ state: 'hidden', timeout: 5000 }) - .catch(() => {}); - // Re-open the saved views panel (waits for any ongoing search to settle first). await this.clickSavedViewsExpand(); @@ -3702,11 +3675,8 @@ export class LogsPage { // Allow OInput debounce (300 ms) to apply the filter before looking for the row. await this.page.waitForTimeout(1000); - // The delete button data-test uses view_id (a UUID), not the view name, so we - // can't build the exact selector. Instead scope to the main saved-views table - // (not favorites) — after filtering by name there is exactly one visible row. - const mainTable = this.page.locator('[data-test="logs-saved-views-dialog-table"]'); - const deleteBtn = mainTable.locator('[data-test^="logs-saved-views-dialog-delete-"]:not([data-test$="-menu"])').first(); + // The delete data-test uses view_id, not the name, so match by prefix; the name filter leaves one row. + const deleteBtn = this.page.locator('[data-test^="logs-search-bar-menu-saved-views-delete-"]').first(); await deleteBtn.waitFor({ state: 'visible', timeout: 10000 }); await deleteBtn.scrollIntoViewIfNeeded(); await deleteBtn.click(); @@ -9079,7 +9049,7 @@ export class LogsPage { if (!opened) throw new Error('Could not open saved views menu after 3 attempts'); // force: true — ODropdown portal transiently detaches items during initial render await listMenuItem.click({ force: true }); - await this.page.locator(this.savedViewsListDialogEl).waitFor({ state: 'visible', timeout: 10000 }); + await this.page.locator(this.savedViewSearchInput).waitFor({ state: 'visible', timeout: 10000 }); testLogger.info('Clicked saved views dropdown arrow'); } @@ -9099,9 +9069,8 @@ export class LogsPage { * @param {string} name - Saved view name */ async clickDeleteSavedViewByName(name) { - // data-test uses view_id (UUID), not name — scope to main table after filtering. - const mainTable = this.page.locator('[data-test="logs-saved-views-dialog-table"]'); - const deleteBtn = mainTable.locator('[data-test^="logs-saved-views-dialog-delete-"]:not([data-test$="-menu"])').first(); + // data-test uses view_id (UUID), not name; the caller's name filter leaves one row. + const deleteBtn = this.page.locator('[data-test^="logs-search-bar-menu-saved-views-delete-"]').first(); await deleteBtn.waitFor({ state: 'visible', timeout: 10000 }); await deleteBtn.click(); testLogger.info(`Clicked delete for saved view: ${name}`); diff --git a/tests/ui-testing/pages/logsPages/logsToolbarPinsPage.js b/tests/ui-testing/pages/logsPages/logsToolbarPinsPage.js index 5f41424de4..1c9df6fba9 100644 --- a/tests/ui-testing/pages/logsPages/logsToolbarPinsPage.js +++ b/tests/ui-testing/pages/logsPages/logsToolbarPinsPage.js @@ -14,7 +14,6 @@ export class LogsToolbarPinsPage { // ===== STORAGE KEYS (useToolbarPins.ts) ===== this.pinsKey = 'logs_toolbar_pinned_items'; this.histogramDecidedKey = 'logs_toolbar_histogram_pin_decided'; - this.savedViewsDecidedKey = 'logs_toolbar_saved_views_pin_decided'; this.tracesPinsKey = 'traces_toolbar_pinned_items'; // ===== LOCATORS ===== @@ -70,10 +69,10 @@ export class LogsToolbarPinsPage { // ===== SETUP ===== - /** Seeds pin storage before boot; a string `pins` is written verbatim, `decided` sets both default-pin flags. */ + /** Seeds pin storage before boot; a string `pins` is written verbatim, `decided` sets the histogram default-pin flag. */ async seedPinStorage({ pins, decided } = {}) { await this.page.addInitScript( - ([key, value, decidedKeys, decidedValue]) => { + ([key, value, decidedKey, decidedValue]) => { try { // addInitScript re-runs on every navigation, so seed once or a reload // would wipe what the test just pinned. @@ -81,10 +80,8 @@ export class LogsToolbarPinsPage { sessionStorage.setItem('__pinSeedApplied', '1'); if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value); - for (const decidedKey of decidedKeys) { - if (decidedValue === null) localStorage.removeItem(decidedKey); - else localStorage.setItem(decidedKey, decidedValue); - } + if (decidedValue === null) localStorage.removeItem(decidedKey); + else localStorage.setItem(decidedKey, decidedValue); } catch (_) { // Private mode / quota — the app falls back to in-memory pins. } @@ -92,7 +89,7 @@ export class LogsToolbarPinsPage { [ this.pinsKey, pins === undefined ? null : typeof pins === 'string' ? pins : JSON.stringify(pins), - [this.histogramDecidedKey, this.savedViewsDecidedKey], + this.histogramDecidedKey, decided === undefined ? null : String(decided), ], ); @@ -228,10 +225,6 @@ export class LogsToolbarPinsPage { return this.page.evaluate((key) => localStorage.getItem(key), this.histogramDecidedKey); } - async readSavedViewsDecidedFlag() { - return this.page.evaluate((key) => localStorage.getItem(key), this.savedViewsDecidedKey); - } - async readTracesPins() { return this.page.evaluate((key) => localStorage.getItem(key), this.tracesPinsKey); } diff --git a/tests/ui-testing/pages/tracesPages/tracesPage.js b/tests/ui-testing/pages/tracesPages/tracesPage.js index f2ee54bf5d..128c702f7b 100644 --- a/tests/ui-testing/pages/tracesPages/tracesPage.js +++ b/tests/ui-testing/pages/tracesPages/tracesPage.js @@ -192,7 +192,6 @@ export class TracesPage { this.moreMenuButton = '[data-test="traces-search-bar-more-menu-btn"]'; this.openDropdownMenu = page.getByRole('menu'); - this.menuSavedViewsGroup = '[data-test="traces-search-bar-menu-saved-views-group"]'; this.menuPinSavedViewsButton = '[data-test="traces-search-bar-menu-pin-saved-views-btn"]'; this.menuListSavedViewsButton = '[data-test="traces-search-bar-menu-list-saved-views-btn"]'; this.menuCreateSavedViewButton = '[data-test="traces-search-bar-menu-create-saved-view-btn"]'; @@ -200,8 +199,6 @@ export class TracesPage { this.pinnedSavedViewsTrigger = '[data-test="traces-search-bar-saved-views-btn"]'; this.pinnedSavedViewItemPrefix = '[data-test^="traces-saved-view-apply-"]'; this.savedViewNameInput = '[data-test="traces-saved-view-name-input"]'; - this.savedViewsListDialog = '[data-test="traces-saved-views-dialog"]'; - this.savedViewsDialogApplyPrefix = '[data-test^="traces-saved-views-dialog-apply-"]'; this.waterfallAxisLastLabels = '[data-test="trace-header-tic-label-3"]'; this.spanBar = '[data-test="span-marker"]'; @@ -3088,10 +3085,7 @@ export class TracesPage { } async applySavedView(viewName) { - await this.page.locator('[data-test="traces-search-bar-saved-views-btn"]').click(); - const view = this.page.locator('[data-test^="traces-saved-view-apply-"]').filter({ hasText: viewName }).first(); - await view.waitFor({ state: 'visible', timeout: 10000 }); - await view.click(); + await this.applySavedViewFromMoreMenu(viewName); } // enterTraceQuery appends to existing text, so clear first and confirm the editor holds only `query`. @@ -3156,22 +3150,25 @@ export class TracesPage { } async closeMoreMenu() { - await this.page.keyboard.press('Escape'); - await expect(this.openDropdownMenu).toBeHidden({ timeout: 10000 }); + // A nested submenu consumes the first Escape, so repeat until no menu is left. + await expect(async () => { + if (await this.openDropdownMenu.count()) await this.page.keyboard.press('Escape'); + await expect(this.openDropdownMenu).toHaveCount(0, { timeout: 1000 }); + }).toPass({ timeout: 10000 }); } async expectMoreMenuSavedViewsEntriesVisible() { - const group = this.page.locator(this.menuSavedViewsGroup); - await expect(group).toBeVisible({ timeout: 10000 }); - await expect(group.locator(this.menuPinSavedViewsButton)).toBeVisible(); - await expect(group.locator(this.menuListSavedViewsButton)).toContainText(/list saved views/i); - await expect(group.locator(this.menuCreateSavedViewButton)).toContainText(/create saved view/i); + const trigger = this.page.locator(this.menuListSavedViewsButton); + await expect(trigger).toBeVisible({ timeout: 10000 }); + await expect(this.page.locator(this.menuPinSavedViewsButton)).toBeVisible(); + await expect(trigger).toContainText(/saved views/i); + await trigger.click(); + await expect(this.page.locator(this.menuCreateSavedViewButton)).toContainText(/create saved view/i); } /** Asserts on an open menu, so an unopened menu cannot pass for a missing group. */ async expectMoreMenuSavedViewsEntriesHidden() { await expect(this.openDropdownMenu).toBeVisible({ timeout: 10000 }); - await expect(this.page.locator(this.menuSavedViewsGroup)).toHaveCount(0); await expect(this.page.locator(this.menuListSavedViewsButton)).toHaveCount(0); await expect(this.page.locator(this.menuCreateSavedViewButton)).toHaveCount(0); } @@ -3198,6 +3195,7 @@ export class TracesPage { async openCreateSavedViewFromMoreMenu() { await this.openMoreMenu(); + await this.page.locator(this.menuListSavedViewsButton).click(); await this.page.locator(this.menuCreateSavedViewButton).click(); await expect(this.page.locator(this.savedViewNameInput)).toBeVisible({ timeout: 10000 }); } @@ -3209,21 +3207,14 @@ export class TracesPage { await view.click(); } - async openSavedViewsListFromMoreMenu() { + async applySavedViewFromMoreMenu(viewName) { await this.openMoreMenu(); await this.page.locator(this.menuListSavedViewsButton).click(); - await expect(this.page.locator(this.savedViewsListDialog)).toBeVisible({ timeout: 10000 }); - } - - savedViewsDialogApplyCell(viewName) { - return this.page.locator(`${this.savedViewsListDialog} ${this.savedViewsDialogApplyPrefix}[data-test-view-name="${viewName}"]`); - } - - async applySavedViewFromListDialog(viewName) { - const cell = this.savedViewsDialogApplyCell(viewName); - await expect(cell).toBeVisible({ timeout: 15000 }); - await cell.click(); - await expect(this.page.locator(this.savedViewsListDialog)).toBeHidden({ timeout: 10000 }); + const view = this.page + .locator('[data-test^="traces-search-bar-menu-saved-views-apply-"]') + .filter({ has: this.page.getByText(viewName, { exact: true }) }); + await expect(view).toBeVisible({ timeout: 15000 }); + await view.click(); } async expectQueryEditorEventuallyContains(text) { diff --git a/tests/ui-testing/playwright-tests/Logs/logsActionReasons.spec.js b/tests/ui-testing/playwright-tests/Logs/logsActionReasons.spec.js index a0400a2eee..babfdb37fc 100644 --- a/tests/ui-testing/playwright-tests/Logs/logsActionReasons.spec.js +++ b/tests/ui-testing/playwright-tests/Logs/logsActionReasons.spec.js @@ -24,7 +24,10 @@ const notes = { }; const baseAxeNodes = [ { id: 'aria-allowed-attr', target: '[role="menu"] div[aria-haspopup="menu"]:has(> [data-cy="syntax-guide-button"])' }, - { id: 'aria-required-children', target: '[role="menu"]:has([data-test="logs-search-bar-menu-create-saved-view-btn"])' }, + // The utilities menu (pin buttons), the Saved views submenu and the pinned dropdown (both hold a search input) are known non-menuitem children. + { id: 'aria-required-children', target: '[role="menu"]:has([data-test="logs-search-bar-menu-list-saved-views-btn"])' }, + { id: 'aria-required-children', target: '[role="menu"]:has([data-test="logs-search-bar-menu-saved-views-content"])' }, + { id: 'aria-required-children', target: '[role="menu"]:has([data-test="logs-search-bar-saved-views-menu-content"])' }, { id: 'button-name', target: '[data-test="logs-search-bar-sql-mode-toggle-btn"]' }, { id: 'button-name', target: '[data-test="logs-search-bar-show-histogram-toggle-btn-btn"]' }, { id: 'button-name', target: '[data-test="logs-search-bar-quick-mode-switch-btn"]' }, @@ -74,6 +77,7 @@ test.describe('G1 visible action reasons C2', () => { const trigger = page.locator(`[data-test="${menu === 'saved' ? 'logs-search-bar-saved-views-pinned-list-btn' : `logs-search-bar-${menu === 'more' ? 'more-options' : 'utilities-menu'}-btn`}"]`); await trigger.click(); await settleMenu(page); + if (menu === 'utilities') await page.locator('[data-test="logs-search-bar-menu-list-saved-views-btn"]').click(); const scheduled = page.locator('[data-test="search-scheduler-create-new-btn"]'); const scheduleLocked = menu === 'more' && await page.locator('[data-test="search-scheduler-locked-icon"]').count() > 0; // A locked item opens the upgrade dialog, not the query, so it carries no edited-query note. diff --git a/tests/ui-testing/playwright-tests/Logs/logsToolbarPins.spec.js b/tests/ui-testing/playwright-tests/Logs/logsToolbarPins.spec.js index 55c9d7363d..add3beccea 100644 --- a/tests/ui-testing/playwright-tests/Logs/logsToolbarPins.spec.js +++ b/tests/ui-testing/playwright-tests/Logs/logsToolbarPins.spec.js @@ -49,18 +49,6 @@ test.describe('Logs toolbar pins', () => { testLogger.info('Histogram renders pinned on a profile that never chose'); }); - test('saved views is pinned by default until the user decides', { - tag: ['@toolbarPins', '@logs', '@P0', '@all'], - }, async () => { - await pm.logsToolbarPinsPage.seedPinStorage(); - await pm.logsToolbarPinsPage.navigateToLogs(getOrgIdentifier()); - - await pm.logsToolbarPinsPage.expectPinnedVisible('savedViews'); - expect(await pm.logsToolbarPinsPage.readSavedViewsDecidedFlag()).toBeNull(); - - testLogger.info('Saved views renders pinned on a profile that never chose'); - }); - test('unpinning the histogram survives a reload', { tag: ['@toolbarPins', '@logs', '@P0', '@all'], }, async () => { @@ -138,7 +126,7 @@ test.describe('Logs toolbar pins', () => { // this moves by nav-rail click, never page.goto. The nav helpers assert a // document stamp survives, which enforces that. const pins = pm.logsToolbarPinsPage; - await pins.seedPinStorage({ pins: [], decided: true }); + await pins.seedPinStorage(); await pins.navigateToLogs(getOrgIdentifier()); await pins.stampDocument(); @@ -147,15 +135,14 @@ test.describe('Logs toolbar pins', () => { expect(await pins.readPins()).toContain('quickMode'); await pins.gotoTracesViaNav(); - // Its own default being pinned proves the traces scope really instantiated. - await pins.expectTracesPinState('savedViews', true); + // Traces has no default pins, and logs' quickMode pin must not leak into it. + await pins.expectTracesPinState('savedViews', false); await pins.expectTracesPinState('histogram', false); await pins.expectTracesMetricsPinnedHidden(); await pins.toggleTracesPin('histogram'); const tracesPins = JSON.parse((await pins.readTracesPins()) ?? 'null'); - expect(tracesPins).toContain('savedViews'); - expect(tracesPins).toContain('histogram'); + expect(tracesPins).toEqual(['histogram']); expect(tracesPins).not.toContain('quickMode'); // Same document, so this reads the live shared map, not storage. diff --git a/tests/ui-testing/playwright-tests/RegressionSet/Traces/traces-15189-saved-views-more-menu.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/Traces/traces-15189-saved-views-more-menu.spec.js index 561f782c92..1254da9fd4 100644 --- a/tests/ui-testing/playwright-tests/RegressionSet/Traces/traces-15189-saved-views-more-menu.spec.js +++ b/tests/ui-testing/playwright-tests/RegressionSet/Traces/traces-15189-saved-views-more-menu.spec.js @@ -51,7 +51,7 @@ test.describe('Traces saved views in the More menu with a pin (#15189)', () => { testLogger.testEnd(testInfo.title, testInfo.status); }); - test('the More menu on Traces search offers "List saved views" and "Create saved view" with a pin', { + test('the More menu on Traces search offers a Saved views submenu with "Create saved view" and a pin', { tag: ['@bug-15189', '@P1', '@regression', '@tracesRegression'], }, async () => { await pm.tracesPage.openMoreMenu(); @@ -62,26 +62,15 @@ test.describe('Traces saved views in the More menu with a pin (#15189)', () => { testLogger.info('Create saved view opened from the More menu'); }); - test('unpinning moves saved views out of the toolbar, pinning brings them back, and both stick after reload', { + test('saved views are not pinned by default, pinning shows them in the toolbar, unpinning removes them, and both stick after reload', { tag: ['@bug-15189', '@P1', '@regression', '@tracesRegression'], }, async () => { - await pm.tracesPage.expectPinnedSavedViewsInToolbar(); - - await pm.tracesPage.toggleSavedViewsPinFromMoreMenu(); - await pm.tracesPage.expectSavedViewsPinTitle(PIN_TITLE); - await pm.tracesPage.closeMoreMenu(); await pm.tracesPage.expectPinnedSavedViewsNotInToolbar(); - await pm.tracesPage.openMoreMenu(); + await pm.tracesPage.expectSavedViewsPinTitle(PIN_TITLE); await pm.tracesPage.expectMoreMenuSavedViewsEntriesVisible(); await pm.tracesPage.closeMoreMenu(); - await pm.tracesPage.reloadPage(); - await pm.tracesPage.openMoreMenu(); - await pm.tracesPage.expectSavedViewsPinTitle(PIN_TITLE); - await pm.tracesPage.closeMoreMenu(); - await pm.tracesPage.expectPinnedSavedViewsNotInToolbar(); - await pm.tracesPage.toggleSavedViewsPinFromMoreMenu(); await pm.tracesPage.expectSavedViewsPinTitle(UNPIN_TITLE); await pm.tracesPage.closeMoreMenu(); @@ -89,27 +78,38 @@ test.describe('Traces saved views in the More menu with a pin (#15189)', () => { await pm.tracesPage.reloadPage(); await pm.tracesPage.expectPinnedSavedViewsInToolbar(); + + await pm.tracesPage.toggleSavedViewsPinFromMoreMenu(); + await pm.tracesPage.expectSavedViewsPinTitle(PIN_TITLE); + await pm.tracesPage.closeMoreMenu(); + await pm.tracesPage.expectPinnedSavedViewsNotInToolbar(); + + await pm.tracesPage.reloadPage(); + await pm.tracesPage.expectPinnedSavedViewsNotInToolbar(); }); test('a saved view applies from the pinned toolbar dropdown', { tag: ['@bug-15189', '@P1', '@regression', '@tracesRegression'], }, async () => { + await pm.tracesPage.toggleSavedViewsPinFromMoreMenu(); + await pm.tracesPage.closeMoreMenu(); await pm.tracesPage.expectPinnedSavedViewsInToolbar(); await pm.tracesPage.applySavedViewFromPinnedGroup(viewName); await pm.tracesPage.expectQueryEditorEventuallyContains(viewFilter); }); - test('a saved view applies from the List saved views dialog opened from the More menu', { + test('a saved view applies from the Saved views submenu in the More menu', { tag: ['@bug-15189', '@P1', '@regression', '@tracesRegression'], }, async () => { - await pm.tracesPage.openSavedViewsListFromMoreMenu(); - await pm.tracesPage.applySavedViewFromListDialog(viewName); + await pm.tracesPage.applySavedViewFromMoreMenu(viewName); await pm.tracesPage.expectQueryEditorEventuallyContains(viewFilter); }); test('the saved views group is hidden on Services Catalog', { tag: ['@bug-15189', '@P2', '@regression', '@tracesRegression'], }, async () => { + await pm.tracesPage.toggleSavedViewsPinFromMoreMenu(); + await pm.tracesPage.closeMoreMenu(); await pm.tracesPage.expectPinnedSavedViewsInToolbar(); await pm.tracesPage.navigateToServicesViaTab(); diff --git a/web/src/components/savedViews/SavedViewsListDialog.spec.ts b/web/src/components/savedViews/SavedViewsListDialog.spec.ts deleted file mode 100644 index 6c3d4fe8ab..0000000000 --- a/web/src/components/savedViews/SavedViewsListDialog.spec.ts +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2026 OpenObserve Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import { describe, it, expect, afterEach, vi } from "vitest"; -import { mount, flushPromises, VueWrapper } from "@vue/test-utils"; -import SavedViewsListDialog from "@/components/savedViews/SavedViewsListDialog.vue"; - -vi.mock("vue-i18n", () => ({ - useI18n: () => ({ t: (k: string) => k, te: () => true }), -})); - -const views = [ - { view_id: "t1", view_name: "checkout errors" }, - { view_id: "t2", view_name: "slow payments" }, -]; - -// The real ODialog portals out of the wrapper; render its body inline instead. -const ODialogStub = { - name: "ODialog", - props: ["open", "size", "title"], - emits: ["update:open"], - template: '
', -}; - -// The real dropdown portals its items; render them inline so the < md menu is reachable. -const ODropdownStub = { - name: "ODropdown", - template: '
', -}; - -const ODropdownItemStub = { - name: "ODropdownItem", - props: ["iconLeft", "variant"], - emits: ["select"], - template: '
', -}; - -describe("SavedViewsListDialog", () => { - let wrapper: VueWrapper | undefined; - - const mountDialog = (props: Record = {}) => - mount(SavedViewsListDialog, { - props: { - open: true, - views, - favoriteIds: [], - favoriteViews: [], - dataTestPrefix: "traces-saved-views-dialog", - ...props, - }, - global: { - stubs: { - ODialog: ODialogStub, - ODropdown: ODropdownStub, - ODropdownItem: ODropdownItemStub, - }, - }, - }); - - const find = (dataTest: string) => wrapper!.find(`[data-test="${dataTest}"]`); - - afterEach(() => { - wrapper?.unmount(); - wrapper = undefined; - }); - - it("lists only the views passed in", async () => { - wrapper = mountDialog({ views: [views[0]] }); - await flushPromises(); - - expect(find("traces-saved-views-dialog-apply-t1").text()).toBe("checkout errors"); - expect(find("traces-saved-views-dialog-apply-t2").exists()).toBe(false); - }); - - it("filters the list by name, case-insensitively", async () => { - wrapper = mountDialog(); - await flushPromises(); - - await wrapper.findComponent({ name: "OSearchInput" }).vm.$emit("update:modelValue", "SLOW"); - await flushPromises(); - - expect(find("traces-saved-views-dialog-apply-t1").exists()).toBe(false); - expect(find("traces-saved-views-dialog-apply-t2").exists()).toBe(true); - expect(wrapper.emitted("update:search")?.[0]).toEqual(["SLOW"]); - }); - - it("clears the search each time the dialog opens", async () => { - wrapper = mountDialog(); - await flushPromises(); - await wrapper.findComponent({ name: "OSearchInput" }).vm.$emit("update:modelValue", "SLOW"); - await flushPromises(); - expect(find("traces-saved-views-dialog-apply-t1").exists()).toBe(false); - - await wrapper.setProps({ open: false }); - await wrapper.setProps({ open: true }); - await flushPromises(); - - expect(wrapper.findComponent({ name: "OSearchInput" }).props("modelValue")).toBe(""); - expect(find("traces-saved-views-dialog-apply-t1").exists()).toBe(true); - expect(wrapper.emitted("update:search")?.at(-1)).toEqual([""]); - }); - - it("clears a bound search model when the dialog opens", async () => { - wrapper = mountDialog({ open: false, search: "SLOW" }); - await wrapper.setProps({ open: true }); - await flushPromises(); - - expect(wrapper.emitted("update:search")).toEqual([[""]]); - }); - - it("emits apply and closes when a view is chosen", async () => { - wrapper = mountDialog(); - await flushPromises(); - - await find("traces-saved-views-dialog-apply-t2").trigger("click"); - - expect(wrapper.emitted("apply")?.[0]).toEqual([views[1]]); - expect(wrapper.emitted("update:open")?.[0]).toEqual([false]); - }); - - it("emits update and delete without closing itself", async () => { - wrapper = mountDialog(); - await flushPromises(); - - await find("traces-saved-views-dialog-update-t1").trigger("click"); - await find("traces-saved-views-dialog-delete-t2").trigger("click"); - - expect(wrapper.emitted("update")?.[0]).toEqual([views[0]]); - expect(wrapper.emitted("delete")?.[0]).toEqual([views[1]]); - expect(wrapper.emitted("apply")).toBeUndefined(); - expect(wrapper.emitted("update:open")).toBeUndefined(); - }); - - it("emits edit and delete from the < md row menu", async () => { - wrapper = mountDialog(); - await flushPromises(); - - expect(find("traces-saved-views-dialog-more-actions-t1").classes()).toContain("md:hidden"); - await find("traces-saved-views-dialog-update-t1-menu").trigger("click"); - await find("traces-saved-views-dialog-delete-t1-menu").trigger("click"); - - expect(wrapper.emitted("update")?.[0]).toEqual([views[0]]); - expect(wrapper.emitted("delete")?.[0]).toEqual([views[0]]); - }); - - it("hides edit and delete buttons below md and reveals row actions on hover", async () => { - wrapper = mountDialog(); - await flushPromises(); - - const edit = find("traces-saved-views-dialog-update-t1"); - expect(edit.classes()).toEqual( - expect.arrayContaining(["max-md:hidden", "opacity-0", "group-hover/row:opacity-100"]), - ); - expect(find("traces-saved-views-dialog-favorite-t1").classes()).not.toContain("max-md:hidden"); - }); - - it("shows the empty state only when not loading", async () => { - wrapper = mountDialog({ views: [] }); - await flushPromises(); - expect(find("traces-saved-views-dialog-empty").text()).toContain("search.savedViewsNotFound"); - expect(find("traces-saved-views-dialog-loading").exists()).toBe(false); - - await wrapper.setProps({ loading: true }); - await flushPromises(); - expect(find("traces-saved-views-dialog-empty").exists()).toBe(false); - expect(find("traces-saved-views-dialog-loading").text()).toContain("confirmDialog.loading"); - }); - - it("renders the favourites pane only when there are favourites", async () => { - wrapper = mountDialog(); - await flushPromises(); - expect(find("traces-saved-views-dialog-favorites-table").exists()).toBe(false); - expect(find("traces-saved-views-dialog-table").element.parentElement!.className).toContain( - "w-full", - ); - - await wrapper.setProps({ favoriteIds: ["t2"], favoriteViews: [views[1]] }); - await flushPromises(); - - const favorites = find("traces-saved-views-dialog-favorites-table"); - expect(favorites.exists()).toBe(true); - expect(favorites.text()).toContain("search.favoriteViews"); - const header = favorites.element.firstElementChild as HTMLElement; - expect(header.textContent!.trim()).toBe("search.favoriteViews"); - const below = header.nextElementSibling as HTMLElement; - for (const el of [header, below]) { - expect(el.tagName).not.toBe("HR"); - expect(el.className).not.toMatch(/(^|\s)border(-[tb])?(\s|$)/); - } - expect(favorites.find("hr").exists()).toBe(false); - expect( - favorites.find('[data-test="traces-saved-views-dialog-favorites-apply-t2"]').text(), - ).toBe("slow payments"); - expect(find("traces-saved-views-dialog-table").element.parentElement!.className).toContain( - "w-3/5", - ); - }); - - it("toggles favourites with the current state, and the favourites pane always removes", async () => { - wrapper = mountDialog({ favoriteIds: ["t2"], favoriteViews: [views[1]] }); - await flushPromises(); - - const starIcon = (dataTest: string) => - find(dataTest).findComponent({ name: "OIcon" }).props("name"); - expect(starIcon("traces-saved-views-dialog-favorite-t1")).toBe("star-outline"); - expect(starIcon("traces-saved-views-dialog-favorite-t2")).toBe("star"); - - await find("traces-saved-views-dialog-favorite-t1").trigger("click"); - await find("traces-saved-views-dialog-favorite-t2").trigger("click"); - await find("traces-saved-views-dialog-favorites-favorite-t2").trigger("click"); - - expect(wrapper.emitted("toggle-favorite")).toEqual([ - [views[0], false], - [views[1], true], - [views[1], true], - ]); - }); - - it("applies from the favourites pane and closes", async () => { - wrapper = mountDialog({ favoriteIds: ["t2"], favoriteViews: [views[1]] }); - await flushPromises(); - - await find("traces-saved-views-dialog-favorites-apply-t2").trigger("click"); - - expect(wrapper.emitted("apply")?.[0]).toEqual([views[1]]); - expect(wrapper.emitted("update:open")?.[0]).toEqual([false]); - }); - - it("renders compact rows with no header and no pager border", async () => { - wrapper = mountDialog(); - await flushPromises(); - - const table = wrapper.findComponent({ name: "OTable" }); - expect(table.props("compact")).toBe(true); - expect(table.props("showHeader")).toBe(false); - expect(table.props("paginationBordered")).toBe(false); - expect(table.find("thead").exists()).toBe(false); - }); - - it("renders the actions as an unpinned, right-aligned action cell", async () => { - wrapper = mountDialog(); - await flushPromises(); - - const cell = wrapper.find('[data-test="o2-table-cell-actions"]'); - expect(cell.attributes("style") ?? "").not.toContain("sticky"); - expect(cell.classes()).toContain("text-right"); - expect(cell.element.firstElementChild!.className).toContain("inline-flex"); - expect(cell.find(".truncate").exists()).toBe(false); - }); - - it("derives every data-test from dataTestPrefix", async () => { - const p = "logs-saved-views-dialog"; - wrapper = mountDialog({ dataTestPrefix: p, favoriteIds: ["t2"], favoriteViews: [views[1]] }); - await flushPromises(); - - const expected = [ - p, - `${p}-list`, - `${p}-search`, - `${p}-search-field`, - `${p}-table`, - `${p}-favorites-table`, - `${p}-apply-t1`, - `${p}-favorite-t1`, - `${p}-update-t1`, - `${p}-delete-t1`, - `${p}-more-actions-t1`, - `${p}-update-t1-menu`, - `${p}-delete-t1-menu`, - `${p}-favorites-apply-t2`, - `${p}-favorites-favorite-t2`, - `${p}-favorites-update-t2`, - `${p}-favorites-delete-t2`, - `${p}-favorites-more-actions-t2`, - `${p}-favorites-update-t2-menu`, - `${p}-favorites-delete-t2-menu`, - ]; - for (const id of expected) { - expect(wrapper.find(`[data-test="${id}"]`).exists(), id).toBe(true); - } - expect(find(`${p}-apply-t1`).attributes("data-test-view-name")).toBe("checkout errors"); - expect(find(`${p}-favorites-apply-t2`).attributes("data-test-view-name")).toBe("slow payments"); - expect(wrapper.find('[data-test^="traces-"]').exists()).toBe(false); - }); - - it("re-renders data-test values when dataTestPrefix changes", async () => { - wrapper = mountDialog(); - await wrapper.setProps({ dataTestPrefix: "other-dialog" }); - - expect(find("other-dialog").exists()).toBe(true); - expect(find("other-dialog-table").exists()).toBe(true); - expect(find("other-dialog-apply-t1").exists()).toBe(true); - expect(find("traces-saved-views-dialog-table").exists()).toBe(false); - }); -}); diff --git a/web/src/components/savedViews/SavedViewsListDialog.vue b/web/src/components/savedViews/SavedViewsListDialog.vue deleted file mode 100644 index d1f35fe33e..0000000000 --- a/web/src/components/savedViews/SavedViewsListDialog.vue +++ /dev/null @@ -1,295 +0,0 @@ - - - - - diff --git a/web/src/components/savedViews/SavedViewsMenu.spec.ts b/web/src/components/savedViews/SavedViewsMenu.spec.ts new file mode 100644 index 0000000000..8d31312846 --- /dev/null +++ b/web/src/components/savedViews/SavedViewsMenu.spec.ts @@ -0,0 +1,451 @@ +// Copyright 2026 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { h, ref } from "vue"; +import { flushPromises, mount } from "@vue/test-utils"; +import i18n from "@/locales"; +import { raw } from "@/types/i18n"; +import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue"; +import SavedViewsMenu from "./SavedViewsMenu.vue"; +import SavedViewsSubmenu from "./SavedViewsSubmenu.vue"; + +const views = [ + { view_id: "a", view_name: "Alpha" }, + { view_id: "b", view_name: "Beta" }, + { view_id: "c", view_name: "Gamma" }, +]; + +let wrapper: ReturnType | undefined; +afterEach(() => { + document.documentElement.dir = "ltr"; + wrapper?.unmount(); + wrapper = undefined; +}); + +const el = (id: string) => document.querySelector(`[data-test="${id}"]`); + +const mountMenu = async ( + props: Record = {}, + listeners: Record = {}, +) => { + wrapper = mount(ODropdown, { + attachTo: document.body, + props: { open: true }, + global: { plugins: [i18n] }, + slots: { + trigger: () => h("button", "Menu"), + default: () => + h(SavedViewsMenu, { + views, + favoriteIds: ["b"], + dataTestPrefix: "sv", + ...props, + ...listeners, + }), + }, + }); + await flushPromises(); +}; + +describe("SavedViewsMenu", () => { + it("groups favorites first and the rest under All Saved Views", async () => { + await mountMenu(); + expect(el("sv-favorites-group")?.textContent).toContain("Favorites (1)"); + expect(el("sv-favorites-group")?.querySelector('[data-test="sv-apply-b"]')).not.toBeNull(); + expect(el("sv-all-group")?.textContent).toContain("All Saved Views"); + expect(el("sv-all-group")?.textContent).not.toContain("(2)"); + expect(el("sv-all-group")?.querySelector('[data-test="sv-apply-a"]')).not.toBeNull(); + expect(el("sv-all-group")?.querySelector('[data-test="sv-apply-b"]')).toBeNull(); + expect(el("sv-search")).not.toBeNull(); + }); + + it("hides the favorites group when there are none", async () => { + await mountMenu({ favoriteIds: [] }); + expect(el("sv-favorites-group")).toBeNull(); + expect(el("sv-all-group")).not.toBeNull(); + }); + + it("filters by name and shows the no-match state", async () => { + await mountMenu(); + const input = el("sv-search")!.querySelector("input")!; + input.value = "gam"; + input.dispatchEvent(new Event("input", { bubbles: true })); + await flushPromises(); + expect(el("sv-apply-c")).not.toBeNull(); + expect(el("sv-apply-a")).toBeNull(); + input.value = "zzz"; + input.dispatchEvent(new Event("input", { bubbles: true })); + await flushPromises(); + expect(el("sv-empty")?.textContent).toContain(i18n.global.t("search.savedViewsNoMatch")); + }); + + it("shows the empty state when there are no views and the loading state while loading", async () => { + await mountMenu({ views: [] }); + expect(el("sv-empty")?.textContent).toContain(i18n.global.t("search.savedViewsEmpty")); + wrapper!.unmount(); + await mountMenu({ loading: true }); + expect(el("sv-loading")).not.toBeNull(); + }); + + it("uses a generic search placeholder while loading and a counted one after", async () => { + await mountMenu({ views: [], loading: true }); + expect(el("sv-search")?.querySelector("input")?.getAttribute("placeholder")).toBe( + "Search saved views", + ); + wrapper!.unmount(); + await mountMenu(); + expect(el("sv-search")?.querySelector("input")?.getAttribute("placeholder")).toBe( + "Search 3 saved views", + ); + }); + + it("renders no button inside another button", async () => { + await mountMenu(); + expect(document.querySelectorAll("button button")).toHaveLength(0); + }); + + it("marks the applied view and keeps the star label constant with aria-pressed", async () => { + await mountMenu({ appliedViewId: "a" }); + expect(el("sv-applied-a")?.getAttribute("aria-label")).toBe( + i18n.global.t("search.savedViewApplied"), + ); + expect(el("sv-applied-b")).toBeNull(); + expect(el("sv-favorite-b")?.getAttribute("aria-pressed")).toBe("true"); + expect(el("sv-favorite-b")?.getAttribute("aria-label")).toBe("Favorite Beta"); + expect(el("sv-favorite-a")?.getAttribute("aria-label")).toBe("Favorite Alpha"); + expect(el("sv-favorite-a")?.getAttribute("aria-pressed")).toBe("false"); + }); + + it("emits apply, favorite, update, delete and create", async () => { + const onApply = vi.fn(); + const onFav = vi.fn(); + const onUpdate = vi.fn(); + const onCreate = vi.fn(); + const onDelete = vi.fn(); + await mountMenu( + {}, + { + onApply, + onToggleFavorite: onFav, + onUpdate, + onCreate, + onDelete, + }, + ); + el("sv-favorite-a")!.click(); + expect(onFav).toHaveBeenCalledWith(views[0], false); + el("sv-favorite-b")!.click(); + expect(onFav).toHaveBeenCalledWith(views[1], true); + expect(el("sv-update-c")?.getAttribute("aria-label")).toBe( + "Overwrite Gamma with current query", + ); + el("sv-update-c")!.click(); + expect(onUpdate).toHaveBeenCalledWith(views[2]); + el("sv-delete-c")!.click(); + expect(onDelete).toHaveBeenCalledWith(views[2]); + expect(el("sv-delete-c")?.getAttribute("aria-label")).toBe("Delete Gamma"); + expect(onApply).not.toHaveBeenCalled(); + el("sv-apply-a")!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + await flushPromises(); + expect(onApply).toHaveBeenCalledWith(views[0]); + }); + + it("applies the first match on Enter in the search box", async () => { + const onApply = vi.fn(); + await mountMenu({}, { onApply }); + const input = el("sv-search")!.querySelector("input")!; + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + await flushPromises(); + expect(onApply).toHaveBeenCalledWith(views[1]); + expect(wrapper!.emitted("update:open")?.at(-1)).toEqual([false]); + }); + + it("keeps focus on the same star after a favorite toggle moves the row", async () => { + const favoriteIds = ref(["b"]); + wrapper = mount(ODropdown, { + attachTo: document.body, + props: { open: true }, + global: { plugins: [i18n] }, + slots: { + trigger: () => h("button", "Menu"), + default: () => + h(SavedViewsMenu, { + views, + favoriteIds: favoriteIds.value, + dataTestPrefix: "sv", + onToggleFavorite: (view: { view_id: string }) => { + favoriteIds.value = [...favoriteIds.value, view.view_id]; + }, + }), + }, + }); + await flushPromises(); + el("sv-favorite-a")!.dispatchEvent( + new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }), + ); + await flushPromises(); + expect(el("sv-favorites-group")?.querySelector('[data-test="sv-apply-a"]')).not.toBeNull(); + expect(document.activeElement).toBe(el("sv-favorite-a")); + }); + + it("highlights the applied row with the selected background", async () => { + await mountMenu({ appliedViewId: "a" }); + expect(el("sv-apply-a")?.className).toContain("bg-select-item-selected-bg"); + expect(el("sv-apply-c")?.className).not.toContain("bg-select-item-selected-bg"); + }); + + it("disables create and edit with the reason when saving is blocked", async () => { + await mountMenu({ saveReason: raw("Run the query first") }); + expect(el("sv-create")?.getAttribute("aria-disabled")).toBe("true"); + expect(el("sv-update-a")?.getAttribute("aria-disabled")).toBe("true"); + expect(el("sv-create")?.textContent).toContain("Run the query first"); + }); + + it("uses the supplied create test id", async () => { + await mountMenu({ createTestId: "my-create" }); + expect(el("my-create")).not.toBeNull(); + expect(el("sv-manage")).toBeNull(); + }); +}); + +describe("SavedViewsSubmenu", () => { + it("summarises views and favorites on the trigger", async () => { + wrapper = mount(ODropdown, { + attachTo: document.body, + props: { open: true }, + global: { plugins: [i18n] }, + slots: { + trigger: () => h("button", "Menu"), + default: () => + h(SavedViewsSubmenu, { + views, + favoriteIds: ["a", "b"], + dataTestPrefix: "sv", + triggerTestId: "sv-trigger", + }), + }, + }); + await flushPromises(); + const text = el("sv-trigger")?.textContent ?? ""; + expect(text).toContain("Saved Views"); + expect(text).toContain("3 views, 2 favorites"); + }); + + it("omits the favorites part when there are none and handles no views", async () => { + wrapper = mount(ODropdown, { + attachTo: document.body, + props: { open: true }, + global: { plugins: [i18n] }, + slots: { + trigger: () => h("button", "Menu"), + default: () => [ + h(SavedViewsSubmenu, { + views: [views[0]], + favoriteIds: [], + dataTestPrefix: "one", + triggerTestId: "one-trigger", + }), + h(SavedViewsSubmenu, { + views: [], + favoriteIds: [], + dataTestPrefix: "none", + triggerTestId: "none-trigger", + }), + ], + }, + }); + await flushPromises(); + expect(el("one-trigger")?.textContent).toContain("1 view"); + expect(el("one-trigger")?.textContent).not.toContain("favorite"); + expect(el("none-trigger")?.textContent).toContain("No saved views yet"); + }); + + it("shows no empty text on the trigger while loading", async () => { + wrapper = mount(ODropdown, { + attachTo: document.body, + props: { open: true }, + global: { plugins: [i18n] }, + slots: { + trigger: () => h("button", "Menu"), + default: () => + h(SavedViewsSubmenu, { + views: [], + favoriteIds: [], + loading: true, + dataTestPrefix: "ld", + triggerTestId: "ld-trigger", + }), + }, + }); + await flushPromises(); + expect(el("ld-trigger")?.textContent).not.toContain("No saved views yet"); + }); + + const mountSubmenu = async ( + props: Record = {}, + slots: Record unknown> = {}, + ) => { + wrapper = mount(ODropdown, { + attachTo: document.body, + props: { open: true }, + global: { plugins: [i18n] }, + slots: { + trigger: () => h("button", "Menu"), + default: () => + h( + SavedViewsSubmenu, + { + views, + favoriteIds: ["b"], + dataTestPrefix: "sv", + triggerTestId: "sv-trigger", + ...props, + }, + slots, + ), + }, + }); + await flushPromises(); + }; + + it("opens the submenu and passes attrs through to the menu", async () => { + const onApply = vi.fn(); + const onUpdate = vi.fn(); + const onDelete = vi.fn(); + const onFav = vi.fn(); + await mountSubmenu({ + appliedViewId: "a", + createTestId: "sv-create-custom", + onApply, + onUpdate, + onDelete, + onToggleFavorite: onFav, + }); + expect(el("sv-content")).toBeNull(); + el("sv-trigger")!.click(); + await flushPromises(); + expect(el("sv-content")).not.toBeNull(); + expect(el("sv-create-custom")).not.toBeNull(); + expect(el("sv-applied-a")).not.toBeNull(); + expect(el("sv-applied-b")).toBeNull(); + el("sv-update-c")!.click(); + expect(onUpdate).toHaveBeenCalledWith(views[2]); + el("sv-delete-c")!.click(); + expect(onDelete).toHaveBeenCalledWith(views[2]); + el("sv-favorite-a")!.click(); + expect(onFav).toHaveBeenCalledWith(views[0], false); + el("sv-apply-a")!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + await flushPromises(); + expect(onApply).toHaveBeenCalledWith(views[0]); + }); + + it("emits open when the submenu opens", async () => { + const onOpen = vi.fn(); + await mountSubmenu({ onOpen }); + expect(onOpen).not.toHaveBeenCalled(); + el("sv-trigger")!.click(); + await flushPromises(); + expect(onOpen).toHaveBeenCalledTimes(1); + }); + + it("keeps the actions slot outside the trigger button", async () => { + await mountSubmenu({}, { actions: () => h("button", { "data-test": "sv-pin" }, "pin") }); + el("sv-trigger")!.click(); + await flushPromises(); + expect(el("sv-pin")).not.toBeNull(); + expect(el("sv-trigger")?.contains(el("sv-pin"))).toBe(false); + expect(document.querySelectorAll("button button")).toHaveLength(0); + }); + + it("moves focus between the trigger and the pin with the arrow keys", async () => { + await mountSubmenu({}, { actions: () => h("button", { "data-test": "sv-pin" }, "pin") }); + el("sv-trigger")!.focus(); + el("sv-trigger")!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true, cancelable: true }), + ); + expect(document.activeElement).toBe(el("sv-pin")); + el("sv-pin")!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true }), + ); + expect(document.activeElement).toBe(el("sv-trigger")); + }); + + it("lets Escape from the pin reach the menu while other keys stay contained", async () => { + await mountSubmenu({}, { actions: () => h("button", { "data-test": "sv-pin" }, "pin") }); + const seen: string[] = []; + const listener = (e: KeyboardEvent) => seen.push(e.key); + document.addEventListener("keydown", listener); + for (const key of ["Escape", "a"]) { + el("sv-pin")!.dispatchEvent( + new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }), + ); + } + document.removeEventListener("keydown", listener); + expect(seen).toEqual(["Escape"]); + }); + + it("swaps the arrow keys between the trigger and the pin under rtl", async () => { + document.documentElement.dir = "rtl"; + await mountSubmenu({}, { actions: () => h("button", { "data-test": "sv-pin" }, "pin") }); + el("sv-trigger")!.focus(); + el("sv-trigger")!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true, cancelable: true }), + ); + expect(document.activeElement).toBe(el("sv-trigger")); + el("sv-trigger")!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true }), + ); + expect(document.activeElement).toBe(el("sv-pin")); + el("sv-pin")!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true, cancelable: true }), + ); + expect(document.activeElement).toBe(el("sv-trigger")); + }); +}); + +describe("SavedViewsMenuRow arrow keys", () => { + const press = (key: string) => + document.activeElement!.dispatchEvent( + new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }), + ); + + it("walks star, edit and delete with ArrowRight and back with ArrowLeft", async () => { + await mountMenu(); + el("sv-favorite-a")!.focus(); + press("ArrowRight"); + expect(document.activeElement).toBe(el("sv-update-a")); + press("ArrowRight"); + expect(document.activeElement).toBe(el("sv-delete-a")); + press("ArrowLeft"); + expect(document.activeElement).toBe(el("sv-update-a")); + }); + + it("walks the actions with the swapped keys under rtl", async () => { + document.documentElement.dir = "rtl"; + await mountMenu(); + el("sv-favorite-a")!.focus(); + press("ArrowLeft"); + expect(document.activeElement).toBe(el("sv-update-a")); + press("ArrowLeft"); + expect(document.activeElement).toBe(el("sv-delete-a")); + press("ArrowRight"); + expect(document.activeElement).toBe(el("sv-update-a")); + press("ArrowRight"); + expect(document.activeElement).toBe(el("sv-favorite-a")); + press("ArrowRight"); + expect(document.activeElement).toBe(el("sv-apply-a")); + }); +}); diff --git a/web/src/components/savedViews/SavedViewsMenu.vue b/web/src/components/savedViews/SavedViewsMenu.vue new file mode 100644 index 0000000000..99f71b3d86 --- /dev/null +++ b/web/src/components/savedViews/SavedViewsMenu.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/web/src/components/savedViews/SavedViewsMenuRow.vue b/web/src/components/savedViews/SavedViewsMenuRow.vue new file mode 100644 index 0000000000..79a25c3624 --- /dev/null +++ b/web/src/components/savedViews/SavedViewsMenuRow.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/web/src/components/savedViews/SavedViewsSubmenu.vue b/web/src/components/savedViews/SavedViewsSubmenu.vue new file mode 100644 index 0000000000..60fb4895a5 --- /dev/null +++ b/web/src/components/savedViews/SavedViewsSubmenu.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/web/src/components/savedViews/savedViews.types.ts b/web/src/components/savedViews/savedViews.types.ts new file mode 100644 index 0000000000..c952cdd4e8 --- /dev/null +++ b/web/src/components/savedViews/savedViews.types.ts @@ -0,0 +1,7 @@ +// Copyright 2026 OpenObserve Inc. + +export interface SavedViewRow { + view_id: string; + view_name: string; + [key: string]: unknown; +} diff --git a/web/src/components/savedViews/savedViewsKeys.ts b/web/src/components/savedViews/savedViewsKeys.ts new file mode 100644 index 0000000000..9abf6286c3 --- /dev/null +++ b/web/src/components/savedViews/savedViewsKeys.ts @@ -0,0 +1,12 @@ +// Copyright 2026 OpenObserve Inc. + +export type HorizontalStep = "forward" | "back"; + +// Same direction source as the nav keyboard handling (Header, ONavGroup). +const isRtl = () => document.documentElement.dir === "rtl"; + +/** Maps a physical arrow key to reading-order movement, which flips under rtl. */ +export const horizontalStep = (key: string): HorizontalStep | null => { + if (key !== "ArrowLeft" && key !== "ArrowRight") return null; + return (key === "ArrowRight") !== isRtl() ? "forward" : "back"; +}; diff --git a/web/src/components/savedViews/savedViewsSort.ts b/web/src/components/savedViews/savedViewsSort.ts index 29e16bf34c..8af75c0729 100644 --- a/web/src/components/savedViews/savedViewsSort.ts +++ b/web/src/components/savedViews/savedViewsSort.ts @@ -18,11 +18,7 @@ export interface SavedViewLike { view_name: string; } -/** - * Ordering for the saved-views quick dropdown: favorited views first, then - * case-insensitive alphabetical. The dropdown has no search box, so ordering - * is what makes a long list scannable. Returns a new array. - */ +/** Favorites first, then case-insensitive alphabetical; returns a new array. */ export function sortSavedViews( views: T[] | null | undefined, favoriteIds: string[], diff --git a/web/src/composables/useToolbarPins.spec.ts b/web/src/composables/useToolbarPins.spec.ts index 66eba6436e..75df679a55 100644 --- a/web/src/composables/useToolbarPins.spec.ts +++ b/web/src/composables/useToolbarPins.spec.ts @@ -27,11 +27,11 @@ describe("useToolbarPins defaults", () => { window.localStorage.clear(); }); - it("pins ONLY histogram and savedViews by default on a pristine profile", async () => { + it("pins ONLY histogram by default on a pristine profile", async () => { const { useToolbarPins } = await importFresh(); const { isPinned } = useToolbarPins(); expect(isPinned("histogram")).toBe(true); - expect(isPinned("savedViews")).toBe(true); + expect(isPinned("savedViews")).toBe(false); expect(isPinned("sqlMode")).toBe(false); }); @@ -44,20 +44,6 @@ describe("useToolbarPins defaults", () => { expect(useToolbarPins().isPinned("savedViews")).toBe(true); }); - it("pins savedViews for a profile that stored pins before it was a default", async () => { - window.localStorage.setItem("logs_toolbar_pinned_items", JSON.stringify(["histogram"])); - const { useToolbarPins } = await importFresh(); - expect(useToolbarPins().isPinned("savedViews")).toBe(true); - }); - - it("stays unpinned once the user unpins savedViews", async () => { - const { useToolbarPins } = await importFresh(); - useToolbarPins().togglePin("savedViews"); - expect(window.localStorage.getItem("logs_toolbar_saved_views_pin_decided")).toBe("true"); - const fresh = await importFresh(); - expect(fresh.useToolbarPins().isPinned("savedViews")).toBe(false); - }); - it("keeps the histogram decided-flag behavior intact", async () => { const { useToolbarPins } = await importFresh(); useToolbarPins().togglePin("histogram"); @@ -71,7 +57,7 @@ describe("useToolbarPins defaults", () => { const { togglePin, pinnedItems } = useToolbarPins(); togglePin("syntaxGuide"); togglePin("sqlMode"); - expect(pinnedItems.value).toEqual(["histogram", "sqlMode", "savedViews", "syntaxGuide"]); + expect(pinnedItems.value).toEqual(["histogram", "sqlMode", "syntaxGuide"]); }); }); @@ -80,30 +66,30 @@ describe("useToolbarPins scopes", () => { window.localStorage.clear(); }); - it("keeps the logs storage key and pins saved views by default", async () => { + it("keeps the logs storage key and defaults unchanged", async () => { const { useToolbarPins } = await importFresh(); const { togglePin, pinnedItems } = useToolbarPins(); - expect(pinnedItems.value).toEqual(["histogram", "savedViews"]); + expect(pinnedItems.value).toEqual(["histogram"]); togglePin("sqlMode"); expect(JSON.parse(window.localStorage.getItem("logs_toolbar_pinned_items")!)).toEqual([ "histogram", - "savedViews", "sqlMode", ]); expect(window.localStorage.getItem("traces_toolbar_pinned_items")).toBeNull(); }); - it("pins traces saved views by default", async () => { + it("pins nothing by default on traces", async () => { const { useToolbarPins } = await importFresh(); const { isPinned, pinnedItems } = useToolbarPins("traces"); - expect(isPinned("savedViews")).toBe(true); + expect(isPinned("savedViews")).toBe(false); expect(isPinned("histogram")).toBe(false); - expect(pinnedItems.value).toEqual(["savedViews"]); + expect(pinnedItems.value).toEqual([]); }); it("pins traces RED Metrics through the histogram key in canonical order", async () => { const { useToolbarPins } = await importFresh(); const { togglePin, pinnedItems } = useToolbarPins("traces"); + togglePin("savedViews"); togglePin("histogram"); expect(pinnedItems.value).toEqual(["histogram", "savedViews"]); expect(JSON.parse(window.localStorage.getItem("traces_toolbar_pinned_items")!)).toEqual([ @@ -129,10 +115,11 @@ describe("useToolbarPins scopes", () => { it("isolates scopes from each other", async () => { const { useToolbarPins } = await importFresh(); useToolbarPins("traces").togglePin("savedViews"); - expect(useToolbarPins("traces").isPinned("savedViews")).toBe(false); - expect(useToolbarPins().isPinned("savedViews")).toBe(true); - useToolbarPins().togglePin("savedViews"); + expect(useToolbarPins("traces").isPinned("savedViews")).toBe(true); expect(useToolbarPins().isPinned("savedViews")).toBe(false); + useToolbarPins().togglePin("savedViews"); + useToolbarPins("traces").togglePin("savedViews"); + expect(useToolbarPins().isPinned("savedViews")).toBe(true); expect(useToolbarPins("traces").isPinned("savedViews")).toBe(false); expect(window.localStorage.getItem("logs_toolbar_histogram_pin_decided")).toBeNull(); }); @@ -142,37 +129,21 @@ describe("useToolbarPins scopes", () => { const a = useToolbarPins("traces"); const b = useToolbarPins("traces"); a.togglePin("savedViews"); - expect(b.isPinned("savedViews")).toBe(false); + expect(b.isPinned("savedViews")).toBe(true); }); - it("keeps traces saved views unpinned across a module reload", async () => { + it("keeps traces saved views pinned across a module reload once pinned", async () => { const { useToolbarPins } = await importFresh(); useToolbarPins("traces").togglePin("savedViews"); - expect(window.localStorage.getItem("traces_toolbar_saved_views_pin_decided")).toBe("true"); const fresh = await importFresh(); - expect(fresh.useToolbarPins("traces").isPinned("savedViews")).toBe(false); + expect(fresh.useToolbarPins("traces").isPinned("savedViews")).toBe(true); }); - it("keeps each default's decided flag when stored pins are corrupt", async () => { + it("keeps the logs histogram decided flag when stored pins are corrupt", async () => { window.localStorage.setItem("logs_toolbar_pinned_items", "{not json"); window.localStorage.setItem("traces_toolbar_pinned_items", "{not json"); - window.localStorage.setItem("traces_toolbar_saved_views_pin_decided", "true"); const { useToolbarPins } = await importFresh(); expect(useToolbarPins("traces").isPinned("savedViews")).toBe(false); expect(useToolbarPins().isPinned("histogram")).toBe(true); }); }); - -describe("useToolbarPins scope keys", () => { - beforeEach(() => { - window.localStorage.clear(); - }); - - it("ignores a key outside the scope's key list", async () => { - const { useToolbarPins } = await importFresh(); - const traces = useToolbarPins("traces"); - traces.togglePin("sqlMode"); - expect(traces.isPinned("sqlMode")).toBe(false); - expect(window.localStorage.getItem("traces_toolbar_pinned_items")).toBeNull(); - }); -}); diff --git a/web/src/composables/useToolbarPins.ts b/web/src/composables/useToolbarPins.ts index f322eafe7c..8ac4ac7a21 100644 --- a/web/src/composables/useToolbarPins.ts +++ b/web/src/composables/useToolbarPins.ts @@ -44,15 +44,12 @@ const SCOPES: Record = { logs: { storageKey: "logs_toolbar_pinned_items", keys: TOOLBAR_PIN_ORDER, - defaultPins: { - histogram: "logs_toolbar_histogram_pin_decided", - savedViews: "logs_toolbar_saved_views_pin_decided", - }, + defaultPins: { histogram: "logs_toolbar_histogram_pin_decided" }, }, traces: { storageKey: "traces_toolbar_pinned_items", keys: ["histogram", "savedViews"], - defaultPins: { savedViews: "traces_toolbar_saved_views_pin_decided" }, + defaultPins: {}, }, }; diff --git a/web/src/locales/languages/ar-SA.json b/web/src/locales/languages/ar-SA.json index 06939659d8..e8e4dc55f3 100644 --- a/web/src/locales/languages/ar-SA.json +++ b/web/src/locales/languages/ar-SA.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "وضع السرعة", "menuMore": "المزيد", "menuGroupSetOnce": "اضبط مرة واحدة", - "menuGroupSavedViews": "المشاهدات المحفوظة", "menuGroupHistory": "التاريخ", "menuGroupDownloads": "التنزيلات", "menuGroupSchedule": "الجدول الزمني", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "يمكن تحديد عدد السجلات مثلا: إذا كان عدد السجلات هو 1000، يمكن للمستخدم الحصول على حد أقصى 1000 سجل", "maxEventsScheduleJob": "يمكن إرجاع 100000 حدث كحد أقصى في المهمة المجدولة", "histogramDisabledScheduleJob": "سيتم تعطيل المدرج التكراري للمهمة المجدولة", - "favoriteViews": "المشاهد المفضلة", "region": "المنطقة", "deleteSavedView": "حذف العرض المحفوظ", "deleteSavedViewConfirm": "هل أنت متأكد من أنك تريد حذف العرض المحفوظ {name}؟", - "updateSavedView": "تحديث العرض المحفوظ", - "updateSavedViewConfirm": "هل أنت متأكد من رغبتك في تحديث العرض المحفوظ؟ هذا الإجراء سيكتب فوق العرض الحالي.", + "overwriteSavedView": "الكتابة فوق العرض المحفوظ", + "overwriteSavedViewConfirm": "هل تريد استبدال الاستعلام المحفوظ للعرض {name} بالاستعلام الحالي؟", + "overwriteSavedViewOk": "الكتابة فوق", "collapse": "طي", "expand": "التوسع", "syntaxGuideLabel": "دليل النحوية", @@ -1250,12 +1249,9 @@ "legendLabel": "وسيلة الإيضاح", "switchToLogs": "العودة إلى السجلات", "savedViewsLabel": "عرض محفوظ", - "savedViewsNotFound": "لم يتم العثور على عرض محفوظ!", "savedViewName": "عرض الاسم", "createSavedView": "إنشاء عرض محفوظ", "listSavedViews": "قائمة المشاهدات المحفوظة", - "manageSavedViews": "إدارة العروض المحفوظة", - "updateSavedViewWithCurrent": "حدث هذا العرض مع البحث الحالي", "pinToToolbar": "تثبيت في شريط الأدوات", "unpinFromToolbar": "فك التثبيت من شريط الأدوات", "shareLink": "مشاركة رابط", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (البيانات التي أعيد إلى: {start} إلى {end})", "histogramErrorBtnLabel": "انقر لتفاصيل الخطأ", "applySearch": "قم بضبط معلمات التصفية واضغط على 'تشغيل الاستعلام'.", - "searchSavedView": "البحث عن العروض المحفوظة", + "savedViewsRowLabel": "العروض المحفوظة", + "savedViewsSummaryViews": "{count} عرض | {count} عروض", + "savedViewsSummaryFavorites": "{count} مفضل | {count} مفضلة", + "savedViewsSummary": "{views}، {favorites}", + "savedViewsSearchCount": "البحث في {count} عرض محفوظ | البحث في {count} عروض محفوظة", + "savedViewsSearchPlaceholder": "البحث عن العروض المحفوظة", + "savedViewsFavoritesGroup": "المفضلة ({count})", + "savedViewsAllGroup": "كل العروض المحفوظة", + "savedViewsEmpty": "لا توجد عروض محفوظة بعد", + "savedViewsNoMatch": "لا توجد عروض محفوظة تطابق بحثك", + "savedViewApplied": "مطبّق حاليًا", + "favoriteViewNamed": "إضافة {name} إلى المفضلة", + "deleteViewNamed": "حذف {name}", + "overwriteSavedViewNamed": "استبدال {name} بالاستعلام الحالي", "searchSavedFunction": "البحث", "streamNotExist": "لم يتم العثور على التدفقات التالية [STREAM_NAME]. يرجى التحقق مما إذا كانت هذه التدفقات موجودة.", "noFieldFound": "لم يتم العثور على حقول للتدفق المختار", diff --git a/web/src/locales/languages/de-DE.json b/web/src/locales/languages/de-DE.json index 795c632a31..cce065407f 100644 --- a/web/src/locales/languages/de-DE.json +++ b/web/src/locales/languages/de-DE.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Schneller Modus", "menuMore": "Mehr", "menuGroupSetOnce": "Einmal einstellen", - "menuGroupSavedViews": "Gespeicherte Ansichten", "menuGroupHistory": "Geschichte", "menuGroupDownloads": "herunterladbare", "menuGroupSchedule": "Zeitplan", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Die Anzahl der Datensätze kann angegeben werden, z. B.: Wenn die Anzahl der Datensätze 1000 ist, kann der Benutzer maximal 1000 Datensätze abrufen", "maxEventsScheduleJob": "Maximal 100.000 Ereignisse können in einem Zeitplanauftrag zurückgegeben werden", "histogramDisabledScheduleJob": "Das Histogramm wird für den Zeitplan-Job deaktiviert", - "favoriteViews": "Bevorzugte Ansichten", "region": "Region", "deleteSavedView": "Gespeicherte Ansicht löschen", "deleteSavedViewConfirm": "Möchten Sie die gespeicherte Ansicht {name} wirklich löschen?", - "updateSavedView": "Gespeicherte Ansicht aktualisieren", - "updateSavedViewConfirm": "Möchten Sie die gespeicherte Ansicht wirklich aktualisieren? Diese Aktion überschreibt die bestehende.", + "overwriteSavedView": "Gespeicherte Ansicht überschreiben", + "overwriteSavedViewConfirm": "Die gespeicherte Abfrage von {name} durch die aktuelle Abfrage ersetzen?", + "overwriteSavedViewOk": "Überschreiben", "collapse": "Einklappen", "expand": "Erweitern", "syntaxGuideLabel": "Leitfaden zur Syntax", @@ -1250,12 +1249,9 @@ "legendLabel": "Legende", "switchToLogs": "Zurück zu Logs", "savedViewsLabel": "Gespeicherte Ansichten", - "savedViewsNotFound": "Gespeicherte Ansicht wurde nicht gefunden!", "savedViewName": "Namen anzeigen", "createSavedView": "Gespeicherte Ansicht erstellen", "listSavedViews": "Gespeicherte Ansichten auflisten", - "manageSavedViews": "Gespeicherte Ansichten verwalten", - "updateSavedViewWithCurrent": "Diese Ansicht mit der aktuellen Suche aktualisieren", "pinToToolbar": "An Werkzeugleiste anheften", "unpinFromToolbar": "Von der Werkzeugleiste lösen", "shareLink": "Link teilen", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Daten zurückgegeben für: {start} bis {end})", "histogramErrorBtnLabel": "Klicken Sie hier für Fehlerdetails", "applySearch": "Passen Sie die Filterparameter an und klicken Sie auf „Abfrage ausführen“.", - "searchSavedView": "Gespeicherte Ansichten durchsuchen", + "savedViewsRowLabel": "Gespeicherte Ansichten", + "savedViewsSummaryViews": "{count} Ansicht | {count} Ansichten", + "savedViewsSummaryFavorites": "{count} Favorit | {count} Favoriten", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "{count} gespeicherte Ansicht durchsuchen | {count} gespeicherte Ansichten durchsuchen", + "savedViewsSearchPlaceholder": "Gespeicherte Ansichten durchsuchen", + "savedViewsFavoritesGroup": "Favoriten ({count})", + "savedViewsAllGroup": "Alle gespeicherten Ansichten", + "savedViewsEmpty": "Noch keine gespeicherten Ansichten", + "savedViewsNoMatch": "Keine gespeicherten Ansichten entsprechen Ihrer Suche", + "savedViewApplied": "Aktuell angewendet", + "favoriteViewNamed": "{name} als Favorit markieren", + "deleteViewNamed": "{name} löschen", + "overwriteSavedViewNamed": "{name} mit aktueller Abfrage überschreiben", "searchSavedFunction": "Suchen", "streamNotExist": "Die folgenden [STREAM_NAME]-Streams wurden nicht gefunden. Bitte überprüfen Sie, ob die Streams existieren.", "noFieldFound": "Für den ausgewählten Stream wurden keine Felder gefunden", diff --git a/web/src/locales/languages/en-US.json b/web/src/locales/languages/en-US.json index cc109341a9..1dace867c8 100644 --- a/web/src/locales/languages/en-US.json +++ b/web/src/locales/languages/en-US.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Quick Mode", "menuMore": "More", "menuGroupSetOnce": "Set Once", - "menuGroupSavedViews": "Saved Views", "menuGroupHistory": "History", "menuGroupDownloads": "Downloads", "menuGroupSchedule": "Schedule", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Number of records can be specified eg: if the no. of records is 1000 then user can get maximum of 1000 records", "maxEventsScheduleJob": "Maximum 100000 events can be returned in schedule job", "histogramDisabledScheduleJob": "Histogram will be disabled for the schedule job", - "favoriteViews": "Favorite Views", "region": "Region", "deleteSavedView": "Delete Saved View", "deleteSavedViewConfirm": "Are you sure you want to delete the {name} saved view?", - "updateSavedView": "Update Saved View", - "updateSavedViewConfirm": "Are you sure you want to update the saved view? This action will overwrite the existing one.", + "overwriteSavedView": "Overwrite Saved View", + "overwriteSavedViewConfirm": "Replace the saved query of {name} with the current query?", + "overwriteSavedViewOk": "Overwrite", "collapse": "Collapse", "expand": "Expand", "syntaxGuideLabel": "Syntax Guide", @@ -1250,12 +1249,9 @@ "legendLabel": "Legend", "switchToLogs": "Switch back to Logs", "savedViewsLabel": "Saved View", - "savedViewsNotFound": "Saved view not found!", "savedViewName": "View Name", "createSavedView": "Create Saved View", "listSavedViews": "List Saved Views", - "manageSavedViews": "Manage Saved Views", - "updateSavedViewWithCurrent": "Update this view with the current search", "pinToToolbar": "Pin to toolbar", "unpinFromToolbar": "Unpin from toolbar", "shareLink": "Share Link", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Data returned for: {start} to {end})", "histogramErrorBtnLabel": "Click for error details", "applySearch": "Adjust filter parameters and click 'Run query'.", - "searchSavedView": "Search saved views", + "savedViewsRowLabel": "Saved Views", + "savedViewsSummaryViews": "{count} view | {count} views", + "savedViewsSummaryFavorites": "{count} favorite | {count} favorites", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Search {count} saved view | Search {count} saved views", + "savedViewsSearchPlaceholder": "Search saved views", + "savedViewsFavoritesGroup": "Favorites ({count})", + "savedViewsAllGroup": "All Saved Views", + "savedViewsEmpty": "No saved views yet", + "savedViewsNoMatch": "No saved views match your search", + "savedViewApplied": "Currently applied", + "favoriteViewNamed": "Favorite {name}", + "deleteViewNamed": "Delete {name}", + "overwriteSavedViewNamed": "Overwrite {name} with current query", "searchSavedFunction": "Search", "streamNotExist": "The following [STREAM_NAME] streams were not found. Please verify if the streams exist.", "noFieldFound": "No fields found for the selected stream", diff --git a/web/src/locales/languages/es-ES.json b/web/src/locales/languages/es-ES.json index 32bb8f79bd..c099e57bd9 100644 --- a/web/src/locales/languages/es-ES.json +++ b/web/src/locales/languages/es-ES.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Modo rápido", "menuMore": "Más", "menuGroupSetOnce": "Establecer una vez", - "menuGroupSavedViews": "Vistas guardadas", "menuGroupHistory": "Historia", "menuGroupDownloads": "Descargas", "menuGroupSchedule": "Programar", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Se puede especificar el número de registros, por ejemplo: si el número de registros es 1000, el usuario puede obtener un máximo de 1000 registros", "maxEventsScheduleJob": "Se pueden devolver un máximo de 100 000 eventos en un trabajo programado", "histogramDisabledScheduleJob": "El histograma se deshabilitará para el trabajo programado", - "favoriteViews": "Vistas favoritas", "region": "Región", "deleteSavedView": "Eliminar vista guardada", "deleteSavedViewConfirm": "¿Seguro que quieres eliminar la vista guardada {name}?", - "updateSavedView": "Actualizar vista guardada", - "updateSavedViewConfirm": "¿Está seguro de que desea actualizar la vista guardada? Esta acción sobrescribirá la existente.", + "overwriteSavedView": "Sobrescribir vista guardada", + "overwriteSavedViewConfirm": "¿Reemplazar la consulta guardada de {name} por la consulta actual?", + "overwriteSavedViewOk": "Sobrescribir", "collapse": "Contraer", "expand": "Expandir", "syntaxGuideLabel": "Guía de sintaxis", @@ -1250,12 +1249,9 @@ "legendLabel": "Leyenda", "switchToLogs": "Volver a Logs", "savedViewsLabel": "Vistas guardadas", - "savedViewsNotFound": "¡No se encontró la vista guardada!", "savedViewName": "Nombre de la vista", "createSavedView": "Crear vista guardada", "listSavedViews": "Listar vistas guardadas", - "manageSavedViews": "Gestionar vistas guardadas", - "updateSavedViewWithCurrent": "Actualizar esta vista con la búsqueda actual", "pinToToolbar": "Fijar a la barra de herramientas", "unpinFromToolbar": "Desanclar de la barra de herramientas", "shareLink": "Compartir enlace", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Datos devueltos para: {start} a {end})", "histogramErrorBtnLabel": "Haga clic para ver los detalles del error", "applySearch": "Ajusta los parámetros del filtro y haz clic en «Ejecutar consulta».", - "searchSavedView": "Buscar vista guardada", + "savedViewsRowLabel": "Vistas guardadas", + "savedViewsSummaryViews": "{count} vista | {count} vistas", + "savedViewsSummaryFavorites": "{count} favorita | {count} favoritas", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Buscar {count} vista guardada | Buscar {count} vistas guardadas", + "savedViewsSearchPlaceholder": "Buscar vistas guardadas", + "savedViewsFavoritesGroup": "Favoritas ({count})", + "savedViewsAllGroup": "Todas las vistas guardadas", + "savedViewsEmpty": "Aún no hay vistas guardadas", + "savedViewsNoMatch": "Ninguna vista guardada coincide con la búsqueda", + "savedViewApplied": "Aplicada actualmente", + "favoriteViewNamed": "Marcar {name} como favorita", + "deleteViewNamed": "Eliminar {name}", + "overwriteSavedViewNamed": "Sobrescribir {name} con la consulta actual", "searchSavedFunction": "Buscar", "streamNotExist": "Los siguientes streams [STREAM_NAME] no se encontraron. Por favor, verifique si los streams existen.", "noFieldFound": "No se han encontrado campos para el stream seleccionado", diff --git a/web/src/locales/languages/fr-FR.json b/web/src/locales/languages/fr-FR.json index 6d160b1190..63ad9ef45a 100644 --- a/web/src/locales/languages/fr-FR.json +++ b/web/src/locales/languages/fr-FR.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Mode rapide", "menuMore": "Plus", "menuGroupSetOnce": "Réglez une fois", - "menuGroupSavedViews": "Vues enregistrées", "menuGroupHistory": "L'histoire", "menuGroupDownloads": "Téléchargements", "menuGroupSchedule": "Calendrier", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Le nombre d'enregistrements peut être spécifié, par exemple : si le nombre d'enregistrements est de 1000, l'utilisateur peut obtenir un maximum de 1000 enregistrements", "maxEventsScheduleJob": "Un maximum de 100 000 événements peuvent être renvoyés dans la tâche de planification", "histogramDisabledScheduleJob": "L'histogramme sera désactivé pour la tâche de planification", - "favoriteViews": "Vues préférées", "region": "Région", "deleteSavedView": "Supprimer la vue enregistrée", "deleteSavedViewConfirm": "Voulez-vous vraiment supprimer la vue enregistrée {name} ?", - "updateSavedView": "Mettre à jour la vue enregistrée", - "updateSavedViewConfirm": "Êtes-vous sûr de vouloir mettre à jour la vue enregistrée ? Cette action remplacera l'action existante.", + "overwriteSavedView": "Écraser la vue enregistrée", + "overwriteSavedViewConfirm": "Remplacer la requête enregistrée de {name} par la requête actuelle ?", + "overwriteSavedViewOk": "Écraser", "collapse": "Effondrement", "expand": "Élargir", "syntaxGuideLabel": "Guide de syntaxe", @@ -1250,12 +1249,9 @@ "legendLabel": "Légende", "switchToLogs": "Revenir aux Logs", "savedViewsLabel": "Vues enregistrées", - "savedViewsNotFound": "La vue enregistrée n'a pas été trouvée !", "savedViewName": "Afficher le nom", "createSavedView": "Créer une vue enregistrée", "listSavedViews": "Répertorier les vues enregistrées", - "manageSavedViews": "Gérer les vues enregistrées", - "updateSavedViewWithCurrent": "Mettre à jour cette vue avec la recherche actuelle", "pinToToolbar": "Ajouter à la barre d'outils", "unpinFromToolbar": "Détacher de la barre d'outils", "shareLink": "Partager le lien", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Données renvoyées pour : {start} à {end})", "histogramErrorBtnLabel": "Cliquez pour obtenir les détails de l'erreur", "applySearch": "Ajustez les paramètres du filtre et cliquez sur « Exécuter la requête ».", - "searchSavedView": "Rechercher dans la vue enregistrée", + "savedViewsRowLabel": "Vues enregistrées", + "savedViewsSummaryViews": "{count} vue | {count} vues", + "savedViewsSummaryFavorites": "{count} favorite | {count} favorites", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Rechercher {count} vue enregistrée | Rechercher {count} vues enregistrées", + "savedViewsSearchPlaceholder": "Rechercher des vues enregistrées", + "savedViewsFavoritesGroup": "Favorites ({count})", + "savedViewsAllGroup": "Toutes les vues enregistrées", + "savedViewsEmpty": "Aucune vue enregistrée pour le moment", + "savedViewsNoMatch": "Aucune vue enregistrée ne correspond à votre recherche", + "savedViewApplied": "Actuellement appliquée", + "favoriteViewNamed": "Mettre {name} en favori", + "deleteViewNamed": "Supprimer {name}", + "overwriteSavedViewNamed": "Remplacer {name} par la requête actuelle", "searchSavedFunction": "Rechercher", "streamNotExist": "Les flux [STREAM_NAME] suivants n'ont pas été trouvés. Veuillez vérifier si les flux existent.", "noFieldFound": "Aucun champ n'a été trouvé pour le flux sélectionné", diff --git a/web/src/locales/languages/it-IT.json b/web/src/locales/languages/it-IT.json index bc57fee8e7..5ba836c435 100644 --- a/web/src/locales/languages/it-IT.json +++ b/web/src/locales/languages/it-IT.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Modalità rapida", "menuMore": "Altro", "menuGroupSetOnce": "Imposta una volta", - "menuGroupSavedViews": "Visualizzazioni salvate", "menuGroupHistory": "Cronologia", "menuGroupDownloads": "Download", "menuGroupSchedule": "Pianificazione", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "È possibile specificare il numero di record, ad esempio: se il numero di record è 1000, l'utente può ottenere un massimo di 1000 record", "maxEventsScheduleJob": "Un job pianificato può restituire al massimo 100000 eventi", "histogramDisabledScheduleJob": "L'istogramma verrà disabilitato per il job pianificato", - "favoriteViews": "Visualizzazioni preferite", "region": "Regione", "deleteSavedView": "Elimina visualizzazione salvata", "deleteSavedViewConfirm": "Sei sicuro di voler eliminare la vista salvata {name}?", - "updateSavedView": "Aggiorna visualizzazione salvata", - "updateSavedViewConfirm": "Sei sicuro di voler aggiornare la visualizzazione salvata? Questa azione sovrascriverà quella esistente.", + "overwriteSavedView": "Sovrascrivi vista salvata", + "overwriteSavedViewConfirm": "Sostituire la query salvata di {name} con la query corrente?", + "overwriteSavedViewOk": "Sovrascrivi", "collapse": "Comprimi", "expand": "Espandi", "syntaxGuideLabel": "Guida alla sintassi", @@ -1250,12 +1249,9 @@ "legendLabel": "Legenda", "switchToLogs": "Torna ai Log", "savedViewsLabel": "Visualizzazioni salvate", - "savedViewsNotFound": "Visualizzazione salvata non trovata!", "savedViewName": "Visualizza nome", "createSavedView": "Crea visualizzazione salvata", "listSavedViews": "Elenca visualizzazioni salvate", - "manageSavedViews": "Gestisci viste salvate", - "updateSavedViewWithCurrent": "Aggiorna questa vista con la ricerca corrente", "pinToToolbar": "Aggiungi alla barra degli strumenti", "unpinFromToolbar": "Sblocca dalla barra degli strumenti", "shareLink": "Condividi link", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Dati restituiti per: da {start} a {end})", "histogramErrorBtnLabel": "Fai clic per visualizzare i dettagli dell'errore", "applySearch": "Regola i parametri del filtro e fai clic su «Esegui query».", - "searchSavedView": "Cerca nella visualizzazione salvata", + "savedViewsRowLabel": "Viste salvate", + "savedViewsSummaryViews": "{count} vista | {count} viste", + "savedViewsSummaryFavorites": "{count} preferita | {count} preferite", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Cerca {count} vista salvata | Cerca {count} viste salvate", + "savedViewsSearchPlaceholder": "Cerca viste salvate", + "savedViewsFavoritesGroup": "Preferite ({count})", + "savedViewsAllGroup": "Tutte le viste salvate", + "savedViewsEmpty": "Nessuna vista salvata", + "savedViewsNoMatch": "Nessuna vista salvata corrisponde alla ricerca", + "savedViewApplied": "Attualmente applicata", + "favoriteViewNamed": "Aggiungi {name} ai preferiti", + "deleteViewNamed": "Elimina {name}", + "overwriteSavedViewNamed": "Sovrascrivi {name} con la query corrente", "searchSavedFunction": "Cerca", "streamNotExist": "I seguenti stream [STREAM_NAME] non sono stati trovati. Verifica che gli stream esistano.", "noFieldFound": "Nessun campo trovato per lo stream selezionato", diff --git a/web/src/locales/languages/ja-JP.json b/web/src/locales/languages/ja-JP.json index 7b04dc2e18..22593d74b5 100644 --- a/web/src/locales/languages/ja-JP.json +++ b/web/src/locales/languages/ja-JP.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "クイックモード", "menuMore": "もっと", "menuGroupSetOnce": "1 回設定", - "menuGroupSavedViews": "保存済みビュー", "menuGroupHistory": "歴史", "menuGroupDownloads": "ダウンロード", "menuGroupSchedule": "スケジュール", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "レコード数を指定できます。たとえば、レコード数が1000の場合、ユーザーは最大1000レコードを取得できます。", "maxEventsScheduleJob": "スケジュールジョブでは最大10万件のイベントを返すことができます", "histogramDisabledScheduleJob": "スケジュールジョブのヒストグラムは無効になります", - "favoriteViews": "お気に入りビュー", "region": "リージョン", "deleteSavedView": "保存したビューを削除", "deleteSavedViewConfirm": "{name} の保存ビューを削除してもよろしいですか?", - "updateSavedView": "保存済みビューを更新", - "updateSavedViewConfirm": "保存したビューを更新してよろしいですか?この操作は既存のものを上書きします。", + "overwriteSavedView": "保存したビューを上書き", + "overwriteSavedViewConfirm": "{name} の保存済みクエリを現在のクエリで置き換えますか?", + "overwriteSavedViewOk": "上書き", "collapse": "折りたたむ", "expand": "拡大", "syntaxGuideLabel": "シンタックスガイド", @@ -1250,12 +1249,9 @@ "legendLabel": "凡例", "switchToLogs": "ログに戻る", "savedViewsLabel": "保存済みビュー", - "savedViewsNotFound": "保存されたビューが見つかりません", "savedViewName": "ビュー名", "createSavedView": "保存済みビューを作成", "listSavedViews": "保存済みビューを一覧表示", - "manageSavedViews": "保存済みビューの管理", - "updateSavedViewWithCurrent": "現在の検索でこのビューを更新", "pinToToolbar": "ツールバーに固定", "unpinFromToolbar": "ツールバーからピン留め解除", "shareLink": "リンクを共有", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message}(データの返却範囲:{start} から {end})", "histogramErrorBtnLabel": "クリックするとエラーの詳細が表示されます", "applySearch": "フィルターパラメーターを調整し、[クエリを実行] をクリックします。", - "searchSavedView": "保存済みビューを検索", + "savedViewsRowLabel": "保存済みビュー", + "savedViewsSummaryViews": "{count} 件のビュー | {count} 件のビュー", + "savedViewsSummaryFavorites": "お気に入り {count} 件 | お気に入り {count} 件", + "savedViewsSummary": "{views}、{favorites}", + "savedViewsSearchCount": "{count} 件の保存済みビューを検索 | {count} 件の保存済みビューを検索", + "savedViewsSearchPlaceholder": "保存済みビューを検索", + "savedViewsFavoritesGroup": "お気に入り ({count})", + "savedViewsAllGroup": "すべての保存済みビュー", + "savedViewsEmpty": "保存済みビューはまだありません", + "savedViewsNoMatch": "検索に一致する保存済みビューはありません", + "savedViewApplied": "現在適用中", + "favoriteViewNamed": "{name} をお気に入りに追加", + "deleteViewNamed": "{name} を削除", + "overwriteSavedViewNamed": "{name} を現在のクエリで上書き", "searchSavedFunction": "保存済み関数を検索", "streamNotExist": "以下の [STREAM_NAME] ストリームが見つかりませんでした。存在するか確認してください。", "noFieldFound": "選択したストリームのフィールドが見つかりません", diff --git a/web/src/locales/languages/ko-KR.json b/web/src/locales/languages/ko-KR.json index ecffd56391..e8ebd51667 100644 --- a/web/src/locales/languages/ko-KR.json +++ b/web/src/locales/languages/ko-KR.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "퀵 모드", "menuMore": "더 보기", "menuGroupSetOnce": "한 번 설정", - "menuGroupSavedViews": "저장된 뷰", "menuGroupHistory": "기록", "menuGroupDownloads": "다운로드", "menuGroupSchedule": "스케줄", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "레코드 수를 지정할 수 있습니다. 예를 들어, 레코드 수가 1000개인 경우 사용자는 최대 1,000개의 레코드를 가져올 수 있습니다.", "maxEventsScheduleJob": "스케줄 작업에서 최대 100,000개의 이벤트를 반환할 수 있습니다.", "histogramDisabledScheduleJob": "스케줄 작업에 대한 히스토그램이 비활성화됩니다.", - "favoriteViews": "즐겨찾기 보기", "region": "지역", "deleteSavedView": "저장된 뷰 삭제", "deleteSavedViewConfirm": "{name} 저장된 보기를 삭제하시겠습니까?", - "updateSavedView": "저장된 뷰 업데이트", - "updateSavedViewConfirm": "저장된 뷰를 정말로 업데이트하시겠습니까?이 작업을 수행하면 기존 항목을 덮어씁니다.", + "overwriteSavedView": "저장된 뷰 덮어쓰기", + "overwriteSavedViewConfirm": "{name}의 저장된 쿼리를 현재 쿼리로 바꾸시겠습니까?", + "overwriteSavedViewOk": "덮어쓰기", "collapse": "축소", "expand": "확장하기", "syntaxGuideLabel": "구문 가이드", @@ -1250,12 +1249,9 @@ "legendLabel": "범례", "switchToLogs": "로그로 다시 전환", "savedViewsLabel": "저장된 뷰", - "savedViewsNotFound": "저장된 뷰를 찾을 수 없습니다!", "savedViewName": "뷰 이름", "createSavedView": "저장된 뷰 생성", "listSavedViews": "저장된 뷰 목록", - "manageSavedViews": "저장된 보기 관리", - "updateSavedViewWithCurrent": "현재 검색으로 이 보기 업데이트", "pinToToolbar": "도구 모음에 고정", "unpinFromToolbar": "도구 모음에서 고정 해제", "shareLink": "링크 공유", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (반환된 데이터: {start}부터 {end}까지)", "histogramErrorBtnLabel": "오류 세부 정보를 보려면 클릭하십시오.", "applySearch": "필터 매개변수를 조정하고 '쿼리 실행'을 클릭합니다.", - "searchSavedView": "저장된 보기 검색", + "savedViewsRowLabel": "저장된 보기", + "savedViewsSummaryViews": "보기 {count}개 | 보기 {count}개", + "savedViewsSummaryFavorites": "즐겨찾기 {count}개 | 즐겨찾기 {count}개", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "저장된 보기 {count}개 검색 | 저장된 보기 {count}개 검색", + "savedViewsSearchPlaceholder": "저장된 보기 검색", + "savedViewsFavoritesGroup": "즐겨찾기 ({count})", + "savedViewsAllGroup": "모든 저장된 보기", + "savedViewsEmpty": "저장된 보기가 아직 없습니다", + "savedViewsNoMatch": "검색과 일치하는 저장된 보기가 없습니다", + "savedViewApplied": "현재 적용됨", + "favoriteViewNamed": "{name} 즐겨찾기에 추가", + "deleteViewNamed": "{name} 삭제", + "overwriteSavedViewNamed": "{name}을(를) 현재 쿼리로 덮어쓰기", "searchSavedFunction": "검색", "streamNotExist": "[STREAM_NAME] 스트림을 찾을 수 없습니다. 스트림이 존재하는지 확인하세요.", "noFieldFound": "선택한 스트림에 대한 필드를 찾을 수 없습니다.", diff --git a/web/src/locales/languages/nl-NL.json b/web/src/locales/languages/nl-NL.json index c58ed5eaa0..f08e94bb6e 100644 --- a/web/src/locales/languages/nl-NL.json +++ b/web/src/locales/languages/nl-NL.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Snelle modus", "menuMore": "Meer", "menuGroupSetOnce": "Eenmaal instellen", - "menuGroupSavedViews": "Opgeslagen weergaven", "menuGroupHistory": "Historie", "menuGroupDownloads": "Downloads", "menuGroupSchedule": "Schema", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Het aantal records kan worden gespecificeerd, bijvoorbeeld: als het aantal records 1000 is, kan de gebruiker maximaal 1000 records krijgen", "maxEventsScheduleJob": "Er kunnen maximaal 100.000 gebeurtenissen worden geretourneerd in de geplande taak", "histogramDisabledScheduleJob": "Het histogram wordt uitgeschakeld voor de planningstaak", - "favoriteViews": "Favoriete weergaven", "region": "regio", "deleteSavedView": "Opgeslagen weergave verwijderen", "deleteSavedViewConfirm": "Weet je zeker dat je de opgeslagen weergave {name} wilt verwijderen?", - "updateSavedView": "Opgeslagen weergave bijwerken", - "updateSavedViewConfirm": "Weet je zeker dat je de opgeslagen weergave wilt bijwerken? Met deze actie wordt de bestaande actie overschreven.", + "overwriteSavedView": "Opgeslagen weergave overschrijven", + "overwriteSavedViewConfirm": "De opgeslagen query van {name} vervangen door de huidige query?", + "overwriteSavedViewOk": "Overschrijven", "collapse": "Inklappen", "expand": "Uitbreiden", "syntaxGuideLabel": "Handleiding voor syntaxis", @@ -1250,12 +1249,9 @@ "legendLabel": "Legenda", "switchToLogs": "Schakel terug naar Logs", "savedViewsLabel": "Opgeslagen weergaven", - "savedViewsNotFound": "Opgeslagen weergave niet gevonden!", "savedViewName": "Naam bekijken", "createSavedView": "Opgeslagen weergave maken", "listSavedViews": "Opgeslagen weergaven weergeven", - "manageSavedViews": "Opgeslagen weergaven beheren", - "updateSavedViewWithCurrent": "Deze weergave bijwerken met de huidige zoekopdracht", "pinToToolbar": "Aan de werkbalk vastmaken", "unpinFromToolbar": "Van de werkbalk loskoppelen", "shareLink": "Link delen", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Gegevens geretourneerd voor: {start} tot {end})", "histogramErrorBtnLabel": "Klik voor foutdetails", "applySearch": "Pas de filterparameters aan en klik op 'Query uitvoeren'.", - "searchSavedView": "Opgeslagen weergaven zoeken", + "savedViewsRowLabel": "Opgeslagen weergaven", + "savedViewsSummaryViews": "{count} weergave | {count} weergaven", + "savedViewsSummaryFavorites": "{count} favoriet | {count} favorieten", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "{count} opgeslagen weergave zoeken | {count} opgeslagen weergaven zoeken", + "savedViewsSearchPlaceholder": "Opgeslagen weergaven zoeken", + "savedViewsFavoritesGroup": "Favorieten ({count})", + "savedViewsAllGroup": "Alle opgeslagen weergaven", + "savedViewsEmpty": "Nog geen opgeslagen weergaven", + "savedViewsNoMatch": "Geen opgeslagen weergaven komen overeen met uw zoekopdracht", + "savedViewApplied": "Momenteel toegepast", + "favoriteViewNamed": "{name} als favoriet markeren", + "deleteViewNamed": "{name} verwijderen", + "overwriteSavedViewNamed": "{name} overschrijven met de huidige query", "searchSavedFunction": "Zoek", "streamNotExist": "De volgende [STREAM_NAME]-streams zijn niet gevonden. Controleer of de streams bestaan.", "noFieldFound": "Geen velden gevonden voor de geselecteerde stream", diff --git a/web/src/locales/languages/pl-PL.json b/web/src/locales/languages/pl-PL.json index e53b39f210..d52b188f52 100644 --- a/web/src/locales/languages/pl-PL.json +++ b/web/src/locales/languages/pl-PL.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Tryb szybki", "menuMore": "Więcej", "menuGroupSetOnce": "Ustaw raz", - "menuGroupSavedViews": "Zapisane widoki", "menuGroupHistory": "Historia", "menuGroupDownloads": "Pobieranie", "menuGroupSchedule": "Harmonogram", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Można określić liczbę rekordów, np. jeśli liczba rekordów wynosi 1000, użytkownik może otrzymać maksymalnie 1000 rekordów", "maxEventsScheduleJob": "Maksymalnie 100 000 zdarzeń może zostać zwróconych w zaplanowanym zadaniu", "histogramDisabledScheduleJob": "Histogram będzie wyłączony dla zaplanowanego zadania", - "favoriteViews": "Ulubione widoki", "region": "Region", "deleteSavedView": "Usuń zapisany widok", "deleteSavedViewConfirm": "Czy na pewno chcesz usunąć zapisany widok {name}?", - "updateSavedView": "Aktualizuj zapisany widok", - "updateSavedViewConfirm": "Czy na pewno chcesz zaktualizować zapisany widok? Ta czynność zastąpi istniejący.", + "overwriteSavedView": "Zastąp zapisany widok", + "overwriteSavedViewConfirm": "Zastąpić zapisane zapytanie widoku {name} bieżącym zapytaniem?", + "overwriteSavedViewOk": "Zastąp", "collapse": "Zwiń", "expand": "Rozwiń", "syntaxGuideLabel": "Przewodnik po składni", @@ -1250,12 +1249,9 @@ "legendLabel": "Legenda", "switchToLogs": "Przełącz z powrotem na Logi", "savedViewsLabel": "Zapisany widok", - "savedViewsNotFound": "Nie znaleziono zapisanego widoku!", "savedViewName": "Nazwa widoku", "createSavedView": "Utwórz zapisany widok", "listSavedViews": "Lista zapisanych widoków", - "manageSavedViews": "Zarządzaj zapisanymi widokami", - "updateSavedViewWithCurrent": "Aktualizuj ten widok bieżącym wyszukiwaniem", "pinToToolbar": "Przypnij do paska narzędzi", "unpinFromToolbar": "Odepnij z paska narzędzi", "shareLink": "Udostępnij link", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Dane zwrócone dla: {start} do {end})", "histogramErrorBtnLabel": "Kliknij, aby zobaczyć szczegóły błędu", "applySearch": "Dostosuj parametry filtrów i kliknij 'Uruchom zapytanie'.", - "searchSavedView": "Szukaj w zapisanych widokach", + "savedViewsRowLabel": "Zapisane widoki", + "savedViewsSummaryViews": "{count} widok | {count} widoków", + "savedViewsSummaryFavorites": "{count} ulubiony | {count} ulubionych", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Szukaj wśród {count} zapisanego widoku | Szukaj wśród {count} zapisanych widoków", + "savedViewsSearchPlaceholder": "Szukaj w zapisanych widokach", + "savedViewsFavoritesGroup": "Ulubione ({count})", + "savedViewsAllGroup": "Wszystkie zapisane widoki", + "savedViewsEmpty": "Brak zapisanych widoków", + "savedViewsNoMatch": "Żaden zapisany widok nie pasuje do wyszukiwania", + "savedViewApplied": "Obecnie zastosowany", + "favoriteViewNamed": "Dodaj {name} do ulubionych", + "deleteViewNamed": "Usuń {name}", + "overwriteSavedViewNamed": "Zastąp {name} bieżącym zapytaniem", "searchSavedFunction": "Szukaj", "streamNotExist": "Nie znaleziono następujących strumieni [STREAM_NAME]. Sprawdź, czy strumienie istnieją.", "noFieldFound": "Nie znaleziono pól dla wybranego strumienia", diff --git a/web/src/locales/languages/pt-PT.json b/web/src/locales/languages/pt-PT.json index d02e52ab23..1d90db095f 100644 --- a/web/src/locales/languages/pt-PT.json +++ b/web/src/locales/languages/pt-PT.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Modo rápido", "menuMore": "Mais", "menuGroupSetOnce": "Definir uma vez", - "menuGroupSavedViews": "Visualizações guardadas", "menuGroupHistory": "Histórico", "menuGroupDownloads": "Transferências", "menuGroupSchedule": "Agendamento", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "O número de registos pode ser especificado, por exemplo: se o número de registos for 1000, o utilizador poderá obter no máximo 1000 registos", "maxEventsScheduleJob": "Podem ser devolvidos no máximo 100.000 eventos no trabalho agendado", "histogramDisabledScheduleJob": "O histograma será desativado para o trabalho agendado", - "favoriteViews": "Visualizações favoritas", "region": "Região", "deleteSavedView": "Eliminar visualização guardada", "deleteSavedViewConfirm": "Tem a certeza de que pretende eliminar a vista guardada {name}?", - "updateSavedView": "Atualizar visualização guardada", - "updateSavedViewConfirm": "Tem a certeza de que pretende atualizar a visualização guardada? Esta ação substituirá a existente.", + "overwriteSavedView": "Substituir vista guardada", + "overwriteSavedViewConfirm": "Substituir a consulta guardada de {name} pela consulta atual?", + "overwriteSavedViewOk": "Substituir", "collapse": "Recolher", "expand": "Expandir", "syntaxGuideLabel": "Guia de sintaxe", @@ -1250,12 +1249,9 @@ "legendLabel": "Legenda", "switchToLogs": "Voltar aos Logs", "savedViewsLabel": "Visualizações guardadas", - "savedViewsNotFound": "A visualização guardada não foi encontrada!", "savedViewName": "Nome da visualização", "createSavedView": "Criar visualização guardada", "listSavedViews": "Listar visualizações guardadas", - "manageSavedViews": "Gerir Visualizações Guardadas", - "updateSavedViewWithCurrent": "Atualizar esta visualização com a pesquisa atual", "pinToToolbar": "Fixar na barra de ferramentas", "unpinFromToolbar": "Desfixar da barra de ferramentas", "shareLink": "Partilhar link", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Dados devolvidos para: {start} a {end})", "histogramErrorBtnLabel": "Clique para ver os detalhes do erro", "applySearch": "Ajuste os parâmetros do filtro e clique em “Executar consulta”.", - "searchSavedView": "Pesquisar visualizações guardadas", + "savedViewsRowLabel": "Visualizações guardadas", + "savedViewsSummaryViews": "{count} visualização | {count} visualizações", + "savedViewsSummaryFavorites": "{count} favorita | {count} favoritas", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Pesquisar {count} visualização guardada | Pesquisar {count} visualizações guardadas", + "savedViewsSearchPlaceholder": "Pesquisar visualizações guardadas", + "savedViewsFavoritesGroup": "Favoritas ({count})", + "savedViewsAllGroup": "Todas as visualizações guardadas", + "savedViewsEmpty": "Ainda não há visualizações guardadas", + "savedViewsNoMatch": "Nenhuma visualização guardada corresponde à pesquisa", + "savedViewApplied": "Aplicada atualmente", + "favoriteViewNamed": "Marcar {name} como favorita", + "deleteViewNamed": "Eliminar {name}", + "overwriteSavedViewNamed": "Substituir {name} pela consulta atual", "searchSavedFunction": "Pesquisar", "streamNotExist": "Os seguintes streams [STREAM_NAME] não foram encontrados. Verifique se os streams existem.", "noFieldFound": "Nenhum campo encontrado para o stream selecionado", diff --git a/web/src/locales/languages/ru-RU.json b/web/src/locales/languages/ru-RU.json index e7aa7ec8b2..6b7eb1b95d 100644 --- a/web/src/locales/languages/ru-RU.json +++ b/web/src/locales/languages/ru-RU.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Быстрый режим", "menuMore": "Ещё", "menuGroupSetOnce": "Установить один раз", - "menuGroupSavedViews": "Сохранённые представления", "menuGroupHistory": "История", "menuGroupDownloads": "Загрузки", "menuGroupSchedule": "Расписание", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Можно указать количество записей, например: если количество записей равно 1000, пользователь может получить максимум 1000 записей", "maxEventsScheduleJob": "В запланированной задаче может быть возвращено максимум 100 000 событий", "histogramDisabledScheduleJob": "Гистограмма будет отключена для запланированной задачи", - "favoriteViews": "Избранные представления", "region": "Регион", "deleteSavedView": "Удалить сохранённое представление", "deleteSavedViewConfirm": "Вы уверены, что хотите удалить сохранённое представление {name}?", - "updateSavedView": "Обновить сохранённое представление", - "updateSavedViewConfirm": "Вы уверены, что хотите обновить сохранённое представление? Это действие перезапишет существующее.", + "overwriteSavedView": "Перезаписать сохранённое представление", + "overwriteSavedViewConfirm": "Заменить сохранённый запрос представления {name} текущим запросом?", + "overwriteSavedViewOk": "Перезаписать", "collapse": "Свернуть", "expand": "Развернуть", "syntaxGuideLabel": "Руководство по синтаксису", @@ -1250,12 +1249,9 @@ "legendLabel": "Легенда", "switchToLogs": "Вернуться к логам", "savedViewsLabel": "Сохранённое представление", - "savedViewsNotFound": "Сохранённое представление не найдено!", "savedViewName": "Имя представления", "createSavedView": "Создать сохранённое представление", "listSavedViews": "Список сохранённых представлений", - "manageSavedViews": "Управление сохранёнными представлениями", - "updateSavedViewWithCurrent": "Обновить это представление текущим запросом", "pinToToolbar": "Закрепить на панели инструментов", "unpinFromToolbar": "Открепить от панели инструментов", "shareLink": "Поделиться ссылкой", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Данные за: {start} - {end})", "histogramErrorBtnLabel": "Нажмите для просмотра ошибки", "applySearch": "Настройте параметры фильтра и нажмите «Выполнить запрос».", - "searchSavedView": "Поиск сохранённых представлений", + "savedViewsRowLabel": "Сохранённые представления", + "savedViewsSummaryViews": "{count} представление | {count} представлений", + "savedViewsSummaryFavorites": "{count} избранное | {count} избранных", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Поиск среди {count} сохранённого представления | Поиск среди {count} сохранённых представлений", + "savedViewsSearchPlaceholder": "Поиск сохранённых представлений", + "savedViewsFavoritesGroup": "Избранное ({count})", + "savedViewsAllGroup": "Все сохранённые представления", + "savedViewsEmpty": "Сохранённых представлений пока нет", + "savedViewsNoMatch": "Нет сохранённых представлений, соответствующих поиску", + "savedViewApplied": "Применено сейчас", + "favoriteViewNamed": "Добавить {name} в избранное", + "deleteViewNamed": "Удалить {name}", + "overwriteSavedViewNamed": "Перезаписать {name} текущим запросом", "searchSavedFunction": "Поиск", "streamNotExist": "Следующие потоки [STREAM_NAME] не найдены. Пожалуйста, проверьте, существуют ли потоки.", "noFieldFound": "Для выбранного потока поля не найдены", diff --git a/web/src/locales/languages/tr-TR.json b/web/src/locales/languages/tr-TR.json index ab285674be..a4091fded9 100644 --- a/web/src/locales/languages/tr-TR.json +++ b/web/src/locales/languages/tr-TR.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Hızlı Mod", "menuMore": "Daha", "menuGroupSetOnce": "Bir Kez Ayarla", - "menuGroupSavedViews": "Kayıtlı Görünümler", "menuGroupHistory": "Tarihçe", "menuGroupDownloads": "İndirilenler", "menuGroupSchedule": "Çizelge", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Kayıt sayısı belirtilebilir, örneğin: kayıt sayısı 1000 ise, kullanıcı maksimum 1000 kayıt alabilir", "maxEventsScheduleJob": "Zamanlama işinde maksimum 100000 olay iade edilebilir", "histogramDisabledScheduleJob": "Zaman çizelgesi işi için histogram devre dışı bırakılır", - "favoriteViews": "Favori Görünümler", "region": "Bölge", "deleteSavedView": "Kayıtlı Görünümü Sil", "deleteSavedViewConfirm": "Kaydedilmiş {name} görünümünü silmek istediğinizden emin misiniz?", - "updateSavedView": "Kayıtlı Görünümü Güncelle", - "updateSavedViewConfirm": "Kaydedilen görünümü güncellemek istediğinizden emin misiniz? Bu işlem mevcut olanın üzerine yazacaktır.", + "overwriteSavedView": "Kayıtlı Görünümün Üzerine Yaz", + "overwriteSavedViewConfirm": "{name} görünümünün kayıtlı sorgusu geçerli sorguyla değiştirilsin mi?", + "overwriteSavedViewOk": "Üzerine yaz", "collapse": "Daralt", "expand": "Genişletin", "syntaxGuideLabel": "Sözdizimi Kılavuzu", @@ -1250,12 +1249,9 @@ "legendLabel": "Lejant", "switchToLogs": "Günlüklere geri dön", "savedViewsLabel": "Kayıtlı Görünümler", - "savedViewsNotFound": "Kaydedilen görünüm bulunamadı!", "savedViewName": "Görünüm Adı", "createSavedView": "Kayıtlı Görünüm Oluştur", "listSavedViews": "Kayıtlı Görünümleri Listele", - "manageSavedViews": "Kayıtlı Görünümleri Yönet", - "updateSavedViewWithCurrent": "Bu görünümü geçerli arama ile güncelle", "pinToToolbar": "Araç çubuğuna sabitleyin", "unpinFromToolbar": "Araç çubuğundan sabitlemeyi kaldırın", "shareLink": "Bağlantıyı Paylaş", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Döndürülen veri aralığı: {start} - {end})", "histogramErrorBtnLabel": "Hata detayları için tıklayınız", "applySearch": "Filtre parametrelerini ayarlayın ve 'Sorguyu çalıştır'ı tıklayın.", - "searchSavedView": "Kayıtlı Görünümde Ara", + "savedViewsRowLabel": "Kayıtlı Görünümler", + "savedViewsSummaryViews": "{count} görünüm | {count} görünüm", + "savedViewsSummaryFavorites": "{count} favori | {count} favori", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "{count} kayıtlı görünümde ara | {count} kayıtlı görünümde ara", + "savedViewsSearchPlaceholder": "Kayıtlı görünümlerde ara", + "savedViewsFavoritesGroup": "Favoriler ({count})", + "savedViewsAllGroup": "Tüm Kayıtlı Görünümler", + "savedViewsEmpty": "Henüz kayıtlı görünüm yok", + "savedViewsNoMatch": "Aramanızla eşleşen kayıtlı görünüm yok", + "savedViewApplied": "Şu anda uygulanıyor", + "favoriteViewNamed": "{name} görünümünü favorilere ekle", + "deleteViewNamed": "{name} görünümünü sil", + "overwriteSavedViewNamed": "{name} görünümünü geçerli sorguyla üzerine yaz", "searchSavedFunction": "Ara", "streamNotExist": "[STREAM_NAME] akışları bulunamadı. Lütfen akışların var olup olmadığını doğrulayın.", "noFieldFound": "Seçilen akış için alan bulunamadı", diff --git a/web/src/locales/languages/vi-VN.json b/web/src/locales/languages/vi-VN.json index c8844a0c09..b449c4122f 100644 --- a/web/src/locales/languages/vi-VN.json +++ b/web/src/locales/languages/vi-VN.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "Chế độ Nhanh", "menuMore": "Thêm", "menuGroupSetOnce": "Đặt một lần", - "menuGroupSavedViews": "Chế độ xem đã lưu", "menuGroupHistory": "Lịch sử", "menuGroupDownloads": "Tải xuống", "menuGroupSchedule": "Lịch biểu", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "Số lượng bản ghi có thể được chỉ định, ví dụ: nếu số bản ghi là 1000 thì người dùng có thể nhận tối đa 1000 bản ghi", "maxEventsScheduleJob": "Tối đa 100000 sự kiện có thể được trả về trong công việc theo lịch", "histogramDisabledScheduleJob": "Biểu đồ histogram sẽ bị vô hiệu hóa cho công việc theo lịch", - "favoriteViews": "Chế độ xem yêu thích", "region": "Vùng", "deleteSavedView": "Xóa chế độ xem đã lưu", "deleteSavedViewConfirm": "Bạn có chắc chắn muốn xóa chế độ xem đã lưu {name} không?", - "updateSavedView": "Cập nhật chế độ xem đã lưu", - "updateSavedViewConfirm": "Bạn có chắc chắn muốn cập nhật chế độ xem đã lưu? Hành động này sẽ ghi đè lên chế độ xem hiện có.", + "overwriteSavedView": "Ghi đè chế độ xem đã lưu", + "overwriteSavedViewConfirm": "Thay thế truy vấn đã lưu của {name} bằng truy vấn hiện tại?", + "overwriteSavedViewOk": "Ghi đè", "collapse": "Thu gọn", "expand": "Mở rộng", "syntaxGuideLabel": "Hướng dẫn cú pháp", @@ -1250,12 +1249,9 @@ "legendLabel": "Chú thích", "switchToLogs": "Quay lại Nhật ký", "savedViewsLabel": "Chế độ xem đã lưu", - "savedViewsNotFound": "Không tìm thấy chế độ xem đã lưu!", "savedViewName": "Tên chế độ xem", "createSavedView": "Tạo chế độ xem đã lưu", "listSavedViews": "Danh sách chế độ xem đã lưu", - "manageSavedViews": "Quản lý chế độ xem đã lưu", - "updateSavedViewWithCurrent": "Cập nhật chế độ xem này với tìm kiếm hiện tại", "pinToToolbar": "Ghim vào thanh công cụ", "unpinFromToolbar": "Bỏ ghim khỏi thanh công cụ", "shareLink": "Chia sẻ liên kết", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message} (Dữ liệu trả về từ {start} đến {end})", "histogramErrorBtnLabel": "Nhấp để xem chi tiết lỗi", "applySearch": "Điều chỉnh tham số bộ lọc và nhấp 'Chạy truy vấn'.", - "searchSavedView": "Tìm kiếm chế độ xem đã lưu", + "savedViewsRowLabel": "Chế độ xem đã lưu", + "savedViewsSummaryViews": "{count} chế độ xem | {count} chế độ xem", + "savedViewsSummaryFavorites": "{count} mục yêu thích | {count} mục yêu thích", + "savedViewsSummary": "{views}, {favorites}", + "savedViewsSearchCount": "Tìm trong {count} chế độ xem đã lưu | Tìm trong {count} chế độ xem đã lưu", + "savedViewsSearchPlaceholder": "Tìm kiếm chế độ xem đã lưu", + "savedViewsFavoritesGroup": "Yêu thích ({count})", + "savedViewsAllGroup": "Tất cả chế độ xem đã lưu", + "savedViewsEmpty": "Chưa có chế độ xem nào được lưu", + "savedViewsNoMatch": "Không có chế độ xem đã lưu nào khớp với tìm kiếm", + "savedViewApplied": "Đang được áp dụng", + "favoriteViewNamed": "Thêm {name} vào mục yêu thích", + "deleteViewNamed": "Xóa {name}", + "overwriteSavedViewNamed": "Ghi đè {name} bằng truy vấn hiện tại", "searchSavedFunction": "Tìm kiếm", "streamNotExist": "Các stream [STREAM_NAME] sau đây không được tìm thấy. Vui lòng kiểm tra xem các stream có tồn tại hay không.", "noFieldFound": "Không tìm thấy trường nào cho stream đã chọn", diff --git a/web/src/locales/languages/zh-CN.json b/web/src/locales/languages/zh-CN.json index e3b0f14520..1e9d70af4b 100644 --- a/web/src/locales/languages/zh-CN.json +++ b/web/src/locales/languages/zh-CN.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "快速模式", "menuMore": "更多", "menuGroupSetOnce": "设置一次", - "menuGroupSavedViews": "保存的视图", "menuGroupHistory": "历史", "menuGroupDownloads": "下载", "menuGroupSchedule": "日程安排", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "可以指定记录数,例如:如果记录数为 1000,则用户最多可以获得 1000 条记录", "maxEventsScheduleJob": "调度任务中最多可以返回 100,000 个事件", "histogramDisabledScheduleJob": "调度作业的直方图将被禁用", - "favoriteViews": "收藏的视图", "region": "区域", "deleteSavedView": "删除保存的视图", "deleteSavedViewConfirm": "您确定要删除已保存视图 {name} 吗?", - "updateSavedView": "更新保存的视图", - "updateSavedViewConfirm": "您确定要更新保存的视图吗?此操作将覆盖现有操作。", + "overwriteSavedView": "覆盖保存的视图", + "overwriteSavedViewConfirm": "要用当前查询替换 {name} 已保存的查询吗?", + "overwriteSavedViewOk": "覆盖", "collapse": "收起", "expand": "扩大", "syntaxGuideLabel": "语法提示", @@ -1250,12 +1249,9 @@ "legendLabel": "图例", "switchToLogs": "切换回日志", "savedViewsLabel": "已保存的视图", - "savedViewsNotFound": "未找到已保存的视图!", "savedViewName": "查看名称", "createSavedView": "创建保存的视图", "listSavedViews": "列出保存的视图", - "manageSavedViews": "管理已保存视图", - "updateSavedViewWithCurrent": "使用当前搜索更新此视图", "pinToToolbar": "固定到工具栏", "unpinFromToolbar": "从工具栏中取消固定", "shareLink": "分享链接", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message}(返回的数据范围:{start} 到 {end})", "histogramErrorBtnLabel": "点击查看错误详情", "applySearch": "调整过滤器参数,然后单击 “运行查询”。", - "searchSavedView": "搜索保存的视图", + "savedViewsRowLabel": "已保存的视图", + "savedViewsSummaryViews": "{count} 个视图 | {count} 个视图", + "savedViewsSummaryFavorites": "{count} 个收藏 | {count} 个收藏", + "savedViewsSummary": "{views},{favorites}", + "savedViewsSearchCount": "搜索 {count} 个已保存的视图 | 搜索 {count} 个已保存的视图", + "savedViewsSearchPlaceholder": "搜索已保存的视图", + "savedViewsFavoritesGroup": "收藏 ({count})", + "savedViewsAllGroup": "所有已保存的视图", + "savedViewsEmpty": "暂无已保存的视图", + "savedViewsNoMatch": "没有与搜索匹配的已保存视图", + "savedViewApplied": "当前已应用", + "favoriteViewNamed": "将 {name} 加入收藏", + "deleteViewNamed": "删除 {name}", + "overwriteSavedViewNamed": "用当前查询覆盖 {name}", "searchSavedFunction": "搜索", "streamNotExist": "以下 [STREAM_NAME] 流未找到。请验证这些流是否存在。", "noFieldFound": "未找到所选数据流的字段", diff --git a/web/src/locales/languages/zh-TW.json b/web/src/locales/languages/zh-TW.json index 6021d81213..f6ad68a944 100644 --- a/web/src/locales/languages/zh-TW.json +++ b/web/src/locales/languages/zh-TW.json @@ -1188,7 +1188,6 @@ "quickModeLabel": "快速模式", "menuMore": "更多", "menuGroupSetOnce": "一次性設定", - "menuGroupSavedViews": "已儲存的檢視", "menuGroupHistory": "歷史記錄", "menuGroupDownloads": "下載", "menuGroupSchedule": "排程", @@ -1216,12 +1215,12 @@ "noOfRecordsTooltip": "可指定記錄數,例如:設為 1000 則最多可取得 1000 筆記錄", "maxEventsScheduleJob": "排程工作最多可傳回 100000 筆事件", "histogramDisabledScheduleJob": "排程工作將停用直方圖", - "favoriteViews": "收藏的檢視", "region": "區域", "deleteSavedView": "刪除已儲存的檢視", "deleteSavedViewConfirm": "您確定要刪除已儲存的檢視 {name} 嗎?", - "updateSavedView": "更新已儲存的檢視", - "updateSavedViewConfirm": "確定要更新已儲存的檢視嗎?此操作會覆蓋現有的檢視。", + "overwriteSavedView": "覆寫已儲存的檢視", + "overwriteSavedViewConfirm": "要以目前的查詢取代 {name} 已儲存的查詢嗎?", + "overwriteSavedViewOk": "覆寫", "collapse": "收合", "expand": "展開", "syntaxGuideLabel": "語法指南", @@ -1250,12 +1249,9 @@ "legendLabel": "圖例", "switchToLogs": "切換回日誌", "savedViewsLabel": "已儲存的檢視", - "savedViewsNotFound": "找不到已儲存的檢視!", "savedViewName": "檢視名稱", "createSavedView": "建立已儲存的檢視", "listSavedViews": "已儲存的檢視列表", - "manageSavedViews": "管理已儲存的檢視", - "updateSavedViewWithCurrent": "使用目前搜尋更新此檢視", "pinToToolbar": "固定到工具列", "unpinFromToolbar": "從工具列取消固定", "shareLink": "分享連結", @@ -1270,7 +1266,20 @@ "functionErrorDataRange": "{message}(資料回傳範圍:{start} 至 {end})", "histogramErrorBtnLabel": "按一下檢視錯誤詳細資訊", "applySearch": "調整篩選參數並按一下 [執行查詢]。", - "searchSavedView": "搜尋已儲存的檢視", + "savedViewsRowLabel": "已儲存的檢視", + "savedViewsSummaryViews": "{count} 個檢視 | {count} 個檢視", + "savedViewsSummaryFavorites": "{count} 個收藏 | {count} 個收藏", + "savedViewsSummary": "{views},{favorites}", + "savedViewsSearchCount": "搜尋 {count} 個已儲存的檢視 | 搜尋 {count} 個已儲存的檢視", + "savedViewsSearchPlaceholder": "搜尋已儲存的檢視", + "savedViewsFavoritesGroup": "收藏 ({count})", + "savedViewsAllGroup": "所有已儲存的檢視", + "savedViewsEmpty": "尚無已儲存的檢視", + "savedViewsNoMatch": "沒有符合搜尋的已儲存檢視", + "savedViewApplied": "目前已套用", + "favoriteViewNamed": "將 {name} 加入收藏", + "deleteViewNamed": "刪除 {name}", + "overwriteSavedViewNamed": "以目前查詢覆蓋 {name}", "searchSavedFunction": "搜尋", "streamNotExist": "找不到下列 [STREAM_NAME] 串流。請確認這些串流是否存在。", "noFieldFound": "所選串流找不到任何欄位", diff --git a/web/src/plugins/logs/SearchBar.autoRun.spec.ts b/web/src/plugins/logs/SearchBar.autoRun.spec.ts index e62edb0a5d..4cfa577413 100644 --- a/web/src/plugins/logs/SearchBar.autoRun.spec.ts +++ b/web/src/plugins/logs/SearchBar.autoRun.spec.ts @@ -341,6 +341,12 @@ describe("SearchBar — auto-run wiring (item 2)", () => { ] as [string, string[]][]) { await wrapper!.get(`[data-test="${trigger}"]`).trigger("click"); await flushPromises(); + if (trigger === "logs-search-bar-utilities-menu-btn") { + document + .querySelector('[data-test="logs-search-bar-menu-list-saved-views-btn"]')! + .click(); + await flushPromises(); + } for (const testId of ids) { const element = document.querySelector(`[data-test="${testId}"]`)!; expect(element).not.toBeNull(); diff --git a/web/src/plugins/logs/SearchBar.nlDetection.spec.ts b/web/src/plugins/logs/SearchBar.nlDetection.spec.ts index 1ca69f2b01..c020cddaeb 100644 --- a/web/src/plugins/logs/SearchBar.nlDetection.spec.ts +++ b/web/src/plugins/logs/SearchBar.nlDetection.spec.ts @@ -45,6 +45,10 @@ describe("SearchBar — Run button ignores natural-language detection (#15049)", name: "ODropdownItem", template: '
', }, + ODropdownSub: { + name: "ODropdownSub", + template: '
', + }, ODropdownSeparator: { name: "ODropdownSeparator", template: "
" }, }; diff --git a/web/src/plugins/logs/SearchBar.refreshCacheDropdown.spec.ts b/web/src/plugins/logs/SearchBar.refreshCacheDropdown.spec.ts index 078535780d..76fa73aa5a 100644 --- a/web/src/plugins/logs/SearchBar.refreshCacheDropdown.spec.ts +++ b/web/src/plugins/logs/SearchBar.refreshCacheDropdown.spec.ts @@ -46,6 +46,10 @@ describe("SearchBar — Refresh Cache & Run Query dropdown visibility (#14488)", '
', emits: ["select"], }, + ODropdownSub: { + name: "ODropdownSub", + template: '
', + }, ODropdownSeparator: { name: "ODropdownSeparator", template: '
', diff --git a/web/src/plugins/logs/SearchBar.savedViewsDialog.spec.ts b/web/src/plugins/logs/SearchBar.savedViewsDialog.spec.ts index 6ee6426f1e..844f0a93d2 100644 --- a/web/src/plugins/logs/SearchBar.savedViewsDialog.spec.ts +++ b/web/src/plugins/logs/SearchBar.savedViewsDialog.spec.ts @@ -19,18 +19,34 @@ import i18n from "@/locales"; import store from "@/test/unit/helpers/store"; import { createRouter, createMemoryHistory } from "vue-router"; import SearchBar from "@/plugins/logs/SearchBar.vue"; -import SavedViewsListDialog from "@/components/savedViews/SavedViewsListDialog.vue"; import ConfirmDialog from "@/components/ConfirmDialog.vue"; +import SavedViewsMenu from "@/components/savedViews/SavedViewsMenu.vue"; +import SavedViewsSubmenu from "@/components/savedViews/SavedViewsSubmenu.vue"; -const { mockSavedViewsGet, mockSavedViewsDelete } = vi.hoisted(() => ({ +const { + mockSavedViewsGet, + mockSavedViewsDelete, + mockSavedViewsPost, + mockSavedViewsPut, + mockGetViewDetail, +} = vi.hoisted(() => ({ mockSavedViewsGet: vi.fn(), mockSavedViewsDelete: vi.fn(), + mockSavedViewsPost: vi.fn(), + mockSavedViewsPut: vi.fn(), + mockGetViewDetail: vi.fn(), })); vi.mock("@/services/saved_views", async (importOriginal) => { const { overlayServiceMock } = await import("@/test/unit/helpers/mockService"); return overlayServiceMock(await importOriginal(), { - default: { get: mockSavedViewsGet, delete: mockSavedViewsDelete }, + default: { + get: mockSavedViewsGet, + delete: mockSavedViewsDelete, + post: mockSavedViewsPost, + put: mockSavedViewsPut, + getViewDetail: mockGetViewDetail, + }, }); }); @@ -53,7 +69,7 @@ const logsView2 = { view_type: "logs", }; -describe("SearchBar — saved views list dialog", () => { +describe("SearchBar — saved views submenu", () => { let wrapper: VueWrapper | undefined; const mountSearchBar = () => @@ -65,7 +81,6 @@ describe("SearchBar — saved views list dialog", () => { }, }); - const dialog = () => wrapper!.findComponent(SavedViewsListDialog); const favorites = () => JSON.parse(localStorage.getItem("savedViews") || "{}"); beforeEach(() => { @@ -81,44 +96,208 @@ describe("SearchBar — saved views list dialog", () => { vi.clearAllMocks(); }); - it("passes the logs data-test prefix, views and favourites to the dialog", async () => { + it("passes the logs data-test prefix, views and favourites to the submenu", async () => { localStorage.setItem("savedViews", JSON.stringify({ l1: logsView })); wrapper = mountSearchBar(); wrapper.vm.searchObj.data.savedViews = [logsView, logsView2]; await flushPromises(); - expect(dialog().props("dataTestPrefix")).toBe("logs-saved-views-dialog"); - expect(dialog().props("views")).toEqual([logsView, logsView2]); - expect(dialog().props("favoriteIds")).toEqual(["l1"]); - expect(dialog().props("favoriteViews")).toEqual([logsView]); + wrapper.vm.appliedSavedViewId = "l2"; + wrapper.vm.utilitiesMenuOpen = true; + await flushPromises(); + + const submenu = wrapper.findComponent(SavedViewsSubmenu); + expect(submenu.props()).toMatchObject({ + dataTestPrefix: "logs-search-bar-menu-saved-views", + triggerTestId: "logs-search-bar-menu-list-saved-views-btn", + views: [logsView, logsView2], + favoriteIds: ["l1"], + }); + expect(submenu.vm.$attrs["applied-view-id"]).toBe("l2"); + + document + .querySelector('[data-test="logs-search-bar-menu-list-saved-views-btn"]')! + .click(); + await flushPromises(); + const menu = wrapper.findComponent(SavedViewsMenu); + expect(menu.props()).toMatchObject({ + views: [logsView, logsView2], + favoriteIds: ["l1"], + appliedViewId: "l2", + dataTestPrefix: "logs-search-bar-menu-saved-views", + }); }); - it("toggles a favourite from the dialog", async () => { + const detail = (streamName = "default") => ({ + status: 200, + data: { + data: { + data: { + resultGrid: { columns: [], colOrder: {}, colSizes: {} }, + stream: { + streamType: "logs", + selectedStream: [streamName], + selectedFields: [], + }, + datetime: {}, + tempFunctionContent: "", + }, + meta: { regions: [], showTransformEditor: false, logsVisualizeToggle: "logs" }, + }, + }, + }); + + it("apply marks the view applied only after the apply path completes", async () => { + wrapper = mountSearchBar(); + await flushPromises(); + wrapper.vm.searchObj.data.stream.streamLists = [{ label: "default", value: "default" }]; + wrapper.vm.searchObj.data.stream.streamType = "logs"; + wrapper.vm.dateTimeRef = { setSavedDate: vi.fn() }; + + mockGetViewDetail.mockResolvedValueOnce(detail("missing")); + await wrapper.vm.applySavedView(logsView); + await flushPromises(); + expect(wrapper.vm.appliedSavedViewId).toBeNull(); + + mockGetViewDetail.mockResolvedValueOnce(detail()); + await wrapper.vm.applySavedView(logsView); + await flushPromises(); + expect(mockGetViewDetail).toHaveBeenLastCalledWith("default", "l1"); + expect(wrapper.vm.appliedSavedViewId).toBe("l1"); + }, 20000); + + it("create marks the new view applied from the response view_id", async () => { + mockSavedViewsPost.mockResolvedValue({ + status: 200, + data: { view_id: "new1", org_id: "default" }, + }); wrapper = mountSearchBar(); await flushPromises(); - dialog().vm.$emit("toggle-favorite", logsView, false); + await wrapper.vm.createSavedViews("fresh view"); await flushPromises(); - expect(dialog().props("favoriteIds")).toEqual(["l1"]); + + expect(mockSavedViewsPost).toHaveBeenCalled(); + expect(wrapper.vm.appliedSavedViewId).toBe("new1"); + }, 20000); + + it("overwrite closes both menus, confirms, then marks the view applied once it saves", async () => { + mockSavedViewsPut.mockResolvedValue({ status: 200, data: {} }); + wrapper = mountSearchBar(); + await flushPromises(); + wrapper.vm.searchObj.data.stream.selectedStream = ["default"]; + wrapper.vm.utilitiesMenuOpen = true; + wrapper.vm.savedViewsDropdownOpen = true; + + wrapper.vm.requestUpdateSavedView(logsView2); + await flushPromises(); + expect(wrapper.vm.utilitiesMenuOpen).toBe(false); + expect(wrapper.vm.savedViewsDropdownOpen).toBe(false); + expect(mockSavedViewsPut).not.toHaveBeenCalled(); + + const confirm = wrapper + .findAllComponents(ConfirmDialog) + .find((c) => c.props("modelValue") === true)!; + expect(confirm.props("title")).toBe(i18n.global.t("search.overwriteSavedView")); + expect(confirm.props()).toMatchObject({ + messageKeypath: "search.overwriteSavedViewConfirm", + messageName: "slow requests", + focusCancel: true, + }); + + confirm.vm.$emit("update:ok"); + await flushPromises(); + + expect(mockSavedViewsPut).toHaveBeenCalled(); + expect(wrapper.vm.appliedSavedViewId).toBe("l2"); + }, 20000); + + it("a cancelled overwrite leaves the view untouched", async () => { + wrapper = mountSearchBar(); + await flushPromises(); + wrapper.vm.searchObj.data.stream.selectedStream = ["default"]; + + wrapper.vm.requestUpdateSavedView(logsView2); + await flushPromises(); + const confirm = wrapper + .findAllComponents(ConfirmDialog) + .find((c) => c.props("modelValue") === true)!; + confirm.vm.$emit("update:cancel"); + await flushPromises(); + + expect(mockSavedViewsPut).not.toHaveBeenCalled(); + expect(wrapper.vm.confirmOverwrite).toBe(false); + expect(wrapper.vm.appliedSavedViewId).toBeNull(); + }, 20000); + + describe("applied check follows the live search", () => { + const applyView = async () => { + wrapper = mountSearchBar(); + await flushPromises(); + wrapper.vm.searchObj.data.stream.streamLists = [{ label: "default", value: "default" }]; + wrapper.vm.searchObj.data.stream.streamType = "logs"; + wrapper.vm.dateTimeRef = { setSavedDate: vi.fn() }; + mockGetViewDetail.mockResolvedValueOnce(detail()); + await wrapper.vm.applySavedView(logsView); + await flushPromises(); + expect(wrapper.vm.appliedSavedViewId).toBe("l1"); + wrapper.vm.searchObj.shouldIgnoreWatcher = false; + }; + + it("keeps the check through the changes the apply itself makes", async () => { + await applyView(); + wrapper.vm.searchObj.shouldIgnoreWatcher = true; + wrapper.vm.searchObj.data.query = "select * from restored"; + await flushPromises(); + wrapper.vm.searchObj.shouldIgnoreWatcher = false; + expect(wrapper.vm.appliedSavedViewId).toBe("l1"); + }); + + it("clears when the query text is edited", async () => { + await applyView(); + wrapper.vm.searchObj.data.query = "level = 'error'"; + await flushPromises(); + expect(wrapper.vm.appliedSavedViewId).toBeNull(); + }); + + it("clears when the stream changes", async () => { + await applyView(); + wrapper.vm.searchObj.data.stream.selectedStream = ["other"]; + await flushPromises(); + expect(wrapper.vm.appliedSavedViewId).toBeNull(); + }); + + it("clears on Reset", async () => { + await applyView(); + wrapper.vm.resetFilters(); + await flushPromises(); + expect(wrapper.vm.appliedSavedViewId).toBeNull(); + }); + }); + + it("toggles a favourite from the submenu", async () => { + wrapper = mountSearchBar(); + await flushPromises(); + + wrapper.vm.handleFavoriteSavedView(logsView, false); + await flushPromises(); + expect(wrapper.vm.favoriteViews).toEqual(["l1"]); expect(Object.keys(favorites())).toEqual(["l1"]); - dialog().vm.$emit("toggle-favorite", logsView, true); + wrapper.vm.handleFavoriteSavedView(logsView, true); await flushPromises(); - expect(dialog().props("favoriteIds")).toEqual([]); + expect(wrapper.vm.favoriteViews).toEqual([]); expect(favorites()).toEqual({}); }); - it("a confirmed delete from the dialog drops the favourite", async () => { + it("a confirmed delete from the submenu drops the favourite", async () => { localStorage.setItem("savedViews", JSON.stringify({ l1: logsView })); wrapper = mountSearchBar(); await flushPromises(); - wrapper.vm.savedViewsListDialog = true; + + wrapper.vm.handleDeleteSavedView(logsView); await flushPromises(); - dialog().vm.$emit("delete", logsView); - await flushPromises(); - - expect(dialog().props("open")).toBe(false); const confirm = wrapper .findAllComponents(ConfirmDialog) .find((c) => c.props("modelValue") === true)!; @@ -134,10 +313,49 @@ describe("SearchBar — saved views list dialog", () => { await flushPromises(); expect(mockSavedViewsDelete).toHaveBeenCalledWith("default", "l1"); - expect(dialog().props("favoriteIds")).toEqual([]); + expect(wrapper.vm.favoriteViews).toEqual([]); expect(favorites()).toEqual({}); }, 20000); + it("clears the applied check when the applied view is deleted", async () => { + wrapper = mountSearchBar(); + await flushPromises(); + wrapper.vm.appliedSavedViewId = "l1"; + wrapper.vm.deleteViewID = "l1"; + + wrapper.vm.deleteSavedViews(); + await flushPromises(); + + expect(wrapper.vm.appliedSavedViewId).toBeNull(); + }, 20000); + + it("keeps the applied check when another view is deleted", async () => { + wrapper = mountSearchBar(); + await flushPromises(); + wrapper.vm.appliedSavedViewId = "l2"; + wrapper.vm.deleteViewID = "l1"; + + wrapper.vm.deleteSavedViews(); + await flushPromises(); + + expect(wrapper.vm.appliedSavedViewId).toBe("l2"); + }, 20000); + + it("closes both saved-views menus and queues the right view for delete", async () => { + wrapper = mountSearchBar(); + await flushPromises(); + wrapper.vm.utilitiesMenuOpen = true; + wrapper.vm.savedViewsDropdownOpen = true; + + wrapper.vm.handleDeleteSavedView(logsView2); + await flushPromises(); + + expect(wrapper.vm.utilitiesMenuOpen).toBe(false); + expect(wrapper.vm.savedViewsDropdownOpen).toBe(false); + expect(wrapper.vm.deleteViewID).toBe("l2"); + expect(wrapper.vm.deleteViewName).toBe("slow requests"); + }); + it("prunes favourites deleted elsewhere once the list loads, freeing the cap", async () => { const stored: Record = { t1: { ...logsView, view_id: "t1", view_type: "traces" }, @@ -148,18 +366,18 @@ describe("SearchBar — saved views list dialog", () => { wrapper = mountSearchBar(); wrapper.vm.searchObj.data.savedViews = []; await flushPromises(); - expect(dialog().props("favoriteIds")).toHaveLength(10); + expect(wrapper.vm.favoriteViews).toHaveLength(10); - wrapper.vm.openSavedViewsList(); + wrapper.vm.onUtilitiesMenuOpenChange(true); await flushPromises(); expect(mockSavedViewsGet).toHaveBeenCalledTimes(1); - expect(dialog().props("favoriteIds")).toEqual(["l1"]); + expect(wrapper.vm.favoriteViews).toEqual(["l1"]); expect(Object.keys(favorites())).toEqual(["t1", "l1"]); - dialog().vm.$emit("toggle-favorite", logsView2, false); + wrapper.vm.handleFavoriteSavedView(logsView2, false); await flushPromises(); - expect(dialog().props("favoriteIds")).toEqual(["l1", "l2"]); + expect(wrapper.vm.favoriteViews).toEqual(["l1", "l2"]); }); it("keeps every favourite when the list fails to load", async () => { @@ -170,13 +388,13 @@ describe("SearchBar — saved views list dialog", () => { wrapper.vm.searchObj.data.savedViews = []; await flushPromises(); - wrapper.vm.openSavedViewsList(); + wrapper.vm.onUtilitiesMenuOpenChange(true); await flushPromises(); expect(mockSavedViewsGet).toHaveBeenCalledTimes(1); - expect(dialog().props("loading")).toBe(false); - expect(dialog().props("views")).toEqual([]); - expect(dialog().props("favoriteIds")).toEqual(["l1", "gone"]); + expect(wrapper.vm.searchObj.loadingSavedView).toBeFalsy(); + expect(wrapper.vm.searchObj.data.savedViews).toEqual([]); + expect(wrapper.vm.favoriteViews).toEqual(["l1", "gone"]); expect(favorites()).toEqual({ l1: logsView, gone }); }); }); diff --git a/web/src/plugins/logs/SearchBar.savedViewsPinned.spec.ts b/web/src/plugins/logs/SearchBar.savedViewsPinned.spec.ts index bfa919fabd..a959823fe0 100644 --- a/web/src/plugins/logs/SearchBar.savedViewsPinned.spec.ts +++ b/web/src/plugins/logs/SearchBar.savedViewsPinned.spec.ts @@ -149,11 +149,11 @@ describe("SearchBar — pinned saved views", () => { await group().get('[data-test="logs-search-bar-saved-views-pinned-list-btn"]').trigger("click"); await flushPromises(); const button = document.querySelector( - '[data-test="logs-search-bar-saved-views-menu-update-Named view"]', + '[data-test="logs-search-bar-saved-views-menu-update-named"]', )!; expect(button).not.toBeNull(); expect(button.getAttribute("aria-label")).toBe( - i18n.global.t("search.updateSavedViewWithCurrent"), + i18n.global.t("search.overwriteSavedViewNamed", { name: "Named view" }), ); expect(button.getAttribute("aria-disabled")).toBe("true"); expect(button.getAttribute("title")).toBeNull(); @@ -172,11 +172,11 @@ describe("SearchBar — pinned saved views", () => { await group().get('[data-test="logs-search-bar-saved-views-pinned-list-btn"]').trigger("click"); await flushPromises(); const button = document.querySelector( - '[data-test="logs-search-bar-saved-views-menu-update-Named view"]', + '[data-test="logs-search-bar-saved-views-menu-update-named"]', )!; expect(button).not.toBeNull(); expect(button.getAttribute("aria-label")).toBe( - i18n.global.t("search.updateSavedViewWithCurrent"), + i18n.global.t("search.overwriteSavedViewNamed", { name: "Named view" }), ); expect(button.getAttribute("aria-disabled")).toBeNull(); expect(button.getAttribute("disabled")).toBeNull(); @@ -192,11 +192,11 @@ describe("SearchBar — pinned saved views", () => { useLogsAutoRun().invalidateExecuted("search-around"); await flushPromises(); const apply = vi.spyOn(wrapper.vm, "applySavedView").mockImplementation(() => {}); - const update = vi.spyOn(wrapper.vm, "quickUpdateSavedView").mockImplementation(() => {}); + const update = vi.spyOn(wrapper.vm, "requestUpdateSavedView").mockImplementation(() => {}); await group().get('[data-test="logs-search-bar-saved-views-pinned-list-btn"]').trigger("click"); await flushPromises(); const button = document.querySelector( - '[data-test="logs-search-bar-saved-views-menu-update-Named view"]', + '[data-test="logs-search-bar-saved-views-menu-update-named"]', )!; const event = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }); button.dispatchEvent(event); @@ -214,11 +214,11 @@ describe("SearchBar — pinned saved views", () => { wrapper.vm.searchObj.meta.logsVisualizeToggle = "logs"; await flushPromises(); const apply = vi.spyOn(wrapper.vm, "applySavedView").mockImplementation(() => {}); - const update = vi.spyOn(wrapper.vm, "quickUpdateSavedView").mockImplementation(() => {}); + const update = vi.spyOn(wrapper.vm, "requestUpdateSavedView").mockImplementation(() => {}); await group().get('[data-test="logs-search-bar-saved-views-pinned-list-btn"]').trigger("click"); await flushPromises(); const button = document.querySelector( - '[data-test="logs-search-bar-saved-views-menu-update-Named view"]', + '[data-test="logs-search-bar-saved-views-menu-update-named"]', )!; expect(button.getAttribute("aria-disabled")).toBeNull(); button.dispatchEvent( @@ -228,7 +228,7 @@ describe("SearchBar — pinned saved views", () => { expect(update).toHaveBeenCalledExactlyOnceWith(view); expect(apply).not.toHaveBeenCalled(); const row = document.querySelector( - '[data-test="logs-search-bar-saved-views-menu-apply-Named view"]', + '[data-test="logs-search-bar-saved-views-menu-apply-named"]', )!; row.dispatchEvent( new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }), diff --git a/web/src/plugins/logs/SearchBar.spec.ts b/web/src/plugins/logs/SearchBar.spec.ts index e56ba3bb73..1e4c801bb8 100644 --- a/web/src/plugins/logs/SearchBar.spec.ts +++ b/web/src/plugins/logs/SearchBar.spec.ts @@ -1354,8 +1354,6 @@ describe("SearchBar.vue Actual Component Methods", () => { savedViewDropdownModel: false, deleteViewID: "view123", confirmDelete: false, - updateViewObj: { view_id: "view456", view_name: "Test View" }, - confirmUpdate: false, customDownloadDialog: false, downloadCustomRange: 100, downloadCustomInitialNumber: 1, @@ -1405,19 +1403,6 @@ describe("SearchBar.vue Actual Component Methods", () => { componentInstance.confirmDelete = true; }), - handleUpdateSavedView: vi.fn((item) => { - if (componentInstance.searchObj.data.stream.selectedStream.length === 0) { - componentInstance.notify({ - type: "negative", - message: "No stream available to update save view.", - }); - return; - } - componentInstance.savedViewDropdownModel = false; - componentInstance.updateViewObj = item; - componentInstance.confirmUpdate = true; - }), - confirmDeleteSavedViews: vi.fn(() => { componentInstance.deleteSavedViews(); }), @@ -1426,13 +1411,6 @@ describe("SearchBar.vue Actual Component Methods", () => { componentInstance.customDownloadDialog = true; }), - confirmUpdateSavedViews: vi.fn(() => { - componentInstance.updateSavedViews( - componentInstance.updateViewObj.view_id, - componentInstance.updateViewObj.view_name, - ); - }), - downloadRangeData: vi.fn(() => { let initNumber = parseInt(componentInstance.downloadCustomInitialNumber); if (initNumber < 0) { @@ -1965,31 +1943,6 @@ describe("SearchBar.vue Actual Component Methods", () => { expect(componentInstance.confirmDelete).toBe(true); }); - // Test 81: handleUpdateSavedView with no streams - it("should notify when no streams available for update", () => { - componentInstance.searchObj.data.stream.selectedStream = []; - const item = { view_id: "view123", view_name: "Test View" }; - - componentInstance.handleUpdateSavedView(item); - - expect(componentInstance.notify).toHaveBeenCalledWith({ - type: "negative", - message: "No stream available to update save view.", - }); - }); - - // Test 82: handleUpdateSavedView with streams available - it("should set up update confirmation when streams available", () => { - componentInstance.searchObj.data.stream.selectedStream = ["test-stream"]; - const item = { view_id: "view123", view_name: "Test View" }; - - componentInstance.handleUpdateSavedView(item); - - expect(componentInstance.savedViewDropdownModel).toBe(false); - expect(componentInstance.updateViewObj).toEqual(item); - expect(componentInstance.confirmUpdate).toBe(true); - }); - // Test 83: toggleCustomDownloadDialog method it("should show custom download dialog", () => { componentInstance.toggleCustomDownloadDialog(); @@ -2875,15 +2828,6 @@ describe("SearchBar.vue Actual Component Methods", () => { expect(componentInstance.searchObj.data.customDownloadQueryObj.query.size).toBe(100); }); - // Test 166: confirmUpdateSavedViews method - it("should call updateSavedViews with correct parameters", () => { - componentInstance.updateViewObj = { view_id: "123", view_name: "Test View" }; - - componentInstance.confirmUpdateSavedViews(); - - expect(componentInstance.updateSavedViews).toHaveBeenCalledWith("123", "Test View"); - }); - // Test 167: confirmDeleteSavedViews method it("should call deleteSavedViews", () => { componentInstance.confirmDeleteSavedViews(); diff --git a/web/src/plugins/logs/SearchBar.vue b/web/src/plugins/logs/SearchBar.vue index 47927d5c98..a36104d56b 100644 --- a/web/src/plugins/logs/SearchBar.vue +++ b/web/src/plugins/logs/SearchBar.vue @@ -267,74 +267,21 @@ along with this program. If not, see . - -
- - {{ t("confirmDialog.loading") }} -
-
- - - {{ view.view_name }} - - -
- - {{ t("search.savedViewsNotFound") }} - -
- - - {{ t("search.createSavedView") }} - - - {{ t("search.manageSavedViews") }} - + . />
- +