mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-23 20:00:42 +01:00
Compare commits
1 Commits
saved-view
...
issue_6107
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed7d193e7 |
@@ -3,22 +3,15 @@ import {
|
||||
MessageActionKindDTO,
|
||||
SavedViewEntityDTO,
|
||||
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import {
|
||||
getSavedView,
|
||||
listSavedViews,
|
||||
} from 'api/generated/services/saved-view';
|
||||
import {
|
||||
GetSavedView200,
|
||||
ListSavedViews200,
|
||||
SavedviewtypesPanelTypeDTO,
|
||||
SavedviewtypesSavedViewDTO,
|
||||
SavedviewtypesSchemaVersionDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getAllViews } from 'api/saveView/getAllViews';
|
||||
import { getViewById } from 'api/saveView/getViewById';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { AllViewsProps, ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import type { History } from 'history';
|
||||
|
||||
import {
|
||||
@@ -38,7 +31,8 @@ import {
|
||||
} from '../resolveOpenResource';
|
||||
import { resourceRoute, ResourceType } from '../resourceRoute';
|
||||
|
||||
jest.mock('api/generated/services/saved-view');
|
||||
jest.mock('api/saveView/getAllViews');
|
||||
jest.mock('api/saveView/getViewById');
|
||||
|
||||
jest.mock(
|
||||
'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi',
|
||||
@@ -54,45 +48,43 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
const mockedListSavedViews = listSavedViews as jest.MockedFunction<
|
||||
typeof listSavedViews
|
||||
const mockedGetAllViews = getAllViews as jest.MockedFunction<
|
||||
typeof getAllViews
|
||||
>;
|
||||
const mockedGetSavedView = getSavedView as jest.MockedFunction<
|
||||
typeof getSavedView
|
||||
const mockedGetViewById = getViewById as jest.MockedFunction<
|
||||
typeof getViewById
|
||||
>;
|
||||
|
||||
function makeView(
|
||||
id: string,
|
||||
source: SavedviewtypesSourceDTO,
|
||||
): SavedviewtypesSavedViewDTO {
|
||||
function makeView(id: string, sourcePage: DataSource): ViewProps {
|
||||
return {
|
||||
id,
|
||||
name: `view-${id}`,
|
||||
source,
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO.v2,
|
||||
name: `View ${id}`,
|
||||
category: 'test',
|
||||
createdAt: '2021-07-07T06:31:00.000Z',
|
||||
createdBy: 'user',
|
||||
updatedAt: '2021-07-07T06:33:00.000Z',
|
||||
updatedBy: 'user',
|
||||
spec: {
|
||||
displayName: `View ${id}`,
|
||||
panelType: SavedviewtypesPanelTypeDTO.list,
|
||||
requestType: 'raw',
|
||||
queries: [{ type: 'builder_query', spec: { name: 'A', signal: source } }],
|
||||
},
|
||||
} as unknown as SavedviewtypesSavedViewDTO;
|
||||
sourcePage,
|
||||
tags: [],
|
||||
extraData: '',
|
||||
compositeQuery: {
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
} as ICompositeMetricQuery,
|
||||
};
|
||||
}
|
||||
|
||||
function mockViewsResponse(
|
||||
views: SavedviewtypesSavedViewDTO[],
|
||||
): ListSavedViews200 {
|
||||
return { status: 'success', data: views };
|
||||
function mockViewsResponse(views: ViewProps[]): AxiosResponse<AllViewsProps> {
|
||||
return {
|
||||
data: { status: 'success', data: views },
|
||||
} as AxiosResponse<AllViewsProps>;
|
||||
}
|
||||
|
||||
function mockViewByIdResponse(
|
||||
view: SavedviewtypesSavedViewDTO,
|
||||
): GetSavedView200 {
|
||||
return { status: 'success', data: view };
|
||||
view: ViewProps,
|
||||
): AxiosResponse<{ status: string; data: ViewProps }> {
|
||||
return {
|
||||
data: { status: 'success', data: view },
|
||||
} as AxiosResponse<{ status: string; data: ViewProps }>;
|
||||
}
|
||||
|
||||
describe('resourceRoute', () => {
|
||||
@@ -198,33 +190,18 @@ describe('resolveOpenResource', () => {
|
||||
|
||||
describe('findSavedViewInLists', () => {
|
||||
beforeEach(() => {
|
||||
mockedListSavedViews.mockReset();
|
||||
mockedGetAllViews.mockReset();
|
||||
});
|
||||
|
||||
it('loads only the hinted source when entity is provided', async () => {
|
||||
const tracesView = makeView('view-traces', SavedviewtypesSourceDTO.traces);
|
||||
mockedListSavedViews.mockResolvedValueOnce(mockViewsResponse([tracesView]));
|
||||
const tracesView = makeView('view-traces', DataSource.TRACES);
|
||||
mockedGetAllViews.mockResolvedValueOnce(mockViewsResponse([tracesView]));
|
||||
|
||||
const result = await findSavedViewInLists('view-traces', DataSource.TRACES);
|
||||
|
||||
expect(result).toStrictEqual(tracesView);
|
||||
expect(mockedListSavedViews).toHaveBeenCalledTimes(1);
|
||||
expect(mockedListSavedViews).toHaveBeenCalledWith({
|
||||
source: SavedviewtypesSourceDTO.traces,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a null list as empty and probes the next source', async () => {
|
||||
const metricsView = makeView('view-metrics', SavedviewtypesSourceDTO.metrics);
|
||||
mockedListSavedViews
|
||||
.mockResolvedValueOnce({ status: 'success', data: null })
|
||||
.mockResolvedValueOnce(mockViewsResponse([]))
|
||||
.mockResolvedValueOnce(mockViewsResponse([metricsView]));
|
||||
|
||||
const result = await findSavedViewInLists('view-metrics');
|
||||
|
||||
expect(result).toStrictEqual(metricsView);
|
||||
expect(mockedListSavedViews).toHaveBeenCalledTimes(3);
|
||||
expect(mockedGetAllViews).toHaveBeenCalledTimes(1);
|
||||
expect(mockedGetAllViews).toHaveBeenCalledWith(DataSource.TRACES);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,75 +227,52 @@ describe('openSavedView', () => {
|
||||
it('navigates with history.push and view query params', () => {
|
||||
const push = jest.fn();
|
||||
const history = { push } as unknown as History;
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
const view = makeView('view-logs', DataSource.LOGS);
|
||||
|
||||
openSavedView(view, history);
|
||||
|
||||
expect(push).toHaveBeenCalledTimes(1);
|
||||
const pushedUrl = push.mock.calls[0][0] as string;
|
||||
expect(pushedUrl).toContain(ROUTES.LOGS_EXPLORER);
|
||||
const params = new URLSearchParams(pushedUrl.split('?')[1]);
|
||||
expect(params.get(QueryParams.viewKey)).toBe('"view-logs"');
|
||||
expect(params.get(QueryParams.viewName)).toBe('"View view-logs"');
|
||||
expect(params.get(QueryParams.panelTypes)).toBe('"list"');
|
||||
});
|
||||
|
||||
it('throws when the view has no source', () => {
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
delete view.source;
|
||||
|
||||
expect(() =>
|
||||
openSavedView(view, { push: jest.fn() } as unknown as History),
|
||||
).toThrow('Unsupported saved view source');
|
||||
});
|
||||
|
||||
it('throws when the view has no queries', () => {
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
view.spec.queries = [];
|
||||
|
||||
expect(() =>
|
||||
openSavedView(view, { push: jest.fn() } as unknown as History),
|
||||
).toThrow('Saved view is missing query data');
|
||||
expect(pushedUrl).toContain(QueryParams.viewKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openSavedViewByKey', () => {
|
||||
beforeEach(() => {
|
||||
mockedListSavedViews.mockReset();
|
||||
mockedGetSavedView.mockReset();
|
||||
mockedGetAllViews.mockReset();
|
||||
mockedGetViewById.mockReset();
|
||||
});
|
||||
|
||||
it('prefers the direct view lookup endpoint', async () => {
|
||||
const view = makeView('view-logs', SavedviewtypesSourceDTO.logs);
|
||||
mockedGetSavedView.mockResolvedValueOnce(mockViewByIdResponse(view));
|
||||
const view = makeView('view-logs', DataSource.LOGS);
|
||||
mockedGetViewById.mockResolvedValueOnce(mockViewByIdResponse(view));
|
||||
const push = jest.fn();
|
||||
const history = { push } as unknown as History;
|
||||
|
||||
await openSavedViewByKey('view-logs', DataSource.LOGS, history);
|
||||
|
||||
expect(mockedGetSavedView).toHaveBeenCalledWith({ id: 'view-logs' });
|
||||
expect(mockedListSavedViews).not.toHaveBeenCalled();
|
||||
expect(mockedGetViewById).toHaveBeenCalledWith('view-logs');
|
||||
expect(mockedGetAllViews).not.toHaveBeenCalled();
|
||||
expect(push).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to list probing when direct lookup fails', async () => {
|
||||
const view = makeView('view-traces', SavedviewtypesSourceDTO.traces);
|
||||
mockedGetSavedView.mockRejectedValueOnce(new Error('not found'));
|
||||
mockedListSavedViews.mockResolvedValueOnce(mockViewsResponse([view]));
|
||||
const view = makeView('view-traces', DataSource.TRACES);
|
||||
mockedGetViewById.mockRejectedValueOnce(new Error('not found'));
|
||||
mockedGetAllViews.mockResolvedValueOnce(mockViewsResponse([view]));
|
||||
const push = jest.fn();
|
||||
const history = { push } as unknown as History;
|
||||
|
||||
await openSavedViewByKey('view-traces', DataSource.TRACES, history);
|
||||
|
||||
expect(mockedListSavedViews).toHaveBeenCalledWith({
|
||||
source: SavedviewtypesSourceDTO.traces,
|
||||
});
|
||||
expect(mockedGetAllViews).toHaveBeenCalledWith(DataSource.TRACES);
|
||||
expect(push).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when the saved view does not exist', async () => {
|
||||
mockedGetSavedView.mockRejectedValueOnce(new Error('not found'));
|
||||
mockedListSavedViews.mockResolvedValue(mockViewsResponse([]));
|
||||
mockedGetViewById.mockRejectedValueOnce(new Error('not found'));
|
||||
mockedGetAllViews.mockResolvedValue(mockViewsResponse([]));
|
||||
|
||||
await expect(
|
||||
openSavedViewByKey('missing', DataSource.LOGS, {
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import {
|
||||
getSavedView,
|
||||
listSavedViews,
|
||||
} from 'api/generated/services/saved-view';
|
||||
import { SavedviewtypesSavedViewDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { getAllViews } from 'api/saveView/getAllViews';
|
||||
import { getViewById } from 'api/saveView/getViewById';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
findSavedView,
|
||||
getSavedViewQuery,
|
||||
SavedViewSourcePage,
|
||||
toSavedViewSource,
|
||||
} from 'container/SavedViews/utils';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { History } from 'history';
|
||||
|
||||
type SavedViewSourceHint = SavedViewSourcePage;
|
||||
type SavedViewSourceHint = DataSource | 'meter';
|
||||
|
||||
const DEFAULT_PROBE_SOURCES: SavedViewSourceHint[] = [
|
||||
DataSource.LOGS,
|
||||
@@ -27,15 +20,13 @@ const DEFAULT_PROBE_SOURCES: SavedViewSourceHint[] = [
|
||||
export async function findSavedViewInLists(
|
||||
viewKey: string,
|
||||
sourceHint?: SavedViewSourceHint | null,
|
||||
): Promise<SavedviewtypesSavedViewDTO | null> {
|
||||
): Promise<ViewProps | null> {
|
||||
const sources = sourceHint ? [sourceHint] : DEFAULT_PROBE_SOURCES;
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const response = await listSavedViews({
|
||||
source: toSavedViewSource(source),
|
||||
});
|
||||
const match = findSavedView(response.data, viewKey);
|
||||
const response = await getAllViews(source);
|
||||
const match = response.data.data.find((view) => view.id === viewKey);
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
@@ -50,11 +41,11 @@ export async function findSavedViewInLists(
|
||||
async function loadSavedView(
|
||||
viewKey: string,
|
||||
sourceHint?: SavedViewSourceHint | null,
|
||||
): Promise<SavedviewtypesSavedViewDTO> {
|
||||
): Promise<ViewProps> {
|
||||
try {
|
||||
const response = await getSavedView({ id: viewKey });
|
||||
if (response.data) {
|
||||
return response.data;
|
||||
const response = await getViewById(viewKey);
|
||||
if (response.data?.data) {
|
||||
return response.data.data;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to list probing when the direct lookup fails.
|
||||
@@ -94,23 +85,20 @@ export function buildExplorerNavigationUrl(
|
||||
return `${route}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function openSavedView(
|
||||
view: SavedviewtypesSavedViewDTO,
|
||||
history: History,
|
||||
): void {
|
||||
const route = view.source ? explorerRouteForSourcePage(view.source) : null;
|
||||
export function openSavedView(view: ViewProps, history: History): void {
|
||||
const route = explorerRouteForSourcePage(view.sourcePage);
|
||||
if (!route) {
|
||||
throw new Error('Unsupported saved view source');
|
||||
}
|
||||
|
||||
if (!view.spec.queries?.length) {
|
||||
if (!view.compositeQuery) {
|
||||
throw new Error('Saved view is missing query data');
|
||||
}
|
||||
|
||||
const query = getSavedViewQuery(view);
|
||||
const query = mapQueryDataFromApi(view.compositeQuery);
|
||||
const url = buildExplorerNavigationUrl(route, query, {
|
||||
[QueryParams.panelTypes]: view.spec.panelType as unknown as PANEL_TYPES,
|
||||
[QueryParams.viewName]: view.spec.displayName,
|
||||
[QueryParams.panelTypes]: view.compositeQuery.panelType as PANEL_TYPES,
|
||||
[QueryParams.viewName]: view.name,
|
||||
[QueryParams.viewKey]: view.id,
|
||||
});
|
||||
history.push(url);
|
||||
@@ -124,3 +112,6 @@ export async function openSavedViewByKey(
|
||||
const view = await loadSavedView(viewKey, sourceHint);
|
||||
openSavedView(view, history);
|
||||
}
|
||||
|
||||
/** @deprecated Use findSavedViewInLists — kept for tests. */
|
||||
export const findSavedView = findSavedViewInLists;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useListSavedViews } from 'api/generated/services/saved-view';
|
||||
import {
|
||||
SavedviewtypesSavedViewDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getSavedViewQuery } from 'container/SavedViews/utils';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import floppyDiscUrl from '@/assets/Icons/floppy-disc.svg';
|
||||
@@ -36,40 +35,38 @@ export default function SavedViews({
|
||||
}): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const [selectedEntity, setSelectedEntity] = useState<string>('logs');
|
||||
const [selectedEntityViews, setSelectedEntityViews] = useState<
|
||||
SavedviewtypesSavedViewDTO[]
|
||||
>([]);
|
||||
const [selectedEntityViews, setSelectedEntityViews] = useState<any[]>([]);
|
||||
|
||||
const {
|
||||
data: logsViewsData,
|
||||
isLoading: logsViewsLoading,
|
||||
isError: logsViewsError,
|
||||
} = useListSavedViews({ source: SavedviewtypesSourceDTO.logs });
|
||||
} = useGetAllViews(DataSource.LOGS);
|
||||
|
||||
const {
|
||||
data: tracesViewsData,
|
||||
isLoading: tracesViewsLoading,
|
||||
isError: tracesViewsError,
|
||||
} = useListSavedViews({ source: SavedviewtypesSourceDTO.traces });
|
||||
} = useGetAllViews(DataSource.TRACES);
|
||||
|
||||
const {
|
||||
data: metricsViewsData,
|
||||
isLoading: metricsViewsLoading,
|
||||
isError: metricsViewsError,
|
||||
} = useListSavedViews({ source: SavedviewtypesSourceDTO.metrics });
|
||||
} = useGetAllViews(DataSource.METRICS);
|
||||
|
||||
const logsViews = useMemo(
|
||||
() => [...(logsViewsData?.data || [])],
|
||||
() => [...(logsViewsData?.data.data || [])],
|
||||
[logsViewsData],
|
||||
);
|
||||
|
||||
const tracesViews = useMemo(
|
||||
() => [...(tracesViewsData?.data || [])],
|
||||
() => [...(tracesViewsData?.data.data || [])],
|
||||
[tracesViewsData],
|
||||
);
|
||||
|
||||
const metricsViews = useMemo(
|
||||
() => [...(metricsViewsData?.data || [])],
|
||||
() => [...(metricsViewsData?.data.data || [])],
|
||||
[metricsViewsData],
|
||||
);
|
||||
|
||||
@@ -91,22 +88,39 @@ export default function SavedViews({
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
const handleRedirectQuery = (view: SavedviewtypesSavedViewDTO): void => {
|
||||
const handleRedirectQuery = (view: ViewProps): void => {
|
||||
logEvent('Homepage: Saved view clicked', {
|
||||
viewId: view.id,
|
||||
viewName: view.spec.displayName,
|
||||
viewName: view.name,
|
||||
entity: selectedEntity,
|
||||
});
|
||||
|
||||
handleExplorerTabChange(
|
||||
view.spec.panelType,
|
||||
{
|
||||
query: getSavedViewQuery(view),
|
||||
viewName: view.spec.displayName,
|
||||
viewKey: view.id,
|
||||
},
|
||||
SOURCEPAGE_VS_ROUTES[selectedEntity],
|
||||
);
|
||||
let currentViews: ViewProps[] = [];
|
||||
if (selectedEntity === 'logs') {
|
||||
currentViews = logsViews;
|
||||
} else if (selectedEntity === 'traces') {
|
||||
currentViews = tracesViews;
|
||||
} else if (selectedEntity === 'metrics') {
|
||||
currentViews = metricsViews;
|
||||
}
|
||||
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(view.id, currentViews);
|
||||
if (!currentViewDetails) {
|
||||
return;
|
||||
}
|
||||
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
|
||||
|
||||
if (selectedEntity) {
|
||||
handleExplorerTabChange(
|
||||
currentPanelType,
|
||||
{
|
||||
query,
|
||||
viewName: name,
|
||||
viewKey: id,
|
||||
},
|
||||
SOURCEPAGE_VS_ROUTES[selectedEntity],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -225,10 +239,24 @@ export default function SavedViews({
|
||||
/>
|
||||
|
||||
<div className="saved-view-item-name home-data-item-name">
|
||||
{view.spec.displayName}
|
||||
{view.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="saved-view-item-description home-data-item-tag">
|
||||
{view.tags?.map((tag: string) => {
|
||||
if (tag === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge color="sienna" key={tag}>
|
||||
{tag}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@@ -279,7 +307,7 @@ export default function SavedViews({
|
||||
logEvent('Homepage: Saved views switched', {
|
||||
tab,
|
||||
});
|
||||
let currentViews: SavedviewtypesSavedViewDTO[] = [];
|
||||
let currentViews: ViewProps[] = [];
|
||||
if (tab === 'logs') {
|
||||
currentViews = logsViews;
|
||||
} else if (tab === 'traces') {
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import {
|
||||
SavedviewtypesPanelTypeDTO,
|
||||
SavedviewtypesSavedViewDTO,
|
||||
SavedviewtypesSchemaVersionDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { findSavedView, getSavedViewQuery, toSavedViewSource } from '../utils';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: (): string => 'test-id',
|
||||
}));
|
||||
|
||||
function makeView(): SavedviewtypesSavedViewDTO {
|
||||
return {
|
||||
id: 'view-1',
|
||||
name: 'errors-by-service-abc123',
|
||||
source: SavedviewtypesSourceDTO.traces,
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO.v2,
|
||||
createdBy: 'a@b.c',
|
||||
updatedBy: 'a@b.c',
|
||||
spec: {
|
||||
displayName: 'Errors by service',
|
||||
panelType: SavedviewtypesPanelTypeDTO.list,
|
||||
requestType: 'raw',
|
||||
queries: [
|
||||
{
|
||||
type: 'builder_query',
|
||||
spec: {
|
||||
name: 'A',
|
||||
signal: 'traces',
|
||||
stepInterval: 60,
|
||||
filter: { expression: 'has_error = true' },
|
||||
// v2 reads back fully defaulted envelopes; nulls must not break the mapper
|
||||
groupBy: null,
|
||||
order: null,
|
||||
selectFields: null,
|
||||
functions: null,
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
selectedFields: [{ name: 'service.name' }],
|
||||
display: { color: 'red' },
|
||||
},
|
||||
} as SavedviewtypesSavedViewDTO;
|
||||
}
|
||||
|
||||
describe('getSavedViewQuery', () => {
|
||||
it('maps the v2 spec through the v5 branch of mapQueryDataFromApi', () => {
|
||||
const query = getSavedViewQuery(makeView());
|
||||
|
||||
expect(query.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(query.promql).toStrictEqual([]);
|
||||
expect(query.clickhouse_sql).toStrictEqual([]);
|
||||
expect(query.builder.queryData).toHaveLength(1);
|
||||
|
||||
const [queryData] = query.builder.queryData;
|
||||
expect(queryData.queryName).toBe('A');
|
||||
expect(queryData.dataSource).toBe(DataSource.TRACES);
|
||||
expect(queryData.filter).toStrictEqual({ expression: 'has_error = true' });
|
||||
expect(queryData.groupBy).toStrictEqual([]);
|
||||
expect(queryData.orderBy).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('keeps formulas alongside builder queries', () => {
|
||||
const view = makeView();
|
||||
view.spec.queries.push({
|
||||
type: 'builder_formula',
|
||||
spec: { name: 'F1', expression: 'A / 2' },
|
||||
} as SavedviewtypesSavedViewDTO['spec']['queries'][number]);
|
||||
|
||||
const query = getSavedViewQuery(view);
|
||||
|
||||
expect(query.builder.queryData).toHaveLength(1);
|
||||
expect(query.builder.queryFormulas).toHaveLength(1);
|
||||
expect(query.builder.queryFormulas[0].queryName).toBe('F1');
|
||||
});
|
||||
|
||||
it('does not read the panel type into the query', () => {
|
||||
const view = makeView();
|
||||
view.spec.panelType = SavedviewtypesPanelTypeDTO.graph;
|
||||
|
||||
const query = getSavedViewQuery(view);
|
||||
|
||||
// panelType travels separately (url param), the Query itself has no such field
|
||||
expect(query).not.toHaveProperty('panelType', PANEL_TYPES.TIME_SERIES);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSavedViewSource', () => {
|
||||
it('maps every explorer source page to the v2 source', () => {
|
||||
expect(toSavedViewSource(DataSource.LOGS)).toBe(SavedviewtypesSourceDTO.logs);
|
||||
expect(toSavedViewSource(DataSource.TRACES)).toBe(
|
||||
SavedviewtypesSourceDTO.traces,
|
||||
);
|
||||
expect(toSavedViewSource(DataSource.METRICS)).toBe(
|
||||
SavedviewtypesSourceDTO.metrics,
|
||||
);
|
||||
expect(toSavedViewSource('meter')).toBe(SavedviewtypesSourceDTO.meter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findSavedView', () => {
|
||||
const views = [
|
||||
{ ...makeView(), id: 'a' },
|
||||
{ ...makeView(), id: 'b' },
|
||||
];
|
||||
|
||||
it('returns the view with the matching id', () => {
|
||||
expect(findSavedView(views, 'b')?.id).toBe('b');
|
||||
});
|
||||
|
||||
it('returns undefined when the id is not in the list', () => {
|
||||
expect(findSavedView(views, 'c')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for a null or not yet loaded list', () => {
|
||||
expect(findSavedView(null, 'a')).toBeUndefined();
|
||||
expect(findSavedView(undefined, 'a')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
SavedviewtypesSavedViewDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryEnvelope } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type SavedViewSourcePage = DataSource | 'meter';
|
||||
|
||||
// Explorers and the preferences module are keyed by DataSource (the signal),
|
||||
// the api keys views by source page. Same values today, so this is the one
|
||||
// place they meet. AI observability views will come with their own source and
|
||||
// DataSource cannot tell them apart from traces, so preferences should move to
|
||||
// source page at that point and this map goes with it.
|
||||
const SAVED_VIEW_SOURCE: Record<SavedViewSourcePage, SavedviewtypesSourceDTO> =
|
||||
{
|
||||
[DataSource.LOGS]: SavedviewtypesSourceDTO.logs,
|
||||
[DataSource.TRACES]: SavedviewtypesSourceDTO.traces,
|
||||
[DataSource.METRICS]: SavedviewtypesSourceDTO.metrics,
|
||||
meter: SavedviewtypesSourceDTO.meter,
|
||||
};
|
||||
|
||||
export function toSavedViewSource(
|
||||
sourcePage: SavedViewSourcePage,
|
||||
): SavedviewtypesSourceDTO {
|
||||
return SAVED_VIEW_SOURCE[sourcePage];
|
||||
}
|
||||
|
||||
// Explorers only save builder queries; v2 carries no queryType, so it is fixed here.
|
||||
export function getSavedViewQuery(view: SavedviewtypesSavedViewDTO): Query {
|
||||
const { queries, panelType } = view.spec;
|
||||
return mapQueryDataFromApi({
|
||||
queries: queries as QueryEnvelope[],
|
||||
panelType: panelType as unknown as PANEL_TYPES,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
unit: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function findSavedView(
|
||||
views: SavedviewtypesSavedViewDTO[] | null | undefined,
|
||||
id: string,
|
||||
): SavedviewtypesSavedViewDTO | undefined {
|
||||
return views?.find((view) => view.id === id);
|
||||
}
|
||||
@@ -1,18 +1,11 @@
|
||||
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
|
||||
import { invalidateListSavedViews } from 'api/generated/services/saved-view';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { deleteView } from 'api/saveView/deleteView';
|
||||
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const useDeleteView = (
|
||||
uuid: string,
|
||||
): UseMutationResult<DeleteViewPayloadProps, Error, string> => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
): UseMutationResult<DeleteViewPayloadProps, Error, string> =>
|
||||
useMutation({
|
||||
mutationKey: [uuid],
|
||||
mutationFn: () => deleteView(uuid),
|
||||
// v1 and v2 share storage; consumers already on v2 must see this write.
|
||||
// Temporary till the v1 client is deleted with the explorer bar.
|
||||
onSuccess: () => invalidateListSavedViews(queryClient),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
|
||||
import { invalidateListSavedViews } from 'api/generated/services/saved-view';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { saveView } from 'api/saveView/saveView';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
||||
@@ -14,14 +13,8 @@ export const useSaveView = ({
|
||||
Error,
|
||||
SaveViewProps,
|
||||
SaveViewPayloadProps
|
||||
> => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
> =>
|
||||
useMutation({
|
||||
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
|
||||
mutationFn: saveView,
|
||||
// v1 and v2 share storage; consumers already on v2 must see this write.
|
||||
// Temporary till the v1 client is deleted with the explorer bar.
|
||||
onSuccess: () => invalidateListSavedViews(queryClient),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
|
||||
import { invalidateListSavedViews } from 'api/generated/services/saved-view';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { updateView } from 'api/saveView/updateView';
|
||||
import {
|
||||
UpdateViewPayloadProps,
|
||||
@@ -17,10 +16,8 @@ export const useUpdateView = ({
|
||||
Error,
|
||||
UpdateViewProps,
|
||||
UpdateViewPayloadProps
|
||||
> => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
> =>
|
||||
useMutation({
|
||||
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
|
||||
mutationFn: () =>
|
||||
updateView({
|
||||
@@ -30,8 +27,4 @@ export const useUpdateView = ({
|
||||
sourcePage,
|
||||
viewKey,
|
||||
}),
|
||||
// v1 and v2 share storage; consumers already on v2 must see this write.
|
||||
// Temporary till the v1 client is deleted with the explorer bar.
|
||||
onSuccess: () => invalidateListSavedViews(queryClient),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -164,10 +164,10 @@ export const homeMocks = defineStoryMocks({
|
||||
),
|
||||
|
||||
rest.get(
|
||||
'http://localhost/api/v2/saved_views',
|
||||
'http://localhost/api/v1/explorer/views',
|
||||
response.json((req) => {
|
||||
const source = req.url.searchParams.get('source') ?? 'logs';
|
||||
const signal = isSavedViewSignal(source) ? source : 'logs';
|
||||
const sourcePage = req.url.searchParams.get('sourcePage') ?? 'logs';
|
||||
const signal = isSavedViewSignal(sourcePage) ? sourcePage : 'logs';
|
||||
|
||||
return savedViewsResponse(
|
||||
values.savedViewSignals.includes(signal) ? values.savedViews : 0,
|
||||
|
||||
@@ -6,21 +6,10 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import { checkListStepToPreferenceKeyMap } from 'container/Home/constants';
|
||||
import {
|
||||
type ListSavedViews200,
|
||||
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregationDTOSignal as LogsSignal,
|
||||
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTOSignal as MetricsSignal,
|
||||
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTOSignal as TracesSignal,
|
||||
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
|
||||
type Querybuildertypesv5QueryEnvelopeDTO,
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
type RuletypesRuleDTO,
|
||||
SavedviewtypesPanelTypeDTO,
|
||||
SavedviewtypesSchemaVersionDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ServiceDataProps } from 'api/metrics/getTopLevelOperations';
|
||||
import { alertRulesFixture } from 'mocks-server/__mockdata__/alert_rules';
|
||||
import { explorerView } from 'mocks-server/__mockdata__/explorer_views';
|
||||
import { defaultFeatureFlags } from 'tests/fixtures/appContextMock';
|
||||
import type { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
|
||||
import type { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||
@@ -176,53 +165,20 @@ const VIEW_NAMES: Record<SavedViewSignal, string[]> = {
|
||||
export const isSavedViewSignal = (value: string): value is SavedViewSignal =>
|
||||
SAVED_VIEW_SIGNALS.includes(value as SavedViewSignal);
|
||||
|
||||
const SAVED_VIEW_SOURCE: Record<SavedViewSignal, SavedviewtypesSourceDTO> = {
|
||||
logs: SavedviewtypesSourceDTO.logs,
|
||||
traces: SavedviewtypesSourceDTO.traces,
|
||||
metrics: SavedviewtypesSourceDTO.metrics,
|
||||
};
|
||||
|
||||
const SAVED_VIEW_QUERY: Record<
|
||||
SavedViewSignal,
|
||||
Querybuildertypesv5QueryEnvelopeDTO
|
||||
> = {
|
||||
logs: {
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
|
||||
spec: { name: 'A', signal: LogsSignal.logs },
|
||||
},
|
||||
traces: {
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
|
||||
spec: { name: 'A', signal: TracesSignal.traces },
|
||||
},
|
||||
metrics: {
|
||||
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
|
||||
spec: { name: 'A', signal: MetricsSignal.metrics },
|
||||
},
|
||||
};
|
||||
|
||||
export const savedViewsResponse = (
|
||||
count: number,
|
||||
signal: SavedViewSignal,
|
||||
): ListSavedViews200 => {
|
||||
const names = VIEW_NAMES[signal];
|
||||
sourcePage: SavedViewSignal,
|
||||
): Record<string, unknown> => {
|
||||
const names = VIEW_NAMES[sourcePage];
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
data: Array.from({ length: Math.min(count, names.length) }, (_, index) => ({
|
||||
id: `storybook-${signal}-view-${index + 1}`,
|
||||
name: `storybook-${signal}-view-${index + 1}`,
|
||||
source: SAVED_VIEW_SOURCE[signal],
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO.v2,
|
||||
createdAt: '2026-08-20T09:00:00Z',
|
||||
createdBy: 'storybook@signoz.io',
|
||||
updatedAt: '2026-08-20T09:00:00Z',
|
||||
updatedBy: 'storybook@signoz.io',
|
||||
spec: {
|
||||
displayName: names[index],
|
||||
panelType: SavedviewtypesPanelTypeDTO.list,
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.raw,
|
||||
queries: [SAVED_VIEW_QUERY[signal]],
|
||||
},
|
||||
...explorerView.data[0],
|
||||
id: `storybook-${sourcePage}-view-${index + 1}`,
|
||||
name: names[index],
|
||||
sourcePage,
|
||||
tags: [sourcePage],
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useListSavedViews } from 'api/generated/services/saved-view';
|
||||
import {
|
||||
SavedviewtypesSavedViewDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultTraceSelectedColumns,
|
||||
ensureLogsRequiredColumns,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { usePreferenceSync } from '../sync/usePreferenceSync';
|
||||
import { PreferenceMode } from '../types';
|
||||
|
||||
jest.mock('api/generated/services/saved-view');
|
||||
|
||||
const loaderPreferences = { columns: [{ name: 'from-loader' }] };
|
||||
jest.mock('../loader/usePreferenceLoader', () => ({
|
||||
usePreferenceLoader: jest.fn(() => ({
|
||||
preferences: loaderPreferences,
|
||||
loading: false,
|
||||
error: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../updater/usePreferenceUpdater', () => ({
|
||||
usePreferenceUpdater: jest.fn(() => ({
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockedUseListSavedViews = useListSavedViews as jest.MockedFunction<
|
||||
typeof useListSavedViews
|
||||
>;
|
||||
|
||||
function makeView(
|
||||
id: string,
|
||||
source: SavedviewtypesSourceDTO,
|
||||
spec: Partial<SavedviewtypesSavedViewDTO['spec']>,
|
||||
): SavedviewtypesSavedViewDTO {
|
||||
return {
|
||||
id,
|
||||
source,
|
||||
schemaVersion: 'v2',
|
||||
spec: {
|
||||
displayName: id,
|
||||
panelType: 'list',
|
||||
requestType: 'raw',
|
||||
queries: [],
|
||||
...spec,
|
||||
},
|
||||
} as unknown as SavedviewtypesSavedViewDTO;
|
||||
}
|
||||
|
||||
function mockViews(views: SavedviewtypesSavedViewDTO[]): void {
|
||||
mockedUseListSavedViews.mockReturnValue({
|
||||
data: { status: 'success', data: views },
|
||||
} as unknown as ReturnType<typeof useListSavedViews>);
|
||||
}
|
||||
|
||||
describe('usePreferenceSync in saved view mode', () => {
|
||||
beforeEach(() => {
|
||||
mockedUseListSavedViews.mockReset();
|
||||
});
|
||||
|
||||
it('fetches the list for the data source only in saved view mode', () => {
|
||||
mockViews([]);
|
||||
|
||||
renderHook(() =>
|
||||
usePreferenceSync({
|
||||
mode: PreferenceMode.DIRECT,
|
||||
dataSource: DataSource.LOGS,
|
||||
savedViewId: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockedUseListSavedViews).toHaveBeenCalledWith(
|
||||
{ source: 'logs' },
|
||||
{ query: { enabled: false } },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns loader preferences outside saved view mode', () => {
|
||||
mockViews([]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceSync({
|
||||
mode: PreferenceMode.DIRECT,
|
||||
dataSource: DataSource.LOGS,
|
||||
savedViewId: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.preferences).toBe(loaderPreferences);
|
||||
});
|
||||
|
||||
it('applies selectedFields and display of the active logs view', () => {
|
||||
mockViews([
|
||||
makeView('view-1', SavedviewtypesSourceDTO.logs, {
|
||||
selectedFields: [{ name: 'service.name' }, { name: 'body' }],
|
||||
display: { maxLines: 3, format: 'raw', fontSize: 'large', color: 'red' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceSync({
|
||||
mode: PreferenceMode.SAVED_VIEW,
|
||||
dataSource: DataSource.LOGS,
|
||||
savedViewId: 'view-1',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.preferences?.columns).toStrictEqual(
|
||||
ensureLogsRequiredColumns([{ name: 'service.name' }, { name: 'body' }]),
|
||||
);
|
||||
expect(result.current.preferences?.formatting).toStrictEqual({
|
||||
maxLines: 3,
|
||||
format: 'raw',
|
||||
fontSize: 'large',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to defaults when the view has zero-valued display and no fields', () => {
|
||||
mockViews([
|
||||
makeView('view-1', SavedviewtypesSourceDTO.logs, {
|
||||
selectedFields: undefined,
|
||||
display: { maxLines: 0, format: '', fontSize: '', color: '' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceSync({
|
||||
mode: PreferenceMode.SAVED_VIEW,
|
||||
dataSource: DataSource.LOGS,
|
||||
savedViewId: 'view-1',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.preferences?.columns).toStrictEqual(
|
||||
ensureLogsRequiredColumns(defaultLogsSelectedColumns),
|
||||
);
|
||||
expect(result.current.preferences?.formatting).toStrictEqual({
|
||||
maxLines: 1,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes trace selectedFields through and defaults when absent', () => {
|
||||
mockViews([
|
||||
makeView('with-fields', SavedviewtypesSourceDTO.traces, {
|
||||
selectedFields: [{ name: 'name' }, { name: 'durationNano' }],
|
||||
}),
|
||||
makeView('without-fields', SavedviewtypesSourceDTO.traces, {}),
|
||||
]);
|
||||
|
||||
const withFields = renderHook(() =>
|
||||
usePreferenceSync({
|
||||
mode: PreferenceMode.SAVED_VIEW,
|
||||
dataSource: DataSource.TRACES,
|
||||
savedViewId: 'with-fields',
|
||||
}),
|
||||
);
|
||||
const withoutFields = renderHook(() =>
|
||||
usePreferenceSync({
|
||||
mode: PreferenceMode.SAVED_VIEW,
|
||||
dataSource: DataSource.TRACES,
|
||||
savedViewId: 'without-fields',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(withFields.result.current.preferences?.columns).toStrictEqual([
|
||||
{ name: 'name' },
|
||||
{ name: 'durationNano' },
|
||||
]);
|
||||
expect(withFields.result.current.preferences?.formatting).toBeUndefined();
|
||||
expect(withoutFields.result.current.preferences?.columns).toBe(
|
||||
defaultTraceSelectedColumns,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses defaults when the saved view id is not in the list', () => {
|
||||
mockViews([makeView('other', SavedviewtypesSourceDTO.logs, {})]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceSync({
|
||||
mode: PreferenceMode.SAVED_VIEW,
|
||||
dataSource: DataSource.LOGS,
|
||||
savedViewId: 'missing',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.preferences?.columns).toStrictEqual(
|
||||
ensureLogsRequiredColumns(defaultLogsSelectedColumns),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,12 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useListSavedViews } from 'api/generated/services/saved-view';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultTraceSelectedColumns,
|
||||
ensureLogsRequiredColumns,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { FontSize, LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { findSavedView, toSavedViewSource } from 'container/SavedViews/utils';
|
||||
import { defaultSelectedColumns as defaultTracesSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
|
||||
@@ -30,16 +28,16 @@ export function usePreferenceSync({
|
||||
updateColumns: (newColumns: TelemetryFieldKey[]) => void;
|
||||
updateFormatting: (newFormatting: FormattingOptions) => void;
|
||||
} {
|
||||
const { data: viewsData } = useListSavedViews(
|
||||
{ source: toSavedViewSource(dataSource) },
|
||||
{ query: { enabled: mode === PreferenceMode.SAVED_VIEW } },
|
||||
const { data: viewsData } = useGetAllViews(
|
||||
dataSource,
|
||||
mode === PreferenceMode.SAVED_VIEW,
|
||||
);
|
||||
|
||||
const [savedViewPreferences, setSavedViewPreferences] =
|
||||
useState<Preferences | null>(null);
|
||||
|
||||
const withColumnNames = (
|
||||
columns: TelemetryFieldKey[] | undefined,
|
||||
const updateExtraDataSelectColumns = (
|
||||
columns: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] | null => {
|
||||
if (!columns) {
|
||||
return null;
|
||||
@@ -51,28 +49,27 @@ export function usePreferenceSync({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const spec = savedViewId
|
||||
? findSavedView(viewsData?.data, savedViewId)?.spec
|
||||
: undefined;
|
||||
const selectedFields = spec?.selectedFields as
|
||||
| TelemetryFieldKey[]
|
||||
| undefined;
|
||||
const extraData = viewsData?.data?.data?.find(
|
||||
(view) => view.id === savedViewId,
|
||||
)?.extraData;
|
||||
|
||||
const parsedExtraData = JSON.parse(extraData || '{}');
|
||||
let columns: TelemetryFieldKey[] = [];
|
||||
let formatting: FormattingOptions | undefined;
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
columns = ensureLogsRequiredColumns(
|
||||
withColumnNames(selectedFields) || defaultLogsSelectedColumns,
|
||||
updateExtraDataSelectColumns(parsedExtraData?.selectColumns) ||
|
||||
defaultLogsSelectedColumns,
|
||||
);
|
||||
formatting = {
|
||||
maxLines: spec?.display?.maxLines || 1,
|
||||
format: (spec?.display?.format as LogViewMode) || 'table',
|
||||
fontSize: (spec?.display?.fontSize as FontSize) || FontSize.SMALL,
|
||||
version: 1,
|
||||
maxLines: parsedExtraData?.maxLines ?? 1,
|
||||
format: parsedExtraData?.format ?? 'table',
|
||||
fontSize: parsedExtraData?.fontSize ?? 'small',
|
||||
version: parsedExtraData?.version ?? 1,
|
||||
};
|
||||
}
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
columns = selectedFields || defaultTraceSelectedColumns;
|
||||
columns = parsedExtraData?.selectColumns || defaultTracesSelectedColumns;
|
||||
}
|
||||
setSavedViewPreferences({ columns, formatting });
|
||||
}, [viewsData, dataSource, savedViewId, mode]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 3,
|
||||
"version": 2,
|
||||
"definition": {
|
||||
"schemaVersion": "v6",
|
||||
"name": "signoz---ai-o11y-overview",
|
||||
@@ -437,7 +437,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -489,7 +489,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -722,7 +722,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -782,7 +782,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -850,7 +850,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -868,7 +868,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -886,7 +886,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -904,7 +904,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -961,7 +961,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -979,7 +979,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -997,7 +997,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -1015,7 +1015,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -1076,7 +1076,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": true,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -1175,7 +1175,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": true,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -1192,7 +1192,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": true,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -1248,7 +1248,7 @@
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_ai_query",
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
@@ -1282,7 +1282,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_ai_query",
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "B",
|
||||
"signal": "traces",
|
||||
@@ -1353,7 +1353,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -2075,7 +2075,7 @@
|
||||
"stepInterval": 0,
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "gen_ai.request.model EXISTS AND gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
"expression": "gen_ai.request.model IN $model AND gen_ai.provider.name IN $provider AND deployment.environment IN $environment AND service.name IN $service_name"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
@@ -2724,7 +2724,7 @@
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_ai_query",
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 1,
|
||||
"definition": {
|
||||
"name": "gen_ai.agent",
|
||||
"condition": {
|
||||
@@ -68,7 +68,7 @@
|
||||
{
|
||||
"key": "final_result",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 10
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 3,
|
||||
"version": 2,
|
||||
"definition": {
|
||||
"name": "gen_ai.llm",
|
||||
"condition": {
|
||||
@@ -250,19 +250,19 @@
|
||||
{
|
||||
"key": "gen_ai.prompt",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 30
|
||||
},
|
||||
{
|
||||
"key": "ai.prompt.messages",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 20
|
||||
},
|
||||
{
|
||||
"key": "input.value",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 10
|
||||
}
|
||||
]
|
||||
@@ -276,25 +276,25 @@
|
||||
{
|
||||
"key": "gen_ai.completion",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 30
|
||||
},
|
||||
{
|
||||
"key": "ai.response.toolCalls",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 25
|
||||
},
|
||||
{
|
||||
"key": "ai.response.text",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 20
|
||||
},
|
||||
{
|
||||
"key": "output.value",
|
||||
"context": "attribute",
|
||||
"operation": "move",
|
||||
"operation": "copy",
|
||||
"priority": 10
|
||||
}
|
||||
]
|
||||
|
||||
@@ -105,6 +105,17 @@ func (m *Manager) RecommendAgentConfig(orgId valuer.UUID, currentConfYaml []byte
|
||||
configId string,
|
||||
err error,
|
||||
) {
|
||||
return m.recommendAgentConfig(orgId, currentConfYaml, true)
|
||||
}
|
||||
|
||||
// Implements opamp.AgentConfigProvider
|
||||
func (m *Manager) PreviewAgentConfig(orgId valuer.UUID, currentConfYaml []byte) ([]byte, error) {
|
||||
recommendation, _, err := m.recommendAgentConfig(orgId, currentConfYaml, false)
|
||||
return recommendation, err
|
||||
}
|
||||
|
||||
func (m *Manager) recommendAgentConfig(orgId valuer.UUID, currentConfYaml []byte, recordDeployment bool) ([]byte, string, error) {
|
||||
var configId string
|
||||
recommendation := currentConfYaml
|
||||
settingVersionsUsed := []string{}
|
||||
|
||||
@@ -134,6 +145,9 @@ func (m *Manager) RecommendAgentConfig(orgId valuer.UUID, currentConfYaml []byte
|
||||
|
||||
settingVersionsUsed = append(settingVersionsUsed, configId)
|
||||
|
||||
if !recordDeployment {
|
||||
continue
|
||||
}
|
||||
_ = m.updateDeployStatus(
|
||||
context.Background(),
|
||||
orgId,
|
||||
|
||||
@@ -239,7 +239,7 @@ func (ic *LogParsingPipelineController) getNormalizePipeline() pipelinetypes.Get
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
ID: "normalize_body_default",
|
||||
Type: "normalize",
|
||||
Enabled: true,
|
||||
If: "body != nil",
|
||||
|
||||
@@ -128,6 +128,12 @@ func (ta *MockAgentConfigProvider) HasReportedDeploymentStatus(orgID valuer.UUID
|
||||
return exists
|
||||
}
|
||||
|
||||
// AgentConfigProvider interface
|
||||
func (ta *MockAgentConfigProvider) PreviewAgentConfig(orgId valuer.UUID, baseConfYaml []byte) ([]byte, error) {
|
||||
recommendedYaml, _, err := ta.RecommendAgentConfig(orgId, baseConfYaml)
|
||||
return recommendedYaml, err
|
||||
}
|
||||
|
||||
// AgentConfigProvider interface
|
||||
func (ta *MockAgentConfigProvider) GetDeployStatusByHash(_ context.Context, _ valuer.UUID, _ string) (opamptypes.DeployStatus, error) {
|
||||
return opamptypes.DeployStatusUnknown, nil
|
||||
|
||||
@@ -24,6 +24,8 @@ type Agent struct {
|
||||
remoteConfig *protobufs.AgentRemoteConfig
|
||||
Status *protobufs.AgentToServer
|
||||
|
||||
reconnectConfigChecked bool
|
||||
|
||||
// can this agent be load balancer
|
||||
CanLB bool
|
||||
|
||||
@@ -291,6 +293,11 @@ func (agent *Agent) processStatusUpdate(
|
||||
|
||||
// We need to recalculate the config.
|
||||
configChanged = agent.updateRemoteConfig(configProvider)
|
||||
} else if agent.remoteConfig == nil && !agent.reconnectConfigChecked && agent.Config != "" {
|
||||
// A running agent reconnected after a server restart; settings may have
|
||||
// changed while it was away (e.g. startup reconciliation).
|
||||
agent.reconnectConfigChecked = true
|
||||
configChanged = agent.updateRemoteConfigIfStale(configProvider)
|
||||
}
|
||||
|
||||
// If remote config is changed and different from what the Agent has then
|
||||
@@ -312,6 +319,20 @@ func (agent *Agent) processStatusUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
// updateRemoteConfigIfStale records a deployment only when the recommendation
|
||||
// differs from the agent's effective config.
|
||||
func (agent *Agent) updateRemoteConfigIfStale(configProvider AgentConfigProvider) bool {
|
||||
recommendedConfig, err := configProvider.PreviewAgentConfig(agent.OrgID, []byte(agent.Config))
|
||||
if err != nil {
|
||||
agent.logger.Error("could not preview config recommendation for agent", "agent_id", agent.AgentID, errors.Attr(err))
|
||||
return false
|
||||
}
|
||||
if string(recommendedConfig) == agent.Config {
|
||||
return false
|
||||
}
|
||||
return agent.updateRemoteConfig(configProvider)
|
||||
}
|
||||
|
||||
func (agent *Agent) updateRemoteConfig(configProvider AgentConfigProvider) bool {
|
||||
recommendedConfig, confId, err := configProvider.RecommendAgentConfig(agent.OrgID, []byte(agent.Config))
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,10 @@ type AgentConfigProvider interface {
|
||||
err error,
|
||||
)
|
||||
|
||||
// PreviewAgentConfig returns the config RecommendAgentConfig would, without
|
||||
// recording a deployment.
|
||||
PreviewAgentConfig(orgId valuer.UUID, currentConfYaml []byte) ([]byte, error)
|
||||
|
||||
// Report deployment status for config recommendations generated by RecommendAgentConfig
|
||||
ReportConfigDeploymentStatus(
|
||||
orgId valuer.UUID,
|
||||
|
||||
Reference in New Issue
Block a user