mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Add saved query support to dashboards (#1584)
Fixes: HDX-1717 On the dashboard page, this PR adds: * An option in the ... dropdown to Save/update/delete default query/filters that will save the WHERE input filter, along with any filters applied to the dashboard via the custom filter functionality + ties it to the dashboard object itself. Future reloads of this dashboard will restore saved values. https://github.com/user-attachments/assets/5f7c18f7-a695-4d19-b338-6de852a4af6b https://github.com/user-attachments/assets/ea7653c8-f862-450f-916c-46edfcbfbf35 Co-authored-by: Himanshu Kapoor <2203925+fleon@users.noreply.github.com>
This commit is contained in:
parent
d760d2db5b
commit
733d612649
9 changed files with 398 additions and 8 deletions
|
|
@ -26,6 +26,9 @@ export default mongoose.model<IDashboard>(
|
|||
default: [],
|
||||
},
|
||||
filters: { type: mongoose.Schema.Types.Array, default: [] },
|
||||
savedQuery: { type: String, required: false },
|
||||
savedQueryLanguage: { type: String, required: false },
|
||||
savedFilterValues: { type: mongoose.Schema.Types.Array, required: false },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ router.patch(
|
|||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
const updates = _.omitBy(req.body, _.isNil);
|
||||
// Only omit undefined values, keep null (which signals field removal)
|
||||
const updates = _.omitBy(req.body, _.isUndefined);
|
||||
|
||||
const updatedDashboard = await updateDashboard(
|
||||
dashboardId,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
|
@ -11,7 +12,7 @@ import Head from 'next/head';
|
|||
import { useRouter } from 'next/router';
|
||||
import { formatRelative } from 'date-fns';
|
||||
import produce from 'immer';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { parseAsJson, parseAsString, useQueryState } from 'nuqs';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import RGL, { WidthProvider } from 'react-grid-layout';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
|
|
@ -52,6 +53,7 @@ import {
|
|||
IconArrowsMaximize,
|
||||
IconBell,
|
||||
IconCopy,
|
||||
IconDeviceFloppy,
|
||||
IconDotsVertical,
|
||||
IconDownload,
|
||||
IconFilterEdit,
|
||||
|
|
@ -61,6 +63,7 @@ import {
|
|||
IconTags,
|
||||
IconTrash,
|
||||
IconUpload,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import { ContactSupportText } from '@/components/ContactSupportText';
|
||||
|
|
@ -733,11 +736,16 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
'whereLanguage',
|
||||
parseAsString.withDefault('lucene'),
|
||||
);
|
||||
// Get raw filter queries from URL (not processed by hook)
|
||||
const [rawFilterQueries] = useQueryState('filters', parseAsJson<Filter[]>());
|
||||
|
||||
// Track if we've initialized query for this dashboard
|
||||
const initializedDashboard = useRef<string>(undefined);
|
||||
|
||||
const [showFiltersModal, setShowFiltersModal] = useState(false);
|
||||
|
||||
const filters = dashboard?.filters ?? [];
|
||||
const { filterValues, setFilterValue, filterQueries } =
|
||||
const { filterValues, setFilterValue, filterQueries, setFilterQueries } =
|
||||
useDashboardFilters(filters);
|
||||
|
||||
const handleSaveFilter = (filter: DashboardFilter) => {
|
||||
|
|
@ -767,7 +775,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
|
||||
const { control, setValue, handleSubmit } = useForm<{
|
||||
const { control, setValue, getValues, handleSubmit } = useForm<{
|
||||
granularity: SQLInterval | 'auto';
|
||||
where: SearchCondition;
|
||||
whereLanguage: SearchConditionLanguage;
|
||||
|
|
@ -807,13 +815,134 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
isLive,
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
const onSubmit = useCallback(() => {
|
||||
onSearch(displayedTimeInputValue);
|
||||
handleSubmit(data => {
|
||||
setWhere(data.where as SearchCondition);
|
||||
setWhereLanguage((data.whereLanguage as SearchConditionLanguage) ?? null);
|
||||
})();
|
||||
};
|
||||
}, [
|
||||
displayedTimeInputValue,
|
||||
handleSubmit,
|
||||
onSearch,
|
||||
setWhere,
|
||||
setWhereLanguage,
|
||||
]);
|
||||
|
||||
// Initialize query/filter state once when dashboard changes.
|
||||
useEffect(() => {
|
||||
if (!dashboard?.id || !router.isReady) return;
|
||||
if (!isLocalDashboard && isFetchingDashboard) return;
|
||||
if (initializedDashboard.current === dashboard.id) return;
|
||||
const isSwitchingDashboards =
|
||||
initializedDashboard.current != null &&
|
||||
initializedDashboard.current !== dashboard.id;
|
||||
|
||||
const hasWhereInUrl = 'where' in router.query;
|
||||
const hasFiltersInUrl = 'filters' in router.query;
|
||||
|
||||
// Query defaults: URL query overrides saved defaults. If switching to a
|
||||
// dashboard without defaults, clear query. On first load/reload, keep current state.
|
||||
if (!hasWhereInUrl) {
|
||||
if (dashboard.savedQuery) {
|
||||
setValue('where', dashboard.savedQuery);
|
||||
setWhere(dashboard.savedQuery);
|
||||
const savedLanguage = dashboard.savedQueryLanguage ?? 'lucene';
|
||||
setValue('whereLanguage', savedLanguage);
|
||||
setWhereLanguage(savedLanguage);
|
||||
} else if (isSwitchingDashboards) {
|
||||
setValue('where', '');
|
||||
setWhere('');
|
||||
setValue('whereLanguage', 'lucene');
|
||||
setWhereLanguage('lucene');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter defaults: URL filters override saved defaults. If switching to a
|
||||
// dashboard without defaults, clear selected filters.
|
||||
if (!hasFiltersInUrl) {
|
||||
if (dashboard.savedFilterValues) {
|
||||
setFilterQueries(dashboard.savedFilterValues);
|
||||
} else if (isSwitchingDashboards) {
|
||||
setFilterQueries(null);
|
||||
}
|
||||
}
|
||||
|
||||
initializedDashboard.current = dashboard.id;
|
||||
}, [
|
||||
dashboard?.id,
|
||||
dashboard?.savedQuery,
|
||||
dashboard?.savedQueryLanguage,
|
||||
dashboard?.savedFilterValues,
|
||||
isLocalDashboard,
|
||||
isFetchingDashboard,
|
||||
router.isReady,
|
||||
router.query,
|
||||
setValue,
|
||||
setWhere,
|
||||
setWhereLanguage,
|
||||
setFilterQueries,
|
||||
]);
|
||||
|
||||
const handleSaveQuery = useCallback(() => {
|
||||
if (!dashboard || isLocalDashboard) return;
|
||||
|
||||
// Execute the query first (updates URL)
|
||||
onSubmit();
|
||||
|
||||
// Then save to database (reads from form values which were just submitted to URL)
|
||||
const formValues = getValues();
|
||||
const currentWhere = formValues.where || null;
|
||||
const currentWhereLanguage = currentWhere
|
||||
? formValues.whereLanguage || 'lucene'
|
||||
: null;
|
||||
const currentFilterValues = rawFilterQueries?.length
|
||||
? rawFilterQueries
|
||||
: null;
|
||||
|
||||
setDashboard(
|
||||
produce(dashboard, draft => {
|
||||
draft.savedQuery = currentWhere;
|
||||
draft.savedQueryLanguage = currentWhereLanguage;
|
||||
draft.savedFilterValues = currentFilterValues;
|
||||
}),
|
||||
() => {
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
title: 'Query saved and executed',
|
||||
message:
|
||||
'Filter query and dropdown values have been saved with the dashboard',
|
||||
autoClose: 3000,
|
||||
});
|
||||
},
|
||||
);
|
||||
}, [
|
||||
dashboard,
|
||||
isLocalDashboard,
|
||||
setDashboard,
|
||||
getValues,
|
||||
rawFilterQueries,
|
||||
onSubmit,
|
||||
]);
|
||||
const handleRemoveSavedQuery = useCallback(() => {
|
||||
if (!dashboard || isLocalDashboard) return;
|
||||
|
||||
setDashboard(
|
||||
produce(dashboard, draft => {
|
||||
draft.savedQuery = null;
|
||||
draft.savedQueryLanguage = null;
|
||||
draft.savedFilterValues = null;
|
||||
}),
|
||||
() => {
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
title: 'Default query and filters removed',
|
||||
message: 'Dashboard will no longer auto-apply saved defaults',
|
||||
autoClose: 3000,
|
||||
});
|
||||
},
|
||||
);
|
||||
}, [dashboard, isLocalDashboard, setDashboard]);
|
||||
|
||||
const [editedTile, setEditedTile] = useState<undefined | Tile>();
|
||||
|
||||
|
|
@ -1008,6 +1137,9 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const hasTiles = dashboard && dashboard.tiles.length > 0;
|
||||
const hasSavedQueryAndFilterDefaults = Boolean(
|
||||
dashboard?.savedQuery || dashboard?.savedFilterValues?.length,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box p="sm" data-testid="dashboard-page">
|
||||
|
|
@ -1097,7 +1229,11 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
{!isLocalDashboard /* local dashboards cant be "deleted" */ && (
|
||||
<Menu width={250}>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="secondary" size="input-xs">
|
||||
<ActionIcon
|
||||
variant="secondary"
|
||||
size="input-xs"
|
||||
data-testid="dashboard-menu-button"
|
||||
>
|
||||
<IconDotsVertical size={14} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
|
@ -1141,6 +1277,27 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
>
|
||||
{hasTiles ? 'Import New Dashboard' : 'Import Dashboard'}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
data-testid="save-default-query-filters-menu-item"
|
||||
leftSection={<IconDeviceFloppy size={16} />}
|
||||
onClick={handleSaveQuery}
|
||||
>
|
||||
{hasSavedQueryAndFilterDefaults
|
||||
? 'Update Default Query & Filters'
|
||||
: 'Save Query & Filters as Default'}
|
||||
</Menu.Item>
|
||||
{hasSavedQueryAndFilterDefaults && (
|
||||
<Menu.Item
|
||||
data-testid="remove-default-query-filters-menu-item"
|
||||
leftSection={<IconX size={16} />}
|
||||
color="red"
|
||||
onClick={handleRemoveSavedQuery}
|
||||
>
|
||||
Remove Default Query & Filters
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={16} />}
|
||||
color="red"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import { useCallback, useMemo, useState } from 'react';
|
|||
import { parseAsJson, useQueryState } from 'nuqs';
|
||||
import {
|
||||
DashboardFilter,
|
||||
Filter,
|
||||
SavedChartConfig,
|
||||
SearchConditionLanguage,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
|
@ -28,6 +30,9 @@ export type Dashboard = {
|
|||
tiles: Tile[];
|
||||
tags: string[];
|
||||
filters?: DashboardFilter[];
|
||||
savedQuery?: string | null;
|
||||
savedQueryLanguage?: SearchConditionLanguage | null;
|
||||
savedFilterValues?: Filter[] | null;
|
||||
};
|
||||
|
||||
export function useUpdateDashboard() {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ describe('usePresetDashboardFilters', () => {
|
|||
filterValues: mockFilterValues,
|
||||
setFilterValue: mockSetFilterValue,
|
||||
filterQueries: mockFilterQueries,
|
||||
setFilterQueries: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const useDashboardFilters = (filters: DashboardFilter[]) => {
|
|||
filterValues: valuesForExistingFilters,
|
||||
filterQueries: queriesForExistingFilters,
|
||||
setFilterValue,
|
||||
setFilterQueries,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -390,4 +390,203 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'should save and restore query and filter values',
|
||||
{ tag: '@full-stack' },
|
||||
async () => {
|
||||
const testQuery = 'level:error';
|
||||
let dashboardUrl: string;
|
||||
|
||||
await test.step('Create dashboard with chart', async () => {
|
||||
await dashboardPage.createNewDashboard();
|
||||
|
||||
// Add a tile so dashboard is saveable
|
||||
await dashboardPage.addTile();
|
||||
await dashboardPage.chartEditor.createBasicChart('Test Chart');
|
||||
|
||||
const chartContainers = dashboardPage.getChartContainers();
|
||||
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
|
||||
|
||||
// Save dashboard URL for later
|
||||
dashboardUrl = dashboardPage.page.url();
|
||||
});
|
||||
|
||||
await test.step('Enter query in search bar', async () => {
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toBeVisible();
|
||||
await searchInput.fill(testQuery);
|
||||
});
|
||||
|
||||
await test.step('Click save query button', async () => {
|
||||
await dashboardPage.saveQueryAndFiltersAsDefault();
|
||||
|
||||
// Wait for success notification
|
||||
const notification = dashboardPage.page.locator(
|
||||
'text=/Filter query and dropdown values/i',
|
||||
);
|
||||
await expect(notification).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('Navigate away from dashboard', async () => {
|
||||
await dashboardPage.page.goto('/search');
|
||||
await expect(dashboardPage.page).toHaveURL(/.*\/search/);
|
||||
});
|
||||
|
||||
await test.step('Return to dashboard and verify query restored', async () => {
|
||||
await dashboardPage.page.goto(dashboardUrl);
|
||||
|
||||
// Wait for dashboard controls to load
|
||||
await expect(
|
||||
dashboardPage.page.getByTestId('dashboard-page'),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(dashboardPage.searchInput).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify saved query is restored in search input
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toHaveValue(testQuery);
|
||||
});
|
||||
|
||||
await test.step('Clear URL params and verify query persists', async () => {
|
||||
// Extract dashboard ID and navigate without query params
|
||||
const dashboardId = dashboardUrl.split('/').pop()?.split('?')[0];
|
||||
await dashboardPage.page.goto(`/dashboards/${dashboardId}`);
|
||||
|
||||
// Verify saved query still loads
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toHaveValue(testQuery);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'should handle URL query params overriding saved query',
|
||||
{ tag: '@full-stack' },
|
||||
async () => {
|
||||
const savedQuery = 'level:error';
|
||||
const urlQuery = 'status:active';
|
||||
let dashboardId: string;
|
||||
|
||||
await test.step('Create dashboard and save query', async () => {
|
||||
await dashboardPage.createNewDashboard();
|
||||
|
||||
// Add a tile
|
||||
await dashboardPage.addTile();
|
||||
await dashboardPage.chartEditor.createBasicChart('Test Chart');
|
||||
|
||||
// Enter and save query
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await searchInput.fill(savedQuery);
|
||||
|
||||
await dashboardPage.saveQueryAndFiltersAsDefault();
|
||||
|
||||
// Wait for save confirmation
|
||||
await dashboardPage.page.waitForTimeout(1000);
|
||||
|
||||
// Extract dashboard ID
|
||||
const url = dashboardPage.page.url();
|
||||
dashboardId = url.split('/').pop()?.split('?')[0] || '';
|
||||
});
|
||||
|
||||
await test.step('Navigate with URL query param', async () => {
|
||||
// Navigate to dashboard with URL query param
|
||||
await dashboardPage.page.goto(
|
||||
`/dashboards/${dashboardId}?where=${encodeURIComponent(urlQuery)}`,
|
||||
);
|
||||
|
||||
// Wait for dashboard controls to load
|
||||
await expect(
|
||||
dashboardPage.page.getByTestId('dashboard-page'),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
await expect(dashboardPage.searchInput).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify URL query takes precedence over saved query
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toHaveValue(urlQuery);
|
||||
await expect(searchInput).not.toHaveValue(savedQuery);
|
||||
});
|
||||
|
||||
await test.step('Navigate without URL params to verify saved query', async () => {
|
||||
await dashboardPage.page.goto(`/dashboards/${dashboardId}`);
|
||||
|
||||
// Verify saved query is restored when no URL params
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toHaveValue(savedQuery);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'should clear saved query when WHERE input is cleared and saved',
|
||||
{ tag: '@full-stack' },
|
||||
async () => {
|
||||
const testQuery = 'level:warn';
|
||||
let dashboardUrl: string;
|
||||
|
||||
await test.step('Create dashboard with chart', async () => {
|
||||
await dashboardPage.createNewDashboard();
|
||||
|
||||
// Add a tile so dashboard is saveable
|
||||
await dashboardPage.addTile();
|
||||
await dashboardPage.chartEditor.createBasicChart('Test Chart');
|
||||
|
||||
const chartContainers = dashboardPage.getChartContainers();
|
||||
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
|
||||
|
||||
// Save dashboard URL for later
|
||||
dashboardUrl = dashboardPage.page.url();
|
||||
});
|
||||
|
||||
await test.step('Enter and save query', async () => {
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toBeVisible();
|
||||
await searchInput.fill(testQuery);
|
||||
|
||||
await dashboardPage.saveQueryAndFiltersAsDefault();
|
||||
|
||||
// Wait for success notification
|
||||
const notification = dashboardPage.page.locator(
|
||||
'text=/Filter query and dropdown values/i',
|
||||
);
|
||||
await expect(notification).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('Navigate away and verify query persists', async () => {
|
||||
await dashboardPage.page.goto('/search');
|
||||
await dashboardPage.page.goto(dashboardUrl);
|
||||
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toHaveValue(testQuery);
|
||||
});
|
||||
|
||||
await test.step('Clear the query and save', async () => {
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await searchInput.clear();
|
||||
|
||||
await dashboardPage.saveQueryAndFiltersAsDefault();
|
||||
|
||||
// Wait for success notification
|
||||
const notification = dashboardPage.page.locator(
|
||||
'text=/Filter query and dropdown values/i',
|
||||
);
|
||||
await expect(notification).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('Navigate away and verify query is cleared', async () => {
|
||||
await dashboardPage.page.goto('/search');
|
||||
|
||||
// Extract dashboard ID and navigate back
|
||||
const dashboardId = dashboardUrl.split('/').pop()?.split('?')[0];
|
||||
await dashboardPage.page.goto(`/dashboards/${dashboardId}`);
|
||||
|
||||
// Wait for dashboard to load
|
||||
const chartContainers = dashboardPage.getChartContainers();
|
||||
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
|
||||
|
||||
// Verify search input is empty (saved query was removed)
|
||||
const searchInput = dashboardPage.searchInput;
|
||||
await expect(searchInput).toHaveValue('');
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,11 +59,11 @@ export class DashboardPage {
|
|||
readonly timePicker: TimePickerComponent;
|
||||
readonly chartEditor: ChartEditorComponent;
|
||||
readonly granularityPicker: Locator;
|
||||
readonly searchInput: Locator;
|
||||
|
||||
private readonly createDashboardButton: Locator;
|
||||
private readonly addTileButton: Locator;
|
||||
private readonly dashboardNameHeading: Locator;
|
||||
private readonly searchInput: Locator;
|
||||
private readonly searchSubmitButton: Locator;
|
||||
private readonly liveButton: Locator;
|
||||
private readonly tempDashboardBanner: Locator;
|
||||
|
|
@ -80,6 +80,9 @@ export class DashboardPage {
|
|||
private readonly confirmModal: Locator;
|
||||
private readonly confirmCancelButton: Locator;
|
||||
private readonly confirmConfirmButton: Locator;
|
||||
private readonly dashboardMenuButton: Locator;
|
||||
private readonly saveDefaultQueryAndFiltersMenuItem: Locator;
|
||||
private readonly removeDefaultQueryAndFiltersMenuItem: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
|
@ -117,6 +120,13 @@ export class DashboardPage {
|
|||
this.confirmModal = page.getByTestId('confirm-modal');
|
||||
this.confirmCancelButton = page.getByTestId('confirm-cancel-button');
|
||||
this.confirmConfirmButton = page.getByTestId('confirm-confirm-button');
|
||||
this.dashboardMenuButton = page.getByTestId('dashboard-menu-button');
|
||||
this.saveDefaultQueryAndFiltersMenuItem = page.getByTestId(
|
||||
'save-default-query-filters-menu-item',
|
||||
);
|
||||
this.removeDefaultQueryAndFiltersMenuItem = page.getByTestId(
|
||||
'remove-default-query-filters-menu-item',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -285,6 +295,16 @@ export class DashboardPage {
|
|||
await this.searchSubmitButton.click();
|
||||
}
|
||||
|
||||
async saveQueryAndFiltersAsDefault() {
|
||||
await this.dashboardMenuButton.click();
|
||||
await this.saveDefaultQueryAndFiltersMenuItem.click();
|
||||
}
|
||||
|
||||
async removeSavedQueryAndFiltersDefaults() {
|
||||
await this.dashboardMenuButton.click();
|
||||
await this.removeDefaultQueryAndFiltersMenuItem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle live mode
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -569,6 +569,9 @@ export const DashboardSchema = z.object({
|
|||
tiles: z.array(TileSchema),
|
||||
tags: z.array(z.string()),
|
||||
filters: z.array(DashboardFilterSchema).optional(),
|
||||
savedQuery: z.string().nullable().optional(),
|
||||
savedQueryLanguage: SearchConditionLanguageSchema.nullable().optional(),
|
||||
savedFilterValues: z.array(FilterSchema).nullable().optional(),
|
||||
});
|
||||
export const DashboardWithoutIdSchema = DashboardSchema.omit({ id: true });
|
||||
export type DashboardWithoutId = z.infer<typeof DashboardWithoutIdSchema>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue