mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-03 16:10:21 +00:00
feat(saved-views): saved views submenu with favorites, edit and delete on Logs and Traces (#16077)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ashish Kolhe <35160958+oasisk@users.noreply.github.com>
This commit is contained in:
co-authored by
github-actions[bot]
Ashish Kolhe
parent
6a1ff919b5
commit
46043dfa9b
@@ -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",
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+17
-17
@@ -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();
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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: '<div v-if="open" v-bind="$attrs"><slot /></div>',
|
||||
};
|
||||
|
||||
// The real dropdown portals its items; render them inline so the < md menu is reachable.
|
||||
const ODropdownStub = {
|
||||
name: "ODropdown",
|
||||
template: '<div><slot name="trigger" /><slot /></div>',
|
||||
};
|
||||
|
||||
const ODropdownItemStub = {
|
||||
name: "ODropdownItem",
|
||||
props: ["iconLeft", "variant"],
|
||||
emits: ["select"],
|
||||
template: '<div v-bind="$attrs" @click="$emit(\'select\')"><slot /></div>',
|
||||
};
|
||||
|
||||
describe("SavedViewsListDialog", () => {
|
||||
let wrapper: VueWrapper<any> | undefined;
|
||||
|
||||
const mountDialog = (props: Record<string, unknown> = {}) =>
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,295 +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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18nTyped, raw } from "@/types/i18n";
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
import OTable from "@/lib/core/Table/OTable.vue";
|
||||
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
|
||||
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
|
||||
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
|
||||
import OTruncatedText from "@/lib/core/Typography/OTruncatedText.vue";
|
||||
|
||||
export interface SavedViewRow {
|
||||
view_id: string;
|
||||
view_name: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
type Pane = "all" | "favorites";
|
||||
|
||||
interface DialogTestIds {
|
||||
root: string;
|
||||
list: string;
|
||||
search: string;
|
||||
loading: string;
|
||||
empty: string;
|
||||
table: (pane: Pane) => string;
|
||||
apply: (pane: Pane, row: SavedViewRow) => string;
|
||||
favorite: (pane: Pane, row: SavedViewRow) => string;
|
||||
update: (pane: Pane, row: SavedViewRow) => string;
|
||||
remove: (pane: Pane, row: SavedViewRow) => string;
|
||||
more: (pane: Pane, row: SavedViewRow) => string;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
views: SavedViewRow[];
|
||||
favoriteIds: string[];
|
||||
favoriteViews: SavedViewRow[];
|
||||
loading?: boolean;
|
||||
dataTestPrefix: string;
|
||||
}>(),
|
||||
{ loading: false },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:open", value: boolean): void;
|
||||
(e: "apply", row: SavedViewRow): void;
|
||||
(e: "update", row: SavedViewRow): void;
|
||||
(e: "delete", row: SavedViewRow): void;
|
||||
(e: "toggle-favorite", row: SavedViewRow, isFavorite: boolean): void;
|
||||
}>();
|
||||
|
||||
const search = defineModel<string>("search", { default: "" });
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) search.value = "";
|
||||
},
|
||||
);
|
||||
|
||||
const paneTestId = (pane: Pane) =>
|
||||
pane === "all" ? props.dataTestPrefix : `${props.dataTestPrefix}-favorites`;
|
||||
|
||||
const ids = computed<DialogTestIds>(() => ({
|
||||
root: props.dataTestPrefix,
|
||||
list: `${props.dataTestPrefix}-list`,
|
||||
search: `${props.dataTestPrefix}-search`,
|
||||
loading: `${props.dataTestPrefix}-loading`,
|
||||
empty: `${props.dataTestPrefix}-empty`,
|
||||
table: (pane) => `${paneTestId(pane)}-table`,
|
||||
apply: (pane, row) => `${paneTestId(pane)}-apply-${row.view_id}`,
|
||||
favorite: (pane, row) => `${paneTestId(pane)}-favorite-${row.view_id}`,
|
||||
update: (pane, row) => `${paneTestId(pane)}-update-${row.view_id}`,
|
||||
remove: (pane, row) => `${paneTestId(pane)}-delete-${row.view_id}`,
|
||||
more: (pane, row) => `${paneTestId(pane)}-more-actions-${row.view_id}`,
|
||||
}));
|
||||
|
||||
// `meta.isAction` gives the right-aligned action cell; the column-level `isAction` would also pin it with a sticky shadow.
|
||||
const columns = [
|
||||
{
|
||||
id: "view_name",
|
||||
header: raw(""),
|
||||
accessorKey: "view_name",
|
||||
sortable: false,
|
||||
meta: { align: "left" as const },
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: raw(""),
|
||||
sortable: false,
|
||||
size: 30,
|
||||
meta: { align: "right" as const, actionCount: 3, isAction: true },
|
||||
},
|
||||
];
|
||||
|
||||
const HOVER_REVEAL =
|
||||
"opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100 max-md:opacity-100";
|
||||
|
||||
const filteredViews = computed(() => {
|
||||
const term = search.value.trim().toLowerCase();
|
||||
if (!term) return props.views;
|
||||
return props.views.filter((view) => String(view.view_name).toLowerCase().includes(term));
|
||||
});
|
||||
|
||||
const panes = computed(() => {
|
||||
const hasFavorites = props.favoriteViews.length > 0;
|
||||
const all = {
|
||||
key: "all" as Pane,
|
||||
rows: filteredViews.value,
|
||||
class: hasFavorites ? "border-card-glass-border w-3/5 border-e" : "w-full",
|
||||
};
|
||||
if (!hasFavorites) return [all];
|
||||
return [all, { key: "favorites" as Pane, rows: props.favoriteViews, class: "w-2/5 ps-3" }];
|
||||
});
|
||||
|
||||
const isFavorite = (pane: Pane, row: SavedViewRow) =>
|
||||
pane === "favorites" || props.favoriteIds.includes(row.view_id);
|
||||
|
||||
const applyView = (row: SavedViewRow) => {
|
||||
emit("apply", row);
|
||||
emit("update:open", false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ODialog
|
||||
:open="props.open"
|
||||
size="lg"
|
||||
:title="t('search.savedViewsLabel')"
|
||||
:data-test="ids.root"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<div :data-test="ids.list" class="flex">
|
||||
<div
|
||||
v-for="pane in panes"
|
||||
:key="pane.key"
|
||||
class="flex max-h-120 min-h-70 flex-col"
|
||||
:class="pane.class"
|
||||
>
|
||||
<OTable
|
||||
:data-test="ids.table(pane.key)"
|
||||
:data="pane.rows"
|
||||
:columns="columns"
|
||||
row-key="view_id"
|
||||
compact
|
||||
:show-header="false"
|
||||
:show-global-filter="false"
|
||||
:pagination-bordered="false"
|
||||
:pagination="pane.key === 'all' ? 'client' : 'none'"
|
||||
:page-size="10"
|
||||
:page-size-options="[10, 20, 50]"
|
||||
class="min-h-0 flex-1"
|
||||
>
|
||||
<template #top>
|
||||
<template v-if="pane.key === 'all'">
|
||||
<div class="box-border w-full min-w-0 p-2">
|
||||
<OSearchInput
|
||||
v-model="search"
|
||||
:data-test="ids.search"
|
||||
clearable
|
||||
:debounce="300"
|
||||
class="w-full"
|
||||
:placeholder="t('search.searchSavedView')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.loading"
|
||||
:data-test="ids.loading"
|
||||
class="flex w-full items-center gap-2 p-2 text-sm font-medium"
|
||||
>
|
||||
<OSpinner size="xs" />
|
||||
{{ t("confirmDialog.loading") }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-text-secondary p-2 text-xs leading-6 font-bold tracking-wide">
|
||||
{{ t("search.favoriteViews") }}
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template #cell-view_name="{ row, value }">
|
||||
<OTruncatedText
|
||||
as="div"
|
||||
class="w-full cursor-pointer text-sm"
|
||||
:data-test="ids.apply(pane.key, row)"
|
||||
:data-test-view-name="row.view_name"
|
||||
@click.stop="applyView(row)"
|
||||
>
|
||||
{{ value }}
|
||||
</OTruncatedText>
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<OButton
|
||||
:title="t('common.favourite')"
|
||||
variant="ghost-neutral"
|
||||
size="icon-sm"
|
||||
:class="HOVER_REVEAL"
|
||||
:data-test="ids.favorite(pane.key, row)"
|
||||
@click.stop="emit('toggle-favorite', row, isFavorite(pane.key, row))"
|
||||
>
|
||||
<OIcon
|
||||
:name="isFavorite(pane.key, row) ? 'star' : 'star-outline'"
|
||||
size="xs"
|
||||
:class="isFavorite(pane.key, row) ? 'text-favorite' : ''"
|
||||
/>
|
||||
</OButton>
|
||||
<OButton
|
||||
:title="t('common.edit')"
|
||||
variant="ghost-neutral"
|
||||
size="icon-sm"
|
||||
:class="[HOVER_REVEAL, 'max-md:hidden']"
|
||||
:data-test="ids.update(pane.key, row)"
|
||||
@click.stop="emit('update', row)"
|
||||
>
|
||||
<OIcon name="edit" size="xs" />
|
||||
</OButton>
|
||||
<OButton
|
||||
:title="t('common.delete')"
|
||||
variant="ghost-neutral"
|
||||
size="icon-sm"
|
||||
:class="[HOVER_REVEAL, 'max-md:hidden']"
|
||||
:data-test="ids.remove(pane.key, row)"
|
||||
@click.stop="emit('delete', row)"
|
||||
>
|
||||
<OIcon name="delete" size="xs" />
|
||||
</OButton>
|
||||
<ODropdown side="bottom" align="end">
|
||||
<template #trigger>
|
||||
<OButton
|
||||
:aria-label="t('common.moreActions')"
|
||||
icon-left="more-vert"
|
||||
variant="ghost"
|
||||
size="icon-xs-sq"
|
||||
class="md:hidden"
|
||||
:data-test="ids.more(pane.key, row)"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<ODropdownItem
|
||||
icon-left="edit"
|
||||
class="md:hidden"
|
||||
:data-test="`${ids.update(pane.key, row)}-menu`"
|
||||
@select="emit('update', row)"
|
||||
>
|
||||
<span>{{ t("common.edit") }}</span>
|
||||
</ODropdownItem>
|
||||
<ODropdownItem
|
||||
icon-left="delete"
|
||||
variant="destructive"
|
||||
class="md:hidden"
|
||||
:data-test="`${ids.remove(pane.key, row)}-menu`"
|
||||
@select="emit('delete', row)"
|
||||
>
|
||||
<span>{{ t("common.delete") }}</span>
|
||||
</ODropdownItem>
|
||||
</ODropdown>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<OEmptyState
|
||||
v-if="!props.loading"
|
||||
size="inline"
|
||||
:title="t('search.savedViewsNotFound')"
|
||||
:data-test="ids.empty"
|
||||
/>
|
||||
</template>
|
||||
</OTable>
|
||||
</div>
|
||||
</div>
|
||||
</ODialog>
|
||||
</template>
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<typeof mount> | undefined;
|
||||
afterEach(() => {
|
||||
document.documentElement.dir = "ltr";
|
||||
wrapper?.unmount();
|
||||
wrapper = undefined;
|
||||
});
|
||||
|
||||
const el = (id: string) => document.querySelector<HTMLElement>(`[data-test="${id}"]`);
|
||||
|
||||
const mountMenu = async (
|
||||
props: Record<string, unknown> = {},
|
||||
listeners: Record<string, unknown> = {},
|
||||
) => {
|
||||
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<string[]>(["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<string, unknown> = {},
|
||||
slots: Record<string, () => 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"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
<!-- 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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
|
||||
import ODropdownGroup from "@/lib/overlay/Dropdown/ODropdownGroup.vue";
|
||||
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
|
||||
import ODropdownSeparator from "@/lib/overlay/Dropdown/ODropdownSeparator.vue";
|
||||
import SavedViewsMenuRow from "./SavedViewsMenuRow.vue";
|
||||
import { sortSavedViews } from "./savedViewsSort";
|
||||
import type { SavedViewRow } from "./savedViews.types";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
views: SavedViewRow[];
|
||||
favoriteIds: string[];
|
||||
appliedViewId?: string | null;
|
||||
loading?: boolean;
|
||||
saveReason?: I18nText | null;
|
||||
saveNote?: I18nText | null;
|
||||
createShortcutId?: string;
|
||||
dataTestPrefix: string;
|
||||
createTestId?: string;
|
||||
}>(),
|
||||
{
|
||||
appliedViewId: null,
|
||||
loading: false,
|
||||
saveReason: null,
|
||||
saveNote: null,
|
||||
createShortcutId: undefined,
|
||||
createTestId: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "apply", view: SavedViewRow): void;
|
||||
(e: "update", view: SavedViewRow): void;
|
||||
(e: "delete", view: SavedViewRow): void;
|
||||
(e: "toggle-favorite", view: SavedViewRow, isFavorite: boolean): void;
|
||||
(e: "create"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
const search = ref("");
|
||||
|
||||
const isFavorite = (view: SavedViewRow) => props.favoriteIds.includes(view.view_id);
|
||||
|
||||
const matching = computed(() => {
|
||||
const term = search.value.trim().toLowerCase();
|
||||
const sorted = sortSavedViews(props.views, props.favoriteIds);
|
||||
if (!term) return sorted;
|
||||
return sorted.filter((view) => String(view.view_name).toLowerCase().includes(term));
|
||||
});
|
||||
|
||||
const favorites = computed(() => matching.value.filter(isFavorite));
|
||||
const others = computed(() => matching.value.filter((view) => !isFavorite(view)));
|
||||
|
||||
const focusSearch = () => root.value?.querySelector<HTMLInputElement>("input")?.focus();
|
||||
const focusFirstRow = () =>
|
||||
root.value?.querySelector<HTMLElement>('[role="menuitem"]:not([data-disabled])')?.focus();
|
||||
|
||||
// Keys typed in the search box must not reach the menu, whose typeahead would pull focus onto a row.
|
||||
const onSearchKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") return;
|
||||
event.stopPropagation();
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
focusFirstRow();
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
root.value?.querySelector<HTMLElement>('[role="menuitem"][data-test*="-apply-"]')?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const onToggleFavorite = async (view: SavedViewRow, favorite: boolean) => {
|
||||
emit("toggle-favorite", view, favorite);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
root.value
|
||||
?.querySelector<HTMLElement>(`[data-test="${props.dataTestPrefix}-favorite-${view.view_id}"]`)
|
||||
?.focus();
|
||||
};
|
||||
|
||||
onMounted(() => nextTick(focusSearch));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
class="flex w-[min(20rem,calc(100vw-2rem))] flex-col"
|
||||
:data-test="`${props.dataTestPrefix}-content`"
|
||||
>
|
||||
<div class="px-1 pt-0.5 pb-0.5" @keydown="onSearchKeydown">
|
||||
<OSearchInput
|
||||
v-model="search"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
props.loading
|
||||
? t('search.savedViewsSearchPlaceholder')
|
||||
: t('search.savedViewsSearchCount', { count: props.views.length }, props.views.length)
|
||||
"
|
||||
:data-test="`${props.dataTestPrefix}-search`"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.loading"
|
||||
class="text-text-secondary flex items-center gap-2 px-3 py-1.5 text-sm"
|
||||
:data-test="`${props.dataTestPrefix}-loading`"
|
||||
>
|
||||
<OSpinner size="xs" />
|
||||
{{ t("confirmDialog.loading") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="matching.length"
|
||||
class="max-h-72 overflow-y-auto overscroll-contain"
|
||||
:data-test="`${props.dataTestPrefix}-list`"
|
||||
>
|
||||
<ODropdownGroup
|
||||
v-if="favorites.length"
|
||||
:label="t('search.savedViewsFavoritesGroup', { count: favorites.length })"
|
||||
:data-test="`${props.dataTestPrefix}-favorites-group`"
|
||||
>
|
||||
<SavedViewsMenuRow
|
||||
v-for="view in favorites"
|
||||
:key="view.view_id"
|
||||
:view="view"
|
||||
:favorite="true"
|
||||
:applied="view.view_id === props.appliedViewId"
|
||||
:update-reason="props.saveReason"
|
||||
:data-test-prefix="props.dataTestPrefix"
|
||||
@apply="emit('apply', view)"
|
||||
@update="emit('update', view)"
|
||||
@delete="emit('delete', view)"
|
||||
@toggle-favorite="onToggleFavorite(view, true)"
|
||||
/>
|
||||
</ODropdownGroup>
|
||||
<ODropdownSeparator v-if="favorites.length && others.length" class="my-0.5!" />
|
||||
<ODropdownGroup
|
||||
v-if="others.length"
|
||||
:label="t('search.savedViewsAllGroup')"
|
||||
:data-test="`${props.dataTestPrefix}-all-group`"
|
||||
>
|
||||
<SavedViewsMenuRow
|
||||
v-for="view in others"
|
||||
:key="view.view_id"
|
||||
:view="view"
|
||||
:favorite="false"
|
||||
:applied="view.view_id === props.appliedViewId"
|
||||
:update-reason="props.saveReason"
|
||||
:data-test-prefix="props.dataTestPrefix"
|
||||
@apply="emit('apply', view)"
|
||||
@update="emit('update', view)"
|
||||
@delete="emit('delete', view)"
|
||||
@toggle-favorite="onToggleFavorite(view, false)"
|
||||
/>
|
||||
</ODropdownGroup>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-text-secondary px-3 py-1.5 text-sm"
|
||||
:data-test="`${props.dataTestPrefix}-empty`"
|
||||
>
|
||||
{{ props.views.length ? t("search.savedViewsNoMatch") : t("search.savedViewsEmpty") }}
|
||||
</div>
|
||||
<ODropdownSeparator class="my-0.5!" />
|
||||
<ODropdownItem
|
||||
class="py-1!"
|
||||
:data-test="props.createTestId ?? `${props.dataTestPrefix}-create`"
|
||||
:disabled="!!props.saveReason"
|
||||
focusable-unavailable
|
||||
:description="props.saveReason || props.saveNote"
|
||||
:shortcut-id="props.createShortcutId"
|
||||
@select="emit('create')"
|
||||
>
|
||||
<template #icon-left><OIcon name="add" size="sm" /></template>
|
||||
{{ t("search.createSavedView") }}
|
||||
</ODropdownItem>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!-- 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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OTruncatedText from "@/lib/core/Typography/OTruncatedText.vue";
|
||||
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
import { horizontalStep } from "./savedViewsKeys";
|
||||
import type { SavedViewRow } from "./savedViews.types";
|
||||
|
||||
const props = defineProps<{
|
||||
view: SavedViewRow;
|
||||
favorite: boolean;
|
||||
applied: boolean;
|
||||
dataTestPrefix: string;
|
||||
updateReason?: I18nText | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "apply"): void;
|
||||
(e: "update"): void;
|
||||
(e: "delete"): void;
|
||||
(e: "toggle-favorite"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
const viewId = props.view.view_id;
|
||||
|
||||
// Arrow keys walk the row's inline actions, so star, edit and delete stay reachable without a mouse.
|
||||
const onRowKeydown = (event: KeyboardEvent) => {
|
||||
const step = horizontalStep(event.key);
|
||||
if (!step) return;
|
||||
const row = event.currentTarget as HTMLElement;
|
||||
const buttons = Array.from(row.querySelectorAll<HTMLElement>("button:not([disabled])"));
|
||||
const index = buttons.indexOf(document.activeElement as HTMLElement);
|
||||
if (step === "back" && index < 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (step === "back" && index === 0) row.focus();
|
||||
else buttons[index + (step === "forward" ? 1 : -1)]?.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ODropdownItem
|
||||
:text-value="props.view.view_name"
|
||||
:aria-current="props.applied ? 'true' : undefined"
|
||||
:class="['py-0.5!', props.applied ? 'bg-select-item-selected-bg' : '']"
|
||||
:data-test="`${props.dataTestPrefix}-apply-${viewId}`"
|
||||
@select="emit('apply')"
|
||||
@keydown="onRowKeydown"
|
||||
>
|
||||
<template #icon-left>
|
||||
<OIcon name="saved-search" size="sm" class="text-text-secondary" />
|
||||
</template>
|
||||
<OTruncatedText class="min-w-0 flex-1">{{ props.view.view_name }}</OTruncatedText>
|
||||
<template #icon-right>
|
||||
<span class="ms-auto flex shrink-0 items-center gap-0.5">
|
||||
<OIcon
|
||||
v-if="props.applied"
|
||||
name="check"
|
||||
size="md"
|
||||
class="text-accent"
|
||||
role="img"
|
||||
:aria-label="t('search.savedViewApplied')"
|
||||
:data-test="`${props.dataTestPrefix}-applied-${viewId}`"
|
||||
/>
|
||||
<OButton
|
||||
variant="ghost-neutral"
|
||||
size="icon-xs-sq"
|
||||
:aria-pressed="props.favorite"
|
||||
:aria-label="t('search.favoriteViewNamed', { name: raw(props.view.view_name) })"
|
||||
:data-test="`${props.dataTestPrefix}-favorite-${viewId}`"
|
||||
@click.stop.prevent="emit('toggle-favorite')"
|
||||
@keydown.enter.stop.prevent="emit('toggle-favorite')"
|
||||
@keydown.space.stop
|
||||
>
|
||||
<OIcon
|
||||
:name="props.favorite ? 'star' : 'star-outline'"
|
||||
size="sm"
|
||||
:class="props.favorite ? 'text-favorite' : ''"
|
||||
/>
|
||||
<OTooltip :content="t('search.favoriteViewNamed', { name: raw(props.view.view_name) })" />
|
||||
</OButton>
|
||||
<OButton
|
||||
variant="ghost-neutral"
|
||||
size="icon-xs-sq"
|
||||
icon-left="edit"
|
||||
:aria-label="t('search.overwriteSavedViewNamed', { name: raw(props.view.view_name) })"
|
||||
:disabled="!!props.updateReason"
|
||||
focusable-unavailable
|
||||
:data-test="`${props.dataTestPrefix}-update-${viewId}`"
|
||||
@click.stop.prevent="emit('update')"
|
||||
@keydown.enter.stop.prevent="emit('update')"
|
||||
@keydown.space.stop
|
||||
>
|
||||
<OTooltip
|
||||
:content="t('search.overwriteSavedViewNamed', { name: raw(props.view.view_name) })"
|
||||
/>
|
||||
<template #unavailable-reason>{{ props.updateReason }}</template>
|
||||
</OButton>
|
||||
<OButton
|
||||
variant="ghost-neutral"
|
||||
size="icon-xs-sq"
|
||||
icon-left="delete"
|
||||
:aria-label="t('search.deleteViewNamed', { name: raw(props.view.view_name) })"
|
||||
:data-test="`${props.dataTestPrefix}-delete-${viewId}`"
|
||||
@click.stop.prevent="emit('delete')"
|
||||
@keydown.enter.stop.prevent="emit('delete')"
|
||||
@keydown.space.stop
|
||||
>
|
||||
<OTooltip :content="t('search.deleteViewNamed', { name: raw(props.view.view_name) })" />
|
||||
</OButton>
|
||||
</span>
|
||||
</template>
|
||||
</ODropdownItem>
|
||||
</template>
|
||||
@@ -0,0 +1,121 @@
|
||||
<!-- 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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots } from "vue";
|
||||
import { raw, useI18nTyped } from "@/types/i18n";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import ODropdownSub from "@/lib/overlay/Dropdown/ODropdownSub.vue";
|
||||
import SavedViewsMenu from "./SavedViewsMenu.vue";
|
||||
import { horizontalStep } from "./savedViewsKeys";
|
||||
import type { SavedViewRow } from "./savedViews.types";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps<{
|
||||
views: SavedViewRow[];
|
||||
favoriteIds: string[];
|
||||
loading?: boolean;
|
||||
dataTestPrefix: string;
|
||||
triggerTestId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ (e: "open"): void }>();
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
const slots = useSlots();
|
||||
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
const actions = ref<HTMLElement | null>(null);
|
||||
|
||||
// The pin sits beside the trigger (a <button>) so no button nests in another; arrows hop between them.
|
||||
const onRootKeydown = (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const step = horizontalStep(event.key);
|
||||
if (actions.value?.contains(target)) {
|
||||
// Escape must still reach the menu, or focus on the pin traps the keyboard.
|
||||
if (event.key !== "Escape") event.stopPropagation();
|
||||
if (step !== "forward") return;
|
||||
event.preventDefault();
|
||||
root.value?.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
|
||||
} else if (step === "back" && slots.actions && target.getAttribute("role") === "menuitem") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
actions.value?.querySelector<HTMLElement>("button")?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const favoriteCount = computed(
|
||||
() => props.views.filter((view) => props.favoriteIds.includes(view.view_id)).length,
|
||||
);
|
||||
|
||||
const summary = computed(() => {
|
||||
if (props.loading && !props.views.length) return raw("");
|
||||
if (!props.views.length) return t("search.savedViewsEmpty");
|
||||
const views = t(
|
||||
"search.savedViewsSummaryViews",
|
||||
{ count: props.views.length },
|
||||
props.views.length,
|
||||
);
|
||||
if (!favoriteCount.value) return views;
|
||||
const favorites = t(
|
||||
"search.savedViewsSummaryFavorites",
|
||||
{ count: favoriteCount.value },
|
||||
favoriteCount.value,
|
||||
);
|
||||
return t("search.savedViewsSummary", { views, favorites });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="root" class="relative" @keydown="onRootKeydown">
|
||||
<ODropdownSub
|
||||
:data-test="props.triggerTestId"
|
||||
:text-value="t('search.savedViewsRowLabel')"
|
||||
:description="summary"
|
||||
@update:open="(open: boolean) => open && emit('open')"
|
||||
>
|
||||
<template #icon-left>
|
||||
<span
|
||||
class="rounded-default bg-section-header-bg text-text-secondary inline-flex h-7 w-7 shrink-0 items-center justify-center"
|
||||
>
|
||||
<OIcon name="saved-search" size="sm" />
|
||||
</span>
|
||||
</template>
|
||||
<template #trigger>{{ t("search.savedViewsRowLabel") }}</template>
|
||||
<template #icon-right>
|
||||
<span class="ms-auto flex items-center gap-1">
|
||||
<span v-if="slots.actions" class="w-7 shrink-0" aria-hidden="true" />
|
||||
<OIcon name="chevron-right" size="sm" />
|
||||
</span>
|
||||
</template>
|
||||
<SavedViewsMenu
|
||||
v-bind="$attrs"
|
||||
:views="props.views"
|
||||
:favorite-ids="props.favoriteIds"
|
||||
:loading="props.loading"
|
||||
:data-test-prefix="props.dataTestPrefix"
|
||||
/>
|
||||
</ODropdownSub>
|
||||
<span
|
||||
v-if="slots.actions"
|
||||
ref="actions"
|
||||
class="absolute end-9 top-1/2 flex -translate-y-1/2 items-center"
|
||||
>
|
||||
<slot name="actions" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2026 OpenObserve Inc.
|
||||
|
||||
export interface SavedViewRow {
|
||||
view_id: string;
|
||||
view_name: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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<T extends SavedViewLike>(
|
||||
views: T[] | null | undefined,
|
||||
favoriteIds: string[],
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,15 +44,12 @@ const SCOPES: Record<ToolbarPinScope, ToolbarPinScopeConfig> = {
|
||||
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: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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": "لم يتم العثور على حقول للتدفق المختار",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "選択したストリームのフィールドが見つかりません",
|
||||
|
||||
@@ -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": "선택한 스트림에 대한 필드를 찾을 수 없습니다.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Для выбранного потока поля не найдены",
|
||||
|
||||
@@ -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ı",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "未找到所选数据流的字段",
|
||||
|
||||
@@ -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": "所選串流找不到任何欄位",
|
||||
|
||||
@@ -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<HTMLElement>('[data-test="logs-search-bar-menu-list-saved-views-btn"]')!
|
||||
.click();
|
||||
await flushPromises();
|
||||
}
|
||||
for (const testId of ids) {
|
||||
const element = document.querySelector<HTMLElement>(`[data-test="${testId}"]`)!;
|
||||
expect(element).not.toBeNull();
|
||||
|
||||
@@ -45,6 +45,10 @@ describe("SearchBar — Run button ignores natural-language detection (#15049)",
|
||||
name: "ODropdownItem",
|
||||
template: '<div class="o-dropdown-item-stub" v-bind="$attrs"><slot /></div>',
|
||||
},
|
||||
ODropdownSub: {
|
||||
name: "ODropdownSub",
|
||||
template: '<div class="o-dropdown-sub-stub"><slot name="trigger" /><slot /></div>',
|
||||
},
|
||||
ODropdownSeparator: { name: "ODropdownSeparator", template: "<div />" },
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ describe("SearchBar — Refresh Cache & Run Query dropdown visibility (#14488)",
|
||||
'<div class="o-dropdown-item-stub" v-bind="$attrs" @click="$emit(\'select\')"><slot name="icon-left" /><slot /></div>',
|
||||
emits: ["select"],
|
||||
},
|
||||
ODropdownSub: {
|
||||
name: "ODropdownSub",
|
||||
template: '<div class="o-dropdown-sub-stub"><slot name="trigger" /><slot /></div>',
|
||||
},
|
||||
ODropdownSeparator: {
|
||||
name: "ODropdownSeparator",
|
||||
template: '<div class="o-dropdown-separator-stub" />',
|
||||
|
||||
@@ -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<any> | 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<HTMLElement>('[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<string, unknown> = {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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();
|
||||
|
||||
+106
-174
@@ -267,74 +267,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<OTooltip :content="t('search.listSavedViews')" :side-offset="2" />
|
||||
</OButton>
|
||||
</template>
|
||||
<ODropdownGroup :label="t('search.savedViewsLabel')">
|
||||
<div
|
||||
v-if="searchObj.loadingSavedView"
|
||||
class="text-text-secondary flex items-center gap-2 px-3 py-1.5 text-sm"
|
||||
>
|
||||
<OSpinner size="xs" />
|
||||
{{ t("confirmDialog.loading") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="sortedSavedViews.length"
|
||||
class="max-h-72 overflow-y-auto overscroll-contain"
|
||||
data-test="logs-search-bar-saved-views-menu-list"
|
||||
>
|
||||
<ODropdownItem
|
||||
v-for="view in sortedSavedViews"
|
||||
:key="view.view_id"
|
||||
:data-test="`logs-search-bar-saved-views-menu-apply-${view.view_name}`"
|
||||
@select="applySavedView(view)"
|
||||
>
|
||||
<template #icon-left>
|
||||
<OIcon
|
||||
:name="favoriteViews.includes(view.view_id) ? 'star' : 'saved-search'"
|
||||
size="sm"
|
||||
:class="favoriteViews.includes(view.view_id) ? 'text-favorite' : ''"
|
||||
/>
|
||||
</template>
|
||||
<OTruncatedText class="max-w-56">{{ view.view_name }}</OTruncatedText>
|
||||
<template #icon-right>
|
||||
<OButton
|
||||
variant="ghost"
|
||||
size="icon-xs-sq"
|
||||
icon-left="edit"
|
||||
class="ms-auto"
|
||||
:aria-label="t('search.updateSavedViewWithCurrent')"
|
||||
:disabled="!!saveViewReason"
|
||||
focusable-unavailable
|
||||
:data-test="`logs-search-bar-saved-views-menu-update-${view.view_name}`"
|
||||
@click.stop.prevent="quickUpdateSavedView(view)"
|
||||
@keydown.enter.stop.prevent="quickUpdateSavedView(view)"
|
||||
@keydown.space.stop
|
||||
>
|
||||
<template #unavailable-reason>{{ saveViewReason }}</template>
|
||||
</OButton>
|
||||
</template>
|
||||
</ODropdownItem>
|
||||
</div>
|
||||
<ODropdownItem v-else disabled>
|
||||
{{ t("search.savedViewsNotFound") }}
|
||||
</ODropdownItem>
|
||||
</ODropdownGroup>
|
||||
<ODropdownSeparator />
|
||||
<ODropdownItem
|
||||
icon-left="save"
|
||||
data-test="logs-search-bar-saved-views-menu-create"
|
||||
:disabled="!!saveViewReason"
|
||||
focusable-unavailable
|
||||
:description="saveViewReason || saveViewNote"
|
||||
@select="fnSavedView"
|
||||
>
|
||||
{{ t("search.createSavedView") }}
|
||||
</ODropdownItem>
|
||||
<ODropdownItem
|
||||
icon-left="settings"
|
||||
data-test="logs-search-bar-saved-views-menu-manage"
|
||||
@select="openSavedViewsList"
|
||||
>
|
||||
{{ t("search.manageSavedViews") }}
|
||||
</ODropdownItem>
|
||||
<SavedViewsMenu
|
||||
:views="searchObj.data.savedViews"
|
||||
:favorite-ids="favoriteViews"
|
||||
:applied-view-id="appliedSavedViewId"
|
||||
:loading="!!searchObj.loadingSavedView"
|
||||
:save-reason="saveViewReason"
|
||||
:save-note="saveViewNote"
|
||||
create-shortcut-id="logsSaveView"
|
||||
data-test-prefix="logs-search-bar-saved-views-menu"
|
||||
@apply="applySavedView"
|
||||
@update="requestUpdateSavedView"
|
||||
@toggle-favorite="handleFavoriteSavedView"
|
||||
@create="fnSavedView"
|
||||
@delete="handleDeleteSavedView"
|
||||
/>
|
||||
</ODropdown>
|
||||
<OSeparator vertical />
|
||||
<OButton
|
||||
@@ -363,7 +310,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/>
|
||||
</div>
|
||||
<!-- this is the button group responsible for showing all the utilities -->
|
||||
<ODropdown class="flex-shrink-0" side="bottom" align="start">
|
||||
<ODropdown
|
||||
class="flex-shrink-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
:open="utilitiesMenuOpen"
|
||||
@update:open="onUtilitiesMenuOpenChange"
|
||||
>
|
||||
<template #trigger>
|
||||
<OButton
|
||||
data-test="logs-search-bar-utilities-menu-btn"
|
||||
@@ -554,8 +507,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<ODropdownSeparator />
|
||||
|
||||
<!-- SAVED VIEWS -->
|
||||
<ODropdownGroup :label="t('search.menuGroupSavedViews')">
|
||||
<template #label-action>
|
||||
<SavedViewsSubmenu
|
||||
:views="searchObj.data.savedViews"
|
||||
:favorite-ids="favoriteViews"
|
||||
:applied-view-id="appliedSavedViewId"
|
||||
:loading="!!searchObj.loadingSavedView"
|
||||
:save-reason="saveViewReason"
|
||||
:save-note="saveViewNote"
|
||||
create-shortcut-id="logsSaveView"
|
||||
data-test-prefix="logs-search-bar-menu-saved-views"
|
||||
trigger-test-id="logs-search-bar-menu-list-saved-views-btn"
|
||||
create-test-id="logs-search-bar-menu-create-saved-view-btn"
|
||||
@apply="applySavedView"
|
||||
@update="requestUpdateSavedView"
|
||||
@toggle-favorite="handleFavoriteSavedView"
|
||||
@create="fnSavedView"
|
||||
@delete="handleDeleteSavedView"
|
||||
>
|
||||
<template #actions>
|
||||
<OButton
|
||||
data-test="logs-search-bar-menu-pin-saved-views-btn"
|
||||
variant="ghost-neutral"
|
||||
@@ -568,40 +537,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<OIcon :name="isPinned('savedViews') ? 'keep' : 'keep-outline'" size="sm" />
|
||||
</OButton>
|
||||
</template>
|
||||
<ODropdownItem
|
||||
data-test="logs-search-bar-menu-list-saved-views-btn"
|
||||
@select="openSavedViewsList"
|
||||
>
|
||||
<template #icon-left>
|
||||
<span
|
||||
class="rounded-default bg-section-header-bg text-text-secondary inline-flex h-7 w-7 shrink-0 items-center justify-center"
|
||||
>
|
||||
<OIcon name="format-list-bulleted" size="sm" />
|
||||
</span>
|
||||
</template>
|
||||
{{ t("search.listSavedViews") }}
|
||||
</ODropdownItem>
|
||||
|
||||
<ODropdownItem
|
||||
data-test="logs-search-bar-menu-create-saved-view-btn"
|
||||
shortcut-id="logsSaveView"
|
||||
:disabled="!!saveViewReason"
|
||||
focusable-unavailable
|
||||
:description="saveViewReason || saveViewNote"
|
||||
@select="fnSavedView"
|
||||
>
|
||||
<template #icon-left>
|
||||
<span
|
||||
class="rounded-default bg-section-header-bg text-text-secondary inline-flex h-7 w-7 shrink-0 items-center justify-center"
|
||||
>
|
||||
<OIcon name="add" size="sm" />
|
||||
</span>
|
||||
</template>
|
||||
{{ t("search.createSavedView") }}
|
||||
</ODropdownItem>
|
||||
</ODropdownGroup>
|
||||
|
||||
<ODropdownSeparator />
|
||||
</SavedViewsSubmenu>
|
||||
|
||||
<!-- SYNTAX GUIDE -->
|
||||
<div class="flex w-full items-center pe-2">
|
||||
@@ -1710,28 +1646,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
v-model="confirmDelete"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
:title="t('search.updateSavedView')"
|
||||
:message="t('search.updateSavedViewConfirm')"
|
||||
@update:ok="confirmUpdateSavedViews"
|
||||
@update:cancel="confirmUpdate = false"
|
||||
v-model="confirmUpdate"
|
||||
:title="t('search.overwriteSavedView')"
|
||||
message-keypath="search.overwriteSavedViewConfirm"
|
||||
:message-name="overwriteViewName"
|
||||
:ok-label="t('search.overwriteSavedViewOk')"
|
||||
focus-cancel
|
||||
@update:ok="confirmOverwriteSavedView"
|
||||
@update:cancel="cancelOverwriteSavedView"
|
||||
v-model="confirmOverwrite"
|
||||
/>
|
||||
<!-- Query Plan Dialog -->
|
||||
<QueryPlanDialog v-model="showExplainDialog" :searchObj="searchObj" />
|
||||
|
||||
<SavedViewsListDialog
|
||||
v-model:open="savedViewsListDialog"
|
||||
v-model:search="searchObj.data.savedViewFilterFields"
|
||||
data-test-prefix="logs-saved-views-dialog"
|
||||
:views="searchObj.data.savedViews"
|
||||
:favorite-ids="favoriteViews"
|
||||
:favorite-views="localSavedViews"
|
||||
:loading="searchObj.loadingSavedView == true"
|
||||
@apply="applySavedView"
|
||||
@update="handleUpdateSavedView"
|
||||
@delete="handleDeleteSavedView"
|
||||
@toggle-favorite="handleFavoriteSavedView"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<string | null>(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,
|
||||
|
||||
@@ -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: '<div v-bind="$attrs"><slot name="label-action" /><slot /></div>',
|
||||
},
|
||||
ODropdownSeparator: { template: "<hr />" },
|
||||
ODropdownSub: {
|
||||
name: "ODropdownSub",
|
||||
template:
|
||||
'<div class="o-dropdown-sub-stub" v-bind="$attrs"><slot name="icon-left" /><slot name="trigger" /><slot name="icon-right" /><slot /></div>',
|
||||
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<boolean>((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();
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
:open="savedViewsDropdownOpen"
|
||||
side="bottom"
|
||||
align="start"
|
||||
@update:open="savedViewsDropdownOpen = $event"
|
||||
@update:open="onSavedViewsDropdownOpenChange"
|
||||
>
|
||||
<template #trigger>
|
||||
<OButton
|
||||
@@ -148,30 +148,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<OTooltip :content="t('search.listSavedViews')" :side-offset="2" />
|
||||
</OButton>
|
||||
</template>
|
||||
<ODropdownGroup :label="t('search.savedViewsLabel')">
|
||||
<TracesSavedViewsMenuItems
|
||||
:views="sortedTracesSavedViews"
|
||||
:favorite-ids="favoriteIds"
|
||||
:loading="savedViewsLoading"
|
||||
@apply="applySavedView"
|
||||
@update="quickUpdateSavedView"
|
||||
/>
|
||||
</ODropdownGroup>
|
||||
<ODropdownSeparator />
|
||||
<ODropdownItem
|
||||
icon-left="save"
|
||||
data-test="traces-saved-view-create"
|
||||
@select="openSaveViewDialog"
|
||||
>
|
||||
{{ t("search.createSavedView") }}
|
||||
</ODropdownItem>
|
||||
<ODropdownItem
|
||||
icon-left="settings"
|
||||
data-test="traces-saved-view-manage"
|
||||
@select="openSavedViewsDialog"
|
||||
>
|
||||
{{ t("search.manageSavedViews") }}
|
||||
</ODropdownItem>
|
||||
<SavedViewsMenu
|
||||
:views="tracesSavedViews"
|
||||
:favorite-ids="favoriteIds"
|
||||
:applied-view-id="appliedSavedViewId"
|
||||
:loading="savedViewsLoading"
|
||||
data-test-prefix="traces-saved-view"
|
||||
@apply="applySavedView"
|
||||
@update="requestUpdateSavedView"
|
||||
@toggle-favorite="toggleFavorite"
|
||||
@create="openSaveViewDialog"
|
||||
@delete="deleteTracesSavedView"
|
||||
/>
|
||||
</ODropdown>
|
||||
<OSeparator vertical />
|
||||
<OButton
|
||||
@@ -186,7 +174,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</OButtonGroup>
|
||||
</div>
|
||||
|
||||
<ODropdown side="bottom" align="start">
|
||||
<ODropdown
|
||||
:open="moreMenuOpen"
|
||||
side="bottom"
|
||||
align="start"
|
||||
@update:open="moreMenuOpen = $event"
|
||||
>
|
||||
<template #trigger>
|
||||
<OButton
|
||||
data-test="traces-search-bar-more-menu-btn"
|
||||
@@ -237,11 +230,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<template v-if="isSearchMode">
|
||||
<ODropdownSeparator />
|
||||
<ODropdownGroup
|
||||
:label="t('search.menuGroupSavedViews')"
|
||||
data-test="traces-search-bar-menu-saved-views-group"
|
||||
<SavedViewsSubmenu
|
||||
:views="tracesSavedViews"
|
||||
:favorite-ids="favoriteIds"
|
||||
:applied-view-id="appliedSavedViewId"
|
||||
:loading="savedViewsLoading"
|
||||
data-test-prefix="traces-search-bar-menu-saved-views"
|
||||
trigger-test-id="traces-search-bar-menu-list-saved-views-btn"
|
||||
create-test-id="traces-search-bar-menu-create-saved-view-btn"
|
||||
@open="refreshSavedViews"
|
||||
@apply="applySavedView"
|
||||
@update="requestUpdateSavedView"
|
||||
@toggle-favorite="toggleFavorite"
|
||||
@create="openSaveViewDialog"
|
||||
@delete="deleteTracesSavedView"
|
||||
>
|
||||
<template #label-action>
|
||||
<template #actions>
|
||||
<OButton
|
||||
data-test="traces-search-bar-menu-pin-saved-views-btn"
|
||||
variant="ghost-neutral"
|
||||
@@ -254,33 +258,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<OIcon :name="isPinned('savedViews') ? 'keep' : 'keep-outline'" size="sm" />
|
||||
</OButton>
|
||||
</template>
|
||||
<ODropdownItem
|
||||
data-test="traces-search-bar-menu-list-saved-views-btn"
|
||||
@select="openSavedViewsDialog"
|
||||
>
|
||||
<template #icon-left>
|
||||
<span
|
||||
class="rounded-default bg-section-header-bg text-text-secondary inline-flex h-7 w-7 shrink-0 items-center justify-center"
|
||||
>
|
||||
<OIcon name="format-list-bulleted" size="sm" />
|
||||
</span>
|
||||
</template>
|
||||
{{ t("search.listSavedViews") }}
|
||||
</ODropdownItem>
|
||||
<ODropdownItem
|
||||
data-test="traces-search-bar-menu-create-saved-view-btn"
|
||||
@select="openSaveViewDialog"
|
||||
>
|
||||
<template #icon-left>
|
||||
<span
|
||||
class="rounded-default bg-section-header-bg text-text-secondary inline-flex h-7 w-7 shrink-0 items-center justify-center"
|
||||
>
|
||||
<OIcon name="add" size="sm" />
|
||||
</span>
|
||||
</template>
|
||||
{{ t("search.createSavedView") }}
|
||||
</ODropdownItem>
|
||||
</ODropdownGroup>
|
||||
</SavedViewsSubmenu>
|
||||
<ODropdownSeparator />
|
||||
</template>
|
||||
|
||||
@@ -596,18 +574,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
data-test="traces-saved-view-name-input"
|
||||
/>
|
||||
</ODialog>
|
||||
<SavedViewsListDialog
|
||||
v-model:open="savedViewsDialogOpen"
|
||||
data-test-prefix="traces-saved-views-dialog"
|
||||
:views="tracesSavedViews"
|
||||
:favorite-ids="favoriteIds"
|
||||
:favorite-views="favoriteViews"
|
||||
:loading="savedViewsLoading"
|
||||
@apply="applySavedView"
|
||||
@update="confirmUpdateSavedView"
|
||||
@delete="deleteTracesSavedView"
|
||||
@toggle-favorite="toggleFavorite"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,95 +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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
|
||||
import OTruncatedText from "@/lib/core/Typography/OTruncatedText.vue";
|
||||
|
||||
interface TracesSavedView {
|
||||
view_id: string;
|
||||
view_name: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
views: TracesSavedView[];
|
||||
favoriteIds: string[];
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{ loading: false },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "apply", view: TracesSavedView): void;
|
||||
(e: "update", view: TracesSavedView): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18nTyped();
|
||||
|
||||
const isFavorite = (view: TracesSavedView) => props.favoriteIds.includes(view.view_id);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="props.loading"
|
||||
class="text-text-secondary flex items-center gap-2 px-3 py-1.5 text-sm"
|
||||
data-test="traces-saved-views-menu-loading"
|
||||
>
|
||||
<OSpinner size="xs" />
|
||||
{{ t("confirmDialog.loading") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="props.views.length"
|
||||
class="max-h-72 overflow-y-auto overscroll-contain"
|
||||
data-test="traces-saved-views-menu-list"
|
||||
>
|
||||
<ODropdownItem
|
||||
v-for="view in props.views"
|
||||
:key="view.view_id"
|
||||
:text-value="view.view_name"
|
||||
:data-test="`traces-saved-view-apply-${view.view_id}`"
|
||||
@select="emit('apply', view)"
|
||||
>
|
||||
<template #icon-left>
|
||||
<OIcon
|
||||
:name="isFavorite(view) ? 'star' : 'saved-search'"
|
||||
size="sm"
|
||||
:class="isFavorite(view) ? 'text-favorite' : ''"
|
||||
:data-test="`traces-saved-view-icon-${view.view_id}`"
|
||||
/>
|
||||
</template>
|
||||
<OTruncatedText class="max-w-56">{{ view.view_name }}</OTruncatedText>
|
||||
<template #icon-right>
|
||||
<OButton
|
||||
variant="ghost"
|
||||
size="icon-xs-sq"
|
||||
icon-left="edit"
|
||||
class="ms-auto"
|
||||
:title="t('search.updateSavedViewWithCurrent')"
|
||||
:data-test="`traces-saved-view-update-${view.view_id}`"
|
||||
@click.stop.prevent="emit('update', view)"
|
||||
/>
|
||||
</template>
|
||||
</ODropdownItem>
|
||||
</div>
|
||||
<ODropdownItem v-else disabled>
|
||||
{{ t("search.savedViewsNotFound") }}
|
||||
</ODropdownItem>
|
||||
</template>
|
||||
Reference in New Issue
Block a user