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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t("confirmDialog.loading") }}
-
-
-
-
- {{ t("search.favoriteViews") }}
-
-
-
-
-
- {{ value }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t("common.edit") }}
-
-
- {{ t("common.delete") }}
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("confirmDialog.loading") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ props.views.length ? t("search.savedViewsNoMatch") : t("search.savedViewsEmpty") }}
+
+
+
+
+ {{ t("search.createSavedView") }}
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ {{ props.view.view_name }}
+
+
+
+
+
+
+
+
+
+ {{ props.updateReason }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ open && emit('open')"
+ >
+
+
+
+ {{ t("search.savedViewsRowLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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 }}
-
-
- {{ saveViewReason }}
-
-
-
-
-
- {{ t("search.savedViewsNotFound") }}
-
-
-
-
- {{ t("search.createSavedView") }}
-
-
- {{ t("search.manageSavedViews") }}
-
+
.
/>
-
+
.
-
-
+
+
.
-
-
-
-
- {{ t("search.listSavedViews") }}
-
-
-
-
-
-
- {{ t("search.createSavedView") }}
-
-
-
-
+
@@ -1710,28 +1646,17 @@ along with this program. If not, see .
v-model="confirmDelete"
/>
-
-
@@ -1885,9 +1810,7 @@ import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
import ODropdownSeparator from "@/lib/overlay/Dropdown/ODropdownSeparator.vue";
import ODropdownGroup from "@/lib/overlay/Dropdown/ODropdownGroup.vue";
import { hasFieldCondition, removeFieldCondition } from "@/plugins/logs/filterUtils";
-import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
-import OTruncatedText from "@/lib/core/Typography/OTruncatedText.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import OSwitch from "@/lib/forms/Switch/OSwitch.vue";
@@ -1903,8 +1826,8 @@ import type { AlertBuildOptions } from "@/ts/interfaces/alertPrefill";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
import OTree from "@/lib/data/Tree/OTree.vue";
import { makeSavedViewSchema, type SavedViewForm } from "./SearchBar.SavedView.schema";
-import { sortSavedViews } from "@/components/savedViews/savedViewsSort";
-import SavedViewsListDialog from "@/components/savedViews/SavedViewsListDialog.vue";
+import SavedViewsMenu from "@/components/savedViews/SavedViewsMenu.vue";
+import SavedViewsSubmenu from "@/components/savedViews/SavedViewsSubmenu.vue";
import { useFavoriteSavedViews } from "@/composables/useFavoriteSavedViews";
import { makeSavedFunctionSchema, type SavedFunctionForm } from "./SearchBar.SavedFunction.schema";
import { applySessionTimezone } from "@/utils/storage";
@@ -1988,9 +1911,7 @@ export default defineComponent({
OToggleGroup,
OToggleGroupItem,
OFormToggleGroup,
- OSpinner,
OTooltip,
- OTruncatedText,
OInput,
OSelect,
OForm,
@@ -1998,10 +1919,11 @@ export default defineComponent({
OFormSelect,
OSwitch,
OTree,
- SavedViewsListDialog,
RecentSearchesButton,
RecentSearchesDrawer,
RecentSearchesFocusDropdown,
+ SavedViewsMenu,
+ SavedViewsSubmenu,
},
emits: [
"searchdata",
@@ -2033,25 +1955,12 @@ export default defineComponent({
},
handleDeleteSavedView(item: any) {
this.savedViewDropdownModel = false;
- this.savedViewsListDialog = false;
+ this.savedViewsDropdownOpen = false;
+ this.utilitiesMenuOpen = false;
this.deleteViewID = item.view_id;
this.deleteViewName = item.view_name;
this.confirmDelete = true;
},
- handleUpdateSavedView(item: any) {
- if (this.searchObj.data.stream.selectedStream.length == 0) {
- toast({
- variant: "error",
- message: this.t("logs.searchBar.noStreamUpdateView"),
- });
- return;
- }
- if (this.blockWithReason(this.saveViewReason)) return;
- this.savedViewDropdownModel = false;
- this.savedViewsListDialog = false;
- this.updateViewObj = item;
- this.confirmUpdate = true;
- },
confirmDeleteSavedViews() {
this.deleteSavedViews();
},
@@ -2059,10 +1968,6 @@ export default defineComponent({
if (this.blockWithReason(this.customRangeReason || this.downloadReason)) return;
this.customDownloadDialog = true;
},
- confirmUpdateSavedViews() {
- this.updateSavedViews(this.updateViewObj.view_id, this.updateViewObj.view_name);
- return;
- },
downloadRangeData() {
let initNumber = parseInt(this.downloadCustomInitialNumber);
if (initNumber < 0) {
@@ -2421,7 +2326,6 @@ export default defineComponent({
const dateTimeRef = ref(null);
const {
favoriteIds: favoriteViews,
- favoriteViews: localSavedViews,
toggleFavorite: handleFavoriteSavedView,
removeFavorite,
pruneFavorites,
@@ -2459,10 +2363,12 @@ export default defineComponent({
}));
const showExplainDialog = ref(false);
const confirmDelete = ref(false);
+ const confirmOverwrite = ref(false);
+ const overwriteViewId = ref("");
+ const overwriteViewName = ref("");
const deleteViewID = ref("");
const deleteViewName = ref("");
const savedViewDropdownModel = ref(false);
- const savedViewsListDialog = ref(false);
const moreOptionsDropdownModel = ref(false);
const searchTerm = ref("");
@@ -2630,9 +2536,6 @@ export default defineComponent({
const vrlEditorNlpMode = ref(false); // Track VRL editor's AI mode
- const confirmUpdate = ref(false);
- const updateViewObj = ref({});
-
const transformTypes = computed(() => {
return [{ label: t("logs.searchBar.transformTypeFunction"), value: "function" }];
});
@@ -3539,11 +3442,6 @@ export default defineComponent({
savedViewDropdownModel.value = false;
};
- const openSavedViewsList = () => {
- loadSavedView();
- savedViewsListDialog.value = true;
- };
-
// ── Saved views quick dropdown (pinned toolbar) ──────────────────────
// Controlled open state so a quick update can close the menu itself.
const savedViewsDropdownOpen = ref(false);
@@ -3553,13 +3451,29 @@ export default defineComponent({
if (open) loadSavedView();
};
- const sortedSavedViews = computed(() =>
- sortSavedViews(searchObj.data.savedViews, favoriteViews.value),
- );
+ const appliedSavedViewId = ref(null);
+ const savedViewSignature = () =>
+ JSON.stringify([searchObj.data.query, searchObj.data.stream.selectedStream]);
+ let appliedSignature = "";
+ const markSavedViewApplied = (viewId: string) => {
+ appliedSignature = savedViewSignature();
+ appliedSavedViewId.value = viewId;
+ };
+ // While a view is being applied the search state is rewritten on purpose, so it re-baselines instead of clearing.
+ watch(savedViewSignature, (signature) => {
+ if (!appliedSavedViewId.value) return;
+ if (searchObj.shouldIgnoreWatcher) appliedSignature = signature;
+ else if (signature !== appliedSignature) appliedSavedViewId.value = null;
+ });
- // One-click overwrite of a view with the current search state — no list
- // dialog, no stacked confirm dialog.
- const quickUpdateSavedView = (item: any) => {
+ const utilitiesMenuOpen = ref(false);
+ const onUtilitiesMenuOpenChange = (open: boolean) => {
+ utilitiesMenuOpen.value = open;
+ if (open) loadSavedView();
+ };
+
+ // Overwriting destroys the stored query, so it is confirmed first.
+ const requestUpdateSavedView = (item: any) => {
if (searchObj.data.stream.selectedStream.length == 0) {
toast({
variant: "error",
@@ -3569,7 +3483,19 @@ export default defineComponent({
}
if (blockWithReason(saveViewReason.value)) return;
savedViewsDropdownOpen.value = false;
- updateSavedViews(item.view_id, item.view_name);
+ utilitiesMenuOpen.value = false;
+ overwriteViewId.value = item.view_id;
+ overwriteViewName.value = item.view_name;
+ confirmOverwrite.value = true;
+ };
+
+ const cancelOverwriteSavedView = () => {
+ confirmOverwrite.value = false;
+ };
+
+ const confirmOverwriteSavedView = () => {
+ confirmOverwrite.value = false;
+ updateSavedViews(overwriteViewId.value, overwriteViewName.value);
};
// Common function to restore visualization data and sync to URL
@@ -3982,6 +3908,7 @@ export default defineComponent({
message: t("logs.searchBar.viewAppliedSuccess", { name: item.view_name }),
variant: "success",
});
+ markSavedViewApplied(item.view_id);
setTimeout(async () => {
try {
searchObj.loadingHistogram = false;
@@ -4064,6 +3991,7 @@ export default defineComponent({
.then((res: any) => {
removeFavorite(deleteViewID.value);
if (res.status == 200) {
+ if (appliedSavedViewId.value === deleteViewID.value) appliedSavedViewId.value = null;
toast({
message: t("search.viewDeletedSuccessfully"),
variant: "success",
@@ -4187,6 +4115,7 @@ export default defineComponent({
message: t("search.viewCreatedSuccessfully"),
variant: "success",
});
+ if (res.data.view_id) markSavedViewApplied(res.data.view_id);
refreshSavedViews();
isSavedViewAction.value = "create";
} else {
@@ -4245,6 +4174,7 @@ export default defineComponent({
},
);
+ markSavedViewApplied(viewID);
toast({
message: t("search.viewUpdatedSuccessfully"),
variant: "success",
@@ -4326,6 +4256,7 @@ export default defineComponent({
}
const resetFilters = () => {
+ appliedSavedViewId.value = null;
flushEditorValue();
if (searchObj.meta.sqlMode == true) {
const parsedSQL = fnParsedSQL();
@@ -5179,12 +5110,9 @@ export default defineComponent({
updateTimezone,
dateTimeRef,
fnSavedView,
- openSavedViewsList,
applySavedView,
savedViewsDropdownOpen,
onSavedViewsDropdownOpenChange,
- sortedSavedViews,
- quickUpdateSavedView,
isSavedViewAction,
// Saved-view OForm (schema returned from setup() so the Options-API
// template resolves :schema; a bare import would be out of scope).
@@ -5196,8 +5124,12 @@ export default defineComponent({
deleteViewID,
deleteViewName,
confirmDelete,
+ confirmOverwrite,
+ overwriteViewName,
+ requestUpdateSavedView,
+ confirmOverwriteSavedView,
+ cancelOverwriteSavedView,
savedViewDropdownModel,
- savedViewsListDialog,
moreOptionsDropdownModel,
fnSavedFunctionDialog,
// Saved-function OForm (owner pattern, Rule ③): the form is created with
@@ -5222,7 +5154,9 @@ export default defineComponent({
confirmSavedViewDialogVisible,
handleFavoriteSavedView,
favoriteViews,
- localSavedViews,
+ appliedSavedViewId,
+ onUtilitiesMenuOpenChange,
+ utilitiesMenuOpen,
loadSavedView,
config,
patternsAccess,
@@ -5268,8 +5202,6 @@ export default defineComponent({
navigateToSearchInspect,
searchTerm,
filteredFunctionOptions,
- confirmUpdate,
- updateViewObj,
updateSavedViews,
checkQuery,
checkFnQuery,
diff --git a/web/src/plugins/traces/SearchBar.spec.ts b/web/src/plugins/traces/SearchBar.spec.ts
index 0b04849dd6..9e3bce3fb6 100644
--- a/web/src/plugins/traces/SearchBar.spec.ts
+++ b/web/src/plugins/traces/SearchBar.spec.ts
@@ -303,7 +303,7 @@ vi.mock("@/composables/useTraces", () => ({
// ---------------------------------------------------------------------------
import SearchBar from "@/plugins/traces/SearchBar.vue";
import analytics from "@/services/product_analytics";
-import SavedViewsListDialog from "@/components/savedViews/SavedViewsListDialog.vue";
+import SavedViewsSubmenu from "@/components/savedViews/SavedViewsSubmenu.vue";
import { useToolbarPins } from "@/composables/useToolbarPins";
import { placeCaret } from "@/test/unit/helpers/monacoCaret";
@@ -418,6 +418,12 @@ const sharedStubs = {
template: '
',
},
ODropdownSeparator: { template: "
" },
+ ODropdownSub: {
+ name: "ODropdownSub",
+ template:
+ '
',
+ props: ["open", "textValue", "description"],
+ },
};
// ---------------------------------------------------------------------------
@@ -951,9 +957,9 @@ describe("SearchBar", () => {
await flushPromises();
expect(pinnedBtn().exists()).toBe(true);
expect(savedViewsGroup().exists()).toBe(false);
- expect(wrapper.find('[data-test="traces-search-bar-menu-saved-views-group"]').exists()).toBe(
- true,
- );
+ expect(
+ wrapper.find('[data-test="traces-search-bar-menu-list-saved-views-btn"]').exists(),
+ ).toBe(true);
});
it("shrinks the toggle labels for the pinned RED Metrics width", async () => {
@@ -2003,6 +2009,16 @@ describe("SearchBar", () => {
});
});
+ it("marks a just-created view as applied when the response has its id", async () => {
+ wrapper = mountSearchBar();
+ await flushPromises();
+
+ (wrapper.vm as any).newViewName = "fresh";
+ await (wrapper.vm as any).saveAsNewView();
+
+ expect((wrapper.vm as any).appliedSavedViewId).toBe("n1");
+ });
+
it("apply emits apply-saved-view with the stored data", async () => {
mockGetViewDetail.mockResolvedValue({ data: { ...tracesView, data: expectedData() } });
wrapper = mountSearchBar();
@@ -2049,12 +2065,12 @@ describe("SearchBar", () => {
expect(wrapper.emitted("apply-saved-view")).toBeUndefined();
});
- it("pinned dropdown has no delete action", async () => {
+ it("pinned dropdown offers a delete action beside update", async () => {
wrapper = mountSearchBar();
await flushPromises();
expect(wrapper.find('[data-test="traces-saved-view-update-t1"]').exists()).toBe(true);
- expect(wrapper.find('[data-test="traces-saved-view-delete-t1"]').exists()).toBe(false);
+ expect(wrapper.find('[data-test="traces-saved-view-delete-t1"]').exists()).toBe(true);
});
it("pinned dropdown lists favourites first, then by name, with a star on favourites", async () => {
@@ -2067,13 +2083,10 @@ describe("SearchBar", () => {
.findAll('[data-test^="traces-saved-view-apply-"]')
.map((item) => item.attributes("data-test"));
expect(items).toEqual(["traces-saved-view-apply-t1", "traces-saved-view-apply-t2"]);
- const iconName = (id: string) =>
- wrapper
- .findAllComponents({ name: "OIcon" })
- .find((c) => c.attributes("data-test") === `traces-saved-view-icon-${id}`)
- ?.props("name");
- expect(iconName("t1")).toBe("star");
- expect(iconName("t2")).toBe("saved-search");
+ const pressed = (id: string) =>
+ wrapper.find(`[data-test="traces-saved-view-favorite-${id}"]`).attributes("aria-pressed");
+ expect(pressed("t1")).toBe("true");
+ expect(pressed("t2")).toBe("false");
});
it("pinned dropdown shows a loading row while the list loads", async () => {
@@ -2081,56 +2094,37 @@ describe("SearchBar", () => {
wrapper = mountSearchBar();
await flushPromises();
- expect(wrapper.find('[data-test="traces-saved-views-menu-loading"]').exists()).toBe(true);
- expect(wrapper.findComponent(SavedViewsListDialog).props("loading")).toBe(true);
+ expect(wrapper.find('[data-test="traces-saved-view-loading"]').exists()).toBe(true);
});
describe("in the More menu", () => {
- const moreGroup = () =>
- wrapper.find('[data-test="traces-search-bar-menu-saved-views-group"]');
+ const moreGroup = () => wrapper.findComponent(SavedViewsSubmenu);
const toolbarGroup = () => wrapper.find('[data-test="traces-search-bar-saved-views"]');
- const dialog = () => wrapper.findComponent(SavedViewsListDialog);
+ const dialog = () => wrapper.findComponent(SavedViewsSubmenu);
- it("shows the saved-views group with List, Create and the pin button", async () => {
+ it("shows the saved-views submenu with Create and the pin button", async () => {
wrapper = mountSearchBar();
await flushPromises();
expect(moreGroup().exists()).toBe(true);
expect(
- moreGroup().find('[data-test="traces-search-bar-menu-list-saved-views-btn"]').exists(),
+ moreGroup().find('[data-test="traces-search-bar-menu-saved-views-delete-t1"]').exists(),
).toBe(true);
+ expect(
+ moreGroup().find('[data-test="traces-search-bar-menu-manage-saved-views-btn"]').exists(),
+ ).toBe(false);
expect(
moreGroup().find('[data-test="traces-search-bar-menu-create-saved-view-btn"]').exists(),
).toBe(true);
expect(
moreGroup().find('[data-test="traces-search-bar-menu-pin-saved-views-btn"]').exists(),
).toBe(true);
- expect(moreGroup().find('[data-test="traces-saved-view-apply-t1"]').exists()).toBe(false);
+ expect(
+ moreGroup().find('[data-test="traces-search-bar-menu-saved-views-apply-t1"]').exists(),
+ ).toBe(true);
});
- it("opens the saved-views dialog from List saved views", async () => {
- wrapper = mountSearchBar();
- await flushPromises();
- expect(dialog().props("open")).toBe(false);
-
- await moreGroup()
- .find('[data-test="traces-search-bar-menu-list-saved-views-btn"]')
- .trigger("click");
-
- expect(dialog().props("open")).toBe(true);
- expect(dialog().props("views")).toEqual([tracesView]);
- });
-
- it("opens the saved-views dialog from the pinned dropdown's Manage item", async () => {
- wrapper = mountSearchBar();
- await flushPromises();
-
- await toolbarGroup().find('[data-test="traces-saved-view-manage"]').trigger("click");
-
- expect(dialog().props("open")).toBe(true);
- });
-
- it("deletes from the dialog through the confirmation flow while unpinned", async () => {
+ it("deletes from the submenu through the confirmation flow while unpinned", async () => {
togglePin("savedViews");
wrapper = mountSearchBar();
await flushPromises();
@@ -2143,7 +2137,7 @@ describe("SearchBar", () => {
expect(mockSavedViewsDelete).toHaveBeenCalledWith(expect.any(String), "t1");
});
- it("applies and updates views from the dialog", async () => {
+ it("applies and updates views from the submenu", async () => {
mockGetViewDetail.mockResolvedValue({ data: { ...tracesView, data: expectedData() } });
wrapper = mountSearchBar();
await flushPromises();
@@ -2160,33 +2154,37 @@ describe("SearchBar", () => {
});
});
- it("refetches the saved views each time the dialog opens", async () => {
+ it("marks a view applied after a successful quick update, not after a failed one", async () => {
+ mockSavedViewsPut.mockRejectedValueOnce(new Error("boom"));
wrapper = mountSearchBar();
await flushPromises();
- const callsBefore = mockSavedViewsGet.mock.calls.length;
- mockSavedViewsGet.mockResolvedValue({
- data: { views: [tracesView, tracesView2, logsView] },
- });
- await wrapper
- .find('[data-test="traces-search-bar-menu-list-saved-views-btn"]')
- .trigger("click");
+ dialog().vm.$emit("update", tracesView);
await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBeNull();
- expect(mockSavedViewsGet.mock.calls.length).toBe(callsBefore + 1);
- expect(dialog().props("open")).toBe(true);
- expect(dialog().props("views")).toEqual([tracesView, tracesView2]);
-
- dialog().vm.$emit("update:open", false);
- await flushPromises();
- expect(dialog().props("open")).toBe(false);
- await wrapper
- .find('[data-test="traces-search-bar-menu-list-saved-views-btn"]')
- .trigger("click");
+ dialog().vm.$emit("update", tracesView);
await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBe("t1");
+ });
- expect(mockSavedViewsGet.mock.calls.length).toBe(callsBefore + 2);
- expect(dialog().props("open")).toBe(true);
+ it("refetches the list when the More submenu or the pinned dropdown opens", async () => {
+ wrapper = mountSearchBar();
+ await flushPromises();
+ mockSavedViewsGet.mockClear();
+
+ dialog().vm.$emit("open");
+ await flushPromises();
+ expect(mockSavedViewsGet).toHaveBeenCalledTimes(1);
+
+ (wrapper.vm as any).onSavedViewsDropdownOpenChange(true);
+ await flushPromises();
+ expect(mockSavedViewsGet).toHaveBeenCalledTimes(2);
+ expect((wrapper.vm as any).savedViewsDropdownOpen).toBe(true);
+
+ (wrapper.vm as any).onSavedViewsDropdownOpenChange(false);
+ await flushPromises();
+ expect(mockSavedViewsGet).toHaveBeenCalledTimes(2);
});
it("prunes favourites deleted elsewhere once the list loads, freeing the cap", async () => {
@@ -2219,7 +2217,6 @@ describe("SearchBar", () => {
wrapper = mountSearchBar();
await flushPromises();
- expect(dialog().props("loading")).toBe(false);
expect(dialog().props("views")).toEqual([]);
expect(dialog().props("favoriteIds")).toEqual(["t1", "gone"]);
expect(JSON.parse(localStorage.getItem("savedViews") || "{}")).toEqual({
@@ -2248,7 +2245,7 @@ describe("SearchBar", () => {
});
});
- it("passes the traces data-test prefix, views and favourites to the dialog", async () => {
+ it("passes the traces data-test prefix, views and favourites to the submenu", async () => {
localStorage.setItem(
"savedViews",
JSON.stringify({ t1: tracesView, l1: { ...logsView, org_id: "default" } }),
@@ -2256,44 +2253,18 @@ describe("SearchBar", () => {
wrapper = mountSearchBar();
await flushPromises();
- expect(dialog().props("dataTestPrefix")).toBe("traces-saved-views-dialog");
- expect(dialog().props("loading")).toBe(false);
+ expect(dialog().props("dataTestPrefix")).toBe("traces-search-bar-menu-saved-views");
expect(dialog().props("favoriteIds")).toEqual(["t1"]);
- expect(dialog().props("favoriteViews")).toEqual([tracesView]);
});
- it("update from the dialog closes it and asks for confirmation first", async () => {
- mockConfirm.mockResolvedValue(false);
- wrapper = mountSearchBar();
- await flushPromises();
- await wrapper
- .find('[data-test="traces-search-bar-menu-list-saved-views-btn"]')
- .trigger("click");
- expect(dialog().props("open")).toBe(true);
-
- dialog().vm.$emit("update", tracesView);
- await flushPromises();
-
- expect(dialog().props("open")).toBe(false);
- expect(mockConfirm).toHaveBeenCalledWith({
- title: "search.updateSavedView",
- message: "search.updateSavedViewConfirm",
- });
- expect(mockSavedViewsPut).not.toHaveBeenCalled();
- });
-
- it("delete from the dialog closes it, confirms, and drops the favourite", async () => {
+ it("delete from the submenu confirms, and drops the favourite", async () => {
localStorage.setItem("savedViews", JSON.stringify({ t1: tracesView }));
wrapper = mountSearchBar();
await flushPromises();
- await wrapper
- .find('[data-test="traces-search-bar-menu-list-saved-views-btn"]')
- .trigger("click");
dialog().vm.$emit("delete", tracesView);
await flushPromises();
- expect(dialog().props("open")).toBe(false);
expect(mockConfirm).toHaveBeenCalledWith({
title: "search.deleteSavedView",
messageKeypath: "search.deleteSavedViewConfirm",
@@ -2305,7 +2276,117 @@ describe("SearchBar", () => {
expect(JSON.parse(localStorage.getItem("savedViews") || "{}")).toEqual({});
});
- it("delete from the dialog does nothing when not confirmed", async () => {
+ it("closes the More menu and the pinned dropdown before deleting", async () => {
+ let release!: (value: boolean) => void;
+ mockConfirm.mockReturnValueOnce(new Promise((resolve) => (release = resolve)));
+ wrapper = mountSearchBar();
+ await flushPromises();
+ (wrapper.vm as any).moreMenuOpen = true;
+ (wrapper.vm as any).savedViewsDropdownOpen = true;
+
+ dialog().vm.$emit("delete", tracesView);
+ await flushPromises();
+
+ expect((wrapper.vm as any).moreMenuOpen).toBe(false);
+ expect((wrapper.vm as any).savedViewsDropdownOpen).toBe(false);
+ expect(mockSavedViewsDelete).not.toHaveBeenCalled();
+ release(false);
+ });
+
+ it("marks the applied view only after its detail loads", async () => {
+ mockGetViewDetail.mockRejectedValueOnce(new Error("boom"));
+ wrapper = mountSearchBar();
+ await flushPromises();
+
+ dialog().vm.$emit("apply", tracesView);
+ await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBeNull();
+
+ mockGetViewDetail.mockResolvedValueOnce({ data: { ...tracesView, data: expectedData() } });
+ dialog().vm.$emit("apply", tracesView);
+ await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBe("t1");
+ });
+
+ it("deleting the applied view clears the applied check", async () => {
+ mockGetViewDetail.mockResolvedValueOnce({ data: { ...tracesView, data: expectedData() } });
+ wrapper = mountSearchBar();
+ await flushPromises();
+ dialog().vm.$emit("apply", tracesView);
+ await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBe("t1");
+
+ dialog().vm.$emit("delete", tracesView);
+ await flushPromises();
+
+ expect((wrapper.vm as any).appliedSavedViewId).toBeNull();
+ });
+
+ describe("applied check follows the live search", () => {
+ const applyView = async () => {
+ mockGetViewDetail.mockResolvedValueOnce({
+ data: { ...tracesView, data: expectedData() },
+ });
+ wrapper = mountSearchBar();
+ await flushPromises();
+ dialog().vm.$emit("apply", tracesView);
+ await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBe("t1");
+ };
+
+ it("clears when the query text is edited", async () => {
+ await applyView();
+ searchObjInstance.data.editorValue = "span_status = 'OK'";
+ await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBeNull();
+ });
+
+ it("clears when the stream changes", async () => {
+ await applyView();
+ searchObjInstance.data.stream.selectedStream = { label: "other", value: "other" };
+ await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBeNull();
+ searchObjInstance.data.stream.selectedStream = { label: "default", value: "default" };
+ });
+
+ it("clears on Reset", async () => {
+ await applyView();
+ (wrapper.vm as any).resetFilters();
+ await flushPromises();
+ expect((wrapper.vm as any).appliedSavedViewId).toBeNull();
+ });
+ });
+
+ it("overwrite asks for confirmation naming the view and keeps focus on Cancel", async () => {
+ wrapper = mountSearchBar();
+ await flushPromises();
+
+ dialog().vm.$emit("update", tracesView);
+ await flushPromises();
+
+ expect(mockConfirm).toHaveBeenCalledWith(
+ expect.objectContaining({
+ messageKeypath: "search.overwriteSavedViewConfirm",
+ messageName: "checkout errors",
+ focusCancel: true,
+ }),
+ );
+ expect(mockSavedViewsPut).toHaveBeenCalledTimes(1);
+ });
+
+ it("a cancelled overwrite leaves the view untouched", async () => {
+ mockConfirm.mockResolvedValue(false);
+ wrapper = mountSearchBar();
+ await flushPromises();
+
+ dialog().vm.$emit("update", tracesView);
+ await flushPromises();
+
+ expect(mockSavedViewsPut).not.toHaveBeenCalled();
+ expect((wrapper.vm as any).appliedSavedViewId).toBeNull();
+ });
+
+ it("delete from the submenu does nothing when not confirmed", async () => {
mockConfirm.mockResolvedValue(false);
localStorage.setItem("savedViews", JSON.stringify({ t1: tracesView }));
wrapper = mountSearchBar();
@@ -2318,7 +2399,7 @@ describe("SearchBar", () => {
expect(dialog().props("favoriteIds")).toEqual(["t1"]);
});
- it("toggles a favourite from the dialog", async () => {
+ it("toggles a favourite from the submenu", async () => {
wrapper = mountSearchBar();
await flushPromises();
diff --git a/web/src/plugins/traces/SearchBar.vue b/web/src/plugins/traces/SearchBar.vue
index 7c09852a96..74ade215e7 100644
--- a/web/src/plugins/traces/SearchBar.vue
+++ b/web/src/plugins/traces/SearchBar.vue
@@ -135,7 +135,7 @@ along with this program. If not, see .
:open="savedViewsDropdownOpen"
side="bottom"
align="start"
- @update:open="savedViewsDropdownOpen = $event"
+ @update:open="onSavedViewsDropdownOpenChange"
>
.
-
-
-
-
-
- {{ t("search.createSavedView") }}
-
-
- {{ t("search.manageSavedViews") }}
-
+
.
-
+
.
-
-
+
.
-
-
-
-
- {{ t("search.listSavedViews") }}
-
-
-
-
-
- {{ t("search.createSavedView") }}
-
-
+
@@ -596,18 +574,6 @@ along with this program. If not, see .
data-test="traces-saved-view-name-input"
/>
-
@@ -637,7 +603,6 @@ import OButtonGroup from "@/lib/core/Button/OButtonGroup.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
-import ODropdownGroup from "@/lib/overlay/Dropdown/ODropdownGroup.vue";
import ODropdownSeparator from "@/lib/overlay/Dropdown/ODropdownSeparator.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
@@ -650,9 +615,8 @@ import { useLockedAffordance } from "@/composables/useLockedAffordance";
import useTraces from "@/composables/useTraces";
import { useSqlEditorDiagnostics } from "@/composables/useSqlEditorDiagnostics";
import SyntaxGuide from "./SyntaxGuide.vue";
-import TracesSavedViewsMenuItems from "./TracesSavedViewsMenuItems.vue";
-import SavedViewsListDialog from "@/components/savedViews/SavedViewsListDialog.vue";
-import { sortSavedViews } from "@/components/savedViews/savedViewsSort";
+import SavedViewsMenu from "@/components/savedViews/SavedViewsMenu.vue";
+import SavedViewsSubmenu from "@/components/savedViews/SavedViewsSubmenu.vue";
import { useFavoriteSavedViews } from "@/composables/useFavoriteSavedViews";
import { debounce } from "lodash-es";
@@ -710,7 +674,6 @@ export default defineComponent({
OIcon,
ODropdown,
ODropdownItem,
- ODropdownGroup,
ODropdownSeparator,
ODialog,
OInput,
@@ -719,11 +682,11 @@ export default defineComponent({
OTooltip,
CodeQueryEditor: defineAsyncComponent(() => import("@/components/CodeQueryEditor.vue")),
SyntaxGuide,
- TracesSavedViewsMenuItems,
- SavedViewsListDialog,
RecentSearchesButton,
RecentSearchesDrawer,
RecentSearchesFocusDropdown,
+ SavedViewsMenu,
+ SavedViewsSubmenu,
},
emits: [
"searchdata",
@@ -1143,6 +1106,7 @@ export default defineComponent({
};
const resetFilters = () => {
+ appliedSavedView.value = null;
searchObj.data.editorValue = "";
searchObj.data.advanceFiltersQuery = "";
Object.values(searchObj.data.stream.fieldValues).forEach((field) => {
@@ -1233,7 +1197,7 @@ export default defineComponent({
(savedViewsList.data.value ?? []).filter((view) => view.view_type === "traces"),
);
const savedViewsLoading = computed(() => savedViewsList.isLoading.value);
- const { favoriteIds, favoriteViews, toggleFavorite, removeFavorite, pruneFavorites } =
+ const { favoriteIds, toggleFavorite, removeFavorite, pruneFavorites } =
useFavoriteSavedViews("traces");
// Only a successful fetch is the complete list; pruning on error would drop every favourite.
watch(
@@ -1245,9 +1209,25 @@ export default defineComponent({
},
{ immediate: true },
);
- const sortedTracesSavedViews = computed(() =>
- sortSavedViews(tracesSavedViews.value, favoriteIds.value),
+ const appliedSavedView = ref<{ id: string; editorValue: string; streamValue: string } | null>(
+ null,
);
+ // Derived from the live state so any later edit, stream switch or reset drops the check.
+ const appliedSavedViewId = computed(() => {
+ const applied = appliedSavedView.value;
+ if (!applied) return null;
+ const matches =
+ searchObj.data.editorValue === applied.editorValue &&
+ searchObj.data.stream.selectedStream?.value === applied.streamValue;
+ return matches ? applied.id : null;
+ });
+ const markSavedViewApplied = (id: string, data: any) => {
+ appliedSavedView.value = {
+ id,
+ editorValue: data?.editorValue ?? "",
+ streamValue: data?.stream?.value ?? "",
+ };
+ };
const createSavedView = useMutation(() => createSavedViewMutation(orgId.value));
const updateSavedView = useMutation(() => updateSavedViewMutation(orgId.value));
const deleteSavedView = useMutation(() => deleteSavedViewMutation(orgId.value));
@@ -1282,12 +1262,6 @@ export default defineComponent({
saveViewDialogOpen.value = true;
};
- const savedViewsDialogOpen = ref(false);
- const openSavedViewsDialog = () => {
- savedViewsList.refetch();
- savedViewsDialogOpen.value = true;
- };
-
const saveAsNewView = async () => {
const viewName = newViewName.value.trim();
if (!viewName) {
@@ -1295,11 +1269,13 @@ export default defineComponent({
return;
}
try {
- await createSavedView.mutateAsync({
+ const viewData = buildSavedViewData();
+ const res = await createSavedView.mutateAsync({
view_name: viewName,
view_type: "traces",
- data: buildSavedViewData(),
+ data: viewData,
});
+ if (res?.data?.view_id) markSavedViewApplied(res.data.view_id, viewData);
saveViewDialogOpen.value = false;
toast({ message: t("search.viewCreatedSuccessfully"), variant: "success" });
} catch (err) {
@@ -1312,10 +1288,12 @@ export default defineComponent({
const updateSavedViewFromCurrent = async (view) => {
try {
+ const viewData = buildSavedViewData();
await updateSavedView.mutateAsync({
viewId: view.view_id,
- view: { view_name: view.view_name, data: buildSavedViewData() },
+ view: { view_name: view.view_name, data: viewData },
});
+ markSavedViewApplied(view.view_id, viewData);
toast({ message: t("search.viewUpdatedSuccessfully"), variant: "success" });
} catch (err) {
toast({
@@ -1326,22 +1304,29 @@ export default defineComponent({
};
const savedViewsDropdownOpen = ref(false);
- const quickUpdateSavedView = (view) => {
- savedViewsDropdownOpen.value = false;
- return updateSavedViewFromCurrent(view);
+ const moreMenuOpen = ref(false);
+ const refreshSavedViews = () => savedViewsList.refetch();
+ const onSavedViewsDropdownOpenChange = (open: boolean) => {
+ savedViewsDropdownOpen.value = open;
+ if (open) refreshSavedViews();
};
-
- const confirmUpdateSavedView = async (view) => {
- savedViewsDialogOpen.value = false;
+ const requestUpdateSavedView = async (view) => {
+ savedViewsDropdownOpen.value = false;
+ moreMenuOpen.value = false;
const confirmed = await confirm({
- title: t("search.updateSavedView"),
- message: t("search.updateSavedViewConfirm"),
+ title: t("search.overwriteSavedView"),
+ messageKeypath: "search.overwriteSavedViewConfirm",
+ messageName: view.view_name,
+ confirmLabel: t("search.overwriteSavedViewOk"),
+ focusCancel: true,
});
- if (confirmed) await updateSavedViewFromCurrent(view);
+ if (!confirmed) return;
+ await updateSavedViewFromCurrent(view);
};
const deleteTracesSavedView = async (view) => {
- savedViewsDialogOpen.value = false;
+ savedViewsDropdownOpen.value = false;
+ moreMenuOpen.value = false;
const confirmed = await confirm({
title: t("search.deleteSavedView"),
messageKeypath: "search.deleteSavedViewConfirm",
@@ -1352,6 +1337,7 @@ export default defineComponent({
try {
await deleteSavedView.mutateAsync(view.view_id);
removeFavorite(view.view_id);
+ if (appliedSavedView.value?.id === view.view_id) appliedSavedView.value = null;
toast({ message: t("search.viewDeletedSuccessfully"), variant: "success" });
} catch (err) {
toast({
@@ -1368,6 +1354,7 @@ export default defineComponent({
// Uncached on purpose, as in logs: applying must use the server's current copy.
const res = await savedViewsService.getViewDetail(orgId.value, view.view_id);
if (seq !== applySavedViewSeq) return;
+ markSavedViewApplied(view.view_id, res.data.data);
emit("apply-saved-view", res.data.data);
} catch (err) {
if (seq !== applySavedViewSeq) return;
@@ -1514,20 +1501,19 @@ export default defineComponent({
saveViewDialogOpen,
newViewName,
openSaveViewDialog,
- savedViewsDialogOpen,
- openSavedViewsDialog,
saveAsNewView,
updateSavedViewFromCurrent,
deleteTracesSavedView,
applySavedView,
savedViewsLoading,
favoriteIds,
- favoriteViews,
toggleFavorite,
- sortedTracesSavedViews,
+ appliedSavedViewId,
savedViewsDropdownOpen,
- quickUpdateSavedView,
- confirmUpdateSavedView,
+ onSavedViewsDropdownOpenChange,
+ refreshSavedViews,
+ moreMenuOpen,
+ requestUpdateSavedView,
serviceGraphAccess,
onSearchModeChange,
};
diff --git a/web/src/plugins/traces/TracesSavedViewsMenuItems.vue b/web/src/plugins/traces/TracesSavedViewsMenuItems.vue
deleted file mode 100644
index c9134f2ac3..0000000000
--- a/web/src/plugins/traces/TracesSavedViewsMenuItems.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
-
-
-
-
-
- {{ t("confirmDialog.loading") }}
-
-
-
-
-
-
- {{ view.view_name }}
-
-
-
-
-
-
- {{ t("search.savedViewsNotFound") }}
-
-