mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-06 18:40:32 +01:00
Compare commits
3 Commits
v0.87.0-pa
...
chore/anal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2af2a1cf4 | ||
|
|
d901b60aa4 | ||
|
|
d85a1a21ac |
@@ -1,5 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -52,11 +53,32 @@ jest.mock('hooks/saveViews/useDeleteView', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ExplorerCard', () => {
|
||||
it('renders a card with a title and a description', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||
@@ -65,7 +87,9 @@ describe('ExplorerCard', () => {
|
||||
it('renders a save view button', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
@@ -106,7 +107,11 @@ export const isQueryUpdatedInView = ({
|
||||
!isEqual(
|
||||
options?.selectColumns,
|
||||
extraData && JSON.parse(extraData)?.selectColumns,
|
||||
)
|
||||
) ||
|
||||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
|
||||
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
|
||||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
|
||||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { FormattingOptions } from 'providers/preferences/types';
|
||||
import {
|
||||
CSSProperties,
|
||||
Dispatch,
|
||||
@@ -270,17 +271,26 @@ function ExplorerOptions({
|
||||
const getUpdatedExtraData = (
|
||||
extraData: string | undefined,
|
||||
newSelectedColumns: BaseAutocompleteData[],
|
||||
formattingOptions?: FormattingOptions,
|
||||
): string => {
|
||||
let updatedExtraData;
|
||||
|
||||
if (extraData) {
|
||||
const parsedExtraData = JSON.parse(extraData);
|
||||
parsedExtraData.selectColumns = newSelectedColumns;
|
||||
if (formattingOptions) {
|
||||
parsedExtraData.format = formattingOptions.format;
|
||||
parsedExtraData.maxLines = formattingOptions.maxLines;
|
||||
parsedExtraData.fontSize = formattingOptions.fontSize;
|
||||
}
|
||||
updatedExtraData = JSON.stringify(parsedExtraData);
|
||||
} else {
|
||||
updatedExtraData = JSON.stringify({
|
||||
color: Color.BG_SIENNA_500,
|
||||
selectColumns: newSelectedColumns,
|
||||
format: formattingOptions?.format,
|
||||
maxLines: formattingOptions?.maxLines,
|
||||
fontSize: formattingOptions?.fontSize,
|
||||
});
|
||||
}
|
||||
return updatedExtraData;
|
||||
@@ -289,6 +299,14 @@ function ExplorerOptions({
|
||||
const updatedExtraData = getUpdatedExtraData(
|
||||
extraData,
|
||||
options?.selectColumns,
|
||||
// pass this only for logs
|
||||
sourcepage === DataSource.LOGS
|
||||
? {
|
||||
format: options?.format,
|
||||
maxLines: options?.maxLines,
|
||||
fontSize: options?.fontSize,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -517,6 +535,14 @@ function ExplorerOptions({
|
||||
color,
|
||||
selectColumns: options.selectColumns,
|
||||
version: 1,
|
||||
...// pass this only for logs
|
||||
(sourcepage === DataSource.LOGS
|
||||
? {
|
||||
format: options?.format,
|
||||
maxLines: options?.maxLines,
|
||||
fontSize: options?.fontSize,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
notifications,
|
||||
panelType: panelType || PANEL_TYPES.LIST,
|
||||
|
||||
@@ -114,7 +114,6 @@ function LogsExplorerViews({
|
||||
|
||||
// Context
|
||||
const {
|
||||
initialDataSource,
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
@@ -144,7 +143,7 @@ function LogsExplorerViews({
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: initialDataSource || DataSource.LOGS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_qu
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { fireEvent, render, RenderResult } from 'tests/test-utils';
|
||||
@@ -87,6 +88,25 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/logs/useCopyLogLink', () => ({
|
||||
useCopyLogLink: jest.fn().mockReturnValue({
|
||||
activeLogId: ACTIVE_LOG_ID,
|
||||
@@ -105,13 +125,15 @@ const renderer = (): RenderResult =>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
);
|
||||
|
||||
@@ -184,13 +206,15 @@ describe('LogsExplorerViews -', () => {
|
||||
lodsQueryServerRequest();
|
||||
render(
|
||||
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'ReactI18';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
@@ -108,11 +109,13 @@ describe('LogsPanelComponent', () => {
|
||||
render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<DashboardProvider>
|
||||
<NewWidget
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
fillSpans={undefined}
|
||||
yAxisUnit={undefined}
|
||||
/>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
fillSpans={undefined}
|
||||
yAxisUnit={undefined}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</DashboardProvider>
|
||||
</I18nextProvider>,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
@@ -11,6 +8,7 @@ import {
|
||||
AllTraceFilterKeys,
|
||||
AllTraceFilterKeyValue,
|
||||
} from 'pages/TracesExplorer/Filter/filterUtils';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -35,10 +33,10 @@ import {
|
||||
import { getOptionsFromKeys } from './utils';
|
||||
|
||||
interface UseOptionsMenuProps {
|
||||
storageKey?: string;
|
||||
dataSource: DataSource;
|
||||
aggregateOperator: string;
|
||||
initialOptions?: InitialOptions;
|
||||
storageKey: LOCALSTORAGE;
|
||||
}
|
||||
|
||||
interface UseOptionsMenu {
|
||||
@@ -48,22 +46,21 @@ interface UseOptionsMenu {
|
||||
}
|
||||
|
||||
const useOptionsMenu = ({
|
||||
storageKey,
|
||||
dataSource,
|
||||
aggregateOperator,
|
||||
initialOptions = {},
|
||||
}: UseOptionsMenuProps): UseOptionsMenu => {
|
||||
const { notifications } = useNotifications();
|
||||
const {
|
||||
preferences,
|
||||
updateColumns,
|
||||
updateFormatting,
|
||||
} = usePreferenceContext();
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||
const debouncedSearchText = useDebounce(searchText, 300);
|
||||
|
||||
const localStorageOptionsQuery = useMemo(
|
||||
() => getFromLocalstorage(storageKey),
|
||||
[storageKey],
|
||||
);
|
||||
|
||||
const initialQueryParams = useMemo(
|
||||
() => ({
|
||||
searchText: '',
|
||||
@@ -77,7 +74,6 @@ const useOptionsMenu = ({
|
||||
|
||||
const {
|
||||
query: optionsQuery,
|
||||
queryData: optionsQueryData,
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
|
||||
@@ -105,7 +101,9 @@ const useOptionsMenu = ({
|
||||
);
|
||||
|
||||
const initialSelectedColumns = useMemo(() => {
|
||||
if (!isFetchedInitialAttributes) return [];
|
||||
if (!isFetchedInitialAttributes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const attributesData = initialAttributesResult?.reduce(
|
||||
(acc, attributeResponse) => {
|
||||
@@ -142,14 +140,12 @@ const useOptionsMenu = ({
|
||||
})
|
||||
.filter(Boolean) as BaseAutocompleteData[];
|
||||
|
||||
// this is the last point where we can set the default columns and if uptil now also we have an empty array then we will set the default columns
|
||||
if (!initialSelected || !initialSelected?.length) {
|
||||
initialSelected = defaultTraceSelectedColumns;
|
||||
}
|
||||
}
|
||||
|
||||
return initialSelected || [];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isFetchedInitialAttributes,
|
||||
initialOptions?.selectColumns,
|
||||
@@ -171,7 +167,6 @@ const useOptionsMenu = ({
|
||||
const searchedAttributeKeys = useMemo(() => {
|
||||
if (searchedAttributesData?.payload?.attributeKeys?.length) {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
// add timestamp and body to the list of attributes
|
||||
return [
|
||||
...defaultLogsSelectedColumns,
|
||||
...searchedAttributesData.payload.attributeKeys.filter(
|
||||
@@ -188,32 +183,35 @@ const useOptionsMenu = ({
|
||||
return [];
|
||||
}, [dataSource, searchedAttributesData?.payload?.attributeKeys]);
|
||||
|
||||
const initialOptionsQuery: OptionsQuery = useMemo(
|
||||
() => ({
|
||||
const initialOptionsQuery: OptionsQuery = useMemo(() => {
|
||||
let defaultColumns = defaultOptionsQuery.selectColumns;
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
defaultColumns = defaultTraceSelectedColumns;
|
||||
} else if (dataSource === DataSource.LOGS) {
|
||||
defaultColumns = defaultLogsSelectedColumns;
|
||||
}
|
||||
|
||||
const finalSelectColumns = initialOptions?.selectColumns
|
||||
? initialSelectedColumns
|
||||
: defaultColumns;
|
||||
|
||||
return {
|
||||
...defaultOptionsQuery,
|
||||
...initialOptions,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
selectColumns: initialOptions?.selectColumns
|
||||
? initialSelectedColumns
|
||||
: dataSource === DataSource.TRACES
|
||||
? defaultTraceSelectedColumns
|
||||
: defaultOptionsQuery.selectColumns,
|
||||
}),
|
||||
[dataSource, initialOptions, initialSelectedColumns],
|
||||
);
|
||||
selectColumns: finalSelectColumns,
|
||||
};
|
||||
}, [dataSource, initialOptions, initialSelectedColumns]);
|
||||
|
||||
const selectedColumnKeys = useMemo(
|
||||
() => optionsQueryData?.selectColumns?.map(({ id }) => id) || [],
|
||||
[optionsQueryData],
|
||||
() => preferences?.columns?.map(({ id }) => id) || [],
|
||||
[preferences?.columns],
|
||||
);
|
||||
|
||||
const optionsFromAttributeKeys = useMemo(() => {
|
||||
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
|
||||
// For other data sources, only filter out 'body' if it exists
|
||||
if (dataSource !== DataSource.LOGS) {
|
||||
return item.key !== 'body';
|
||||
}
|
||||
// For LOGS, keep all keys
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -223,10 +221,8 @@ const useOptionsMenu = ({
|
||||
const handleRedirectWithOptionsData = useCallback(
|
||||
(newQueryData: OptionsQuery) => {
|
||||
redirectWithOptionsData(newQueryData);
|
||||
|
||||
setToLocalstorage(storageKey, JSON.stringify(newQueryData));
|
||||
},
|
||||
[storageKey, redirectWithOptionsData],
|
||||
[redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleSelectColumns = useCallback(
|
||||
@@ -235,7 +231,7 @@ const useOptionsMenu = ({
|
||||
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
|
||||
const column = [
|
||||
...searchedAttributeKeys,
|
||||
...optionsQueryData.selectColumns,
|
||||
...(preferences?.columns || []),
|
||||
].find(({ id }) => id === key);
|
||||
|
||||
if (!column) return acc;
|
||||
@@ -243,75 +239,116 @@ const useOptionsMenu = ({
|
||||
}, [] as BaseAutocompleteData[]);
|
||||
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newSelectedColumns,
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
};
|
||||
|
||||
updateColumns(newSelectedColumns);
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
},
|
||||
[
|
||||
searchedAttributeKeys,
|
||||
selectedColumnKeys,
|
||||
optionsQueryData,
|
||||
preferences,
|
||||
handleRedirectWithOptionsData,
|
||||
updateColumns,
|
||||
],
|
||||
);
|
||||
|
||||
const handleRemoveSelectedColumn = useCallback(
|
||||
(columnKey: string) => {
|
||||
const newSelectedColumns = optionsQueryData?.selectColumns?.filter(
|
||||
const newSelectedColumns = preferences?.columns?.filter(
|
||||
({ id }) => id !== columnKey,
|
||||
);
|
||||
|
||||
if (!newSelectedColumns.length && dataSource !== DataSource.LOGS) {
|
||||
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
|
||||
notifications.error({
|
||||
message: 'There must be at least one selected column',
|
||||
});
|
||||
} else {
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
selectColumns: newSelectedColumns,
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newSelectedColumns || [],
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines:
|
||||
preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize:
|
||||
preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
};
|
||||
|
||||
updateColumns(newSelectedColumns || []);
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
}
|
||||
},
|
||||
[dataSource, notifications, optionsQueryData, handleRedirectWithOptionsData],
|
||||
[
|
||||
dataSource,
|
||||
notifications,
|
||||
preferences,
|
||||
handleRedirectWithOptionsData,
|
||||
updateColumns,
|
||||
],
|
||||
);
|
||||
|
||||
const handleFormatChange = useCallback(
|
||||
(value: LogViewMode) => {
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: preferences?.columns || [],
|
||||
format: value,
|
||||
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
};
|
||||
|
||||
updateFormatting({
|
||||
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
format: value,
|
||||
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
});
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
},
|
||||
[handleRedirectWithOptionsData, optionsQueryData],
|
||||
[handleRedirectWithOptionsData, preferences, updateFormatting],
|
||||
);
|
||||
|
||||
const handleMaxLinesChange = useCallback(
|
||||
(value: string | number | null) => {
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: preferences?.columns || [],
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines: value as number,
|
||||
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
};
|
||||
|
||||
updateFormatting({
|
||||
maxLines: value as number,
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
});
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
},
|
||||
[handleRedirectWithOptionsData, optionsQueryData],
|
||||
[handleRedirectWithOptionsData, preferences, updateFormatting],
|
||||
);
|
||||
|
||||
const handleFontSizeChange = useCallback(
|
||||
(value: FontSize) => {
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: preferences?.columns || [],
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize: value,
|
||||
};
|
||||
|
||||
updateFormatting({
|
||||
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
fontSize: value,
|
||||
});
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
},
|
||||
[handleRedirectWithOptionsData, optionsQueryData],
|
||||
[handleRedirectWithOptionsData, preferences, updateFormatting],
|
||||
);
|
||||
|
||||
const handleSearchAttribute = useCallback((value: string) => {
|
||||
@@ -331,7 +368,7 @@ const useOptionsMenu = ({
|
||||
() => ({
|
||||
addColumn: {
|
||||
isFetching: isSearchedAttributesFetching,
|
||||
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
|
||||
value: preferences?.columns || defaultOptionsQuery.selectColumns,
|
||||
options: optionsFromAttributeKeys || [],
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
@@ -340,24 +377,21 @@ const useOptionsMenu = ({
|
||||
onSearch: handleSearchAttribute,
|
||||
},
|
||||
format: {
|
||||
value: optionsQueryData.format || defaultOptionsQuery.format,
|
||||
value: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
onChange: handleFormatChange,
|
||||
},
|
||||
maxLines: {
|
||||
value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines,
|
||||
value: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
onChange: handleMaxLinesChange,
|
||||
},
|
||||
fontSize: {
|
||||
value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize,
|
||||
value: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
onChange: handleFontSizeChange,
|
||||
},
|
||||
}),
|
||||
[
|
||||
isSearchedAttributesFetching,
|
||||
optionsQueryData?.selectColumns,
|
||||
optionsQueryData.format,
|
||||
optionsQueryData.maxLines,
|
||||
optionsQueryData?.fontSize,
|
||||
preferences,
|
||||
optionsFromAttributeKeys,
|
||||
handleSelectColumns,
|
||||
handleRemoveSelectedColumn,
|
||||
@@ -369,23 +403,25 @@ const useOptionsMenu = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (optionsQuery || !isFetchedInitialAttributes) return;
|
||||
if (optionsQuery || !isFetchedInitialAttributes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextOptionsQuery = localStorageOptionsQuery
|
||||
? JSON.parse(localStorageOptionsQuery)
|
||||
: initialOptionsQuery;
|
||||
|
||||
redirectWithOptionsData(nextOptionsQuery);
|
||||
redirectWithOptionsData(initialOptionsQuery);
|
||||
}, [
|
||||
isFetchedInitialAttributes,
|
||||
optionsQuery,
|
||||
initialOptionsQuery,
|
||||
localStorageOptionsQuery,
|
||||
redirectWithOptionsData,
|
||||
]);
|
||||
|
||||
return {
|
||||
options: optionsQueryData,
|
||||
options: {
|
||||
selectColumns: preferences?.columns || [],
|
||||
format: preferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
},
|
||||
config: optionsMenuConfig,
|
||||
handleOptionsChange: handleRedirectWithOptionsData,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { screen } from '@testing-library/react';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { findByText, fireEvent, render, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
||||
@@ -19,6 +20,18 @@ jest.mock('uplot', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useUrlQuery hook
|
||||
const mockUrlQuery = {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
toString: jest.fn(() => ''),
|
||||
};
|
||||
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => mockUrlQuery),
|
||||
}));
|
||||
|
||||
const samplePipelinePreviewResponse = {
|
||||
isLoading: false,
|
||||
logs: [
|
||||
@@ -57,17 +70,38 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
it('should render PipelineListsView section', () => {
|
||||
const { getByText, container } = render(
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="viewing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>,
|
||||
<PreferenceContextProvider>
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="viewing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
// table headers assertions
|
||||
@@ -91,14 +125,16 @@ describe('PipelinePage container test', () => {
|
||||
|
||||
it('should render expanded content and edit mode correctly', async () => {
|
||||
const { getByText } = render(
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>,
|
||||
<PreferenceContextProvider>
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
// content assertion
|
||||
@@ -122,14 +158,16 @@ describe('PipelinePage container test', () => {
|
||||
|
||||
it('should be able to perform actions and edit on expanded view content', async () => {
|
||||
render(
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>,
|
||||
<PreferenceContextProvider>
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
// content assertion
|
||||
@@ -180,14 +218,16 @@ describe('PipelinePage container test', () => {
|
||||
|
||||
it('should be able to toggle and delete pipeline', async () => {
|
||||
const { getByText } = render(
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>,
|
||||
<PreferenceContextProvider>
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType=""
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
const addNewPipelineBtn = getByText('add_new_pipeline');
|
||||
@@ -247,14 +287,16 @@ describe('PipelinePage container test', () => {
|
||||
|
||||
it('should have populated form fields when edit pipeline is clicked', async () => {
|
||||
render(
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType="edit-pipeline"
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>,
|
||||
<PreferenceContextProvider>
|
||||
<PipelineListsView
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="editing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
isActionType="edit-pipeline"
|
||||
refetchPipelineLists={jest.fn()}
|
||||
/>
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
// content assertion
|
||||
|
||||
@@ -4,6 +4,7 @@ import LiveLogsContainer from 'container/LiveLogs/LiveLogsContainer';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { EventSourceProvider } from 'providers/EventSource';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useEffect } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -17,7 +18,9 @@ function LiveLogs(): JSX.Element {
|
||||
|
||||
return (
|
||||
<EventSourceProvider>
|
||||
<LiveLogsContainer />
|
||||
<PreferenceContextProvider>
|
||||
<LiveLogsContainer />
|
||||
</PreferenceContextProvider>
|
||||
</EventSourceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { noop } from 'lodash-es';
|
||||
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
// https://virtuoso.dev/mocking-in-tests/
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
@@ -73,6 +74,25 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const logsQueryServerRequest = (): void =>
|
||||
server.use(
|
||||
rest.post(queryRangeURL, (req, res, ctx) =>
|
||||
@@ -88,7 +108,11 @@ describe('Logs Explorer Tests', () => {
|
||||
queryByText,
|
||||
getByTestId,
|
||||
queryByTestId,
|
||||
} = render(<LogsExplorer />);
|
||||
} = render(
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorer />
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
// check the presence of frequency chart content
|
||||
expect(getByText(frequencyChartContent)).toBeInTheDocument();
|
||||
@@ -124,11 +148,13 @@ describe('Logs Explorer Tests', () => {
|
||||
// mocking the query range API to return the logs
|
||||
logsQueryServerRequest();
|
||||
const { queryByText, queryByTestId } = render(
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorer />
|
||||
</VirtuosoMockContext.Provider>,
|
||||
<PreferenceContextProvider>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorer />
|
||||
</VirtuosoMockContext.Provider>
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
// check for loading state to be not present
|
||||
@@ -192,11 +218,13 @@ describe('Logs Explorer Tests', () => {
|
||||
isStagedQueryUpdated: (): boolean => false,
|
||||
}}
|
||||
>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorer />
|
||||
</VirtuosoMockContext.Provider>
|
||||
<PreferenceContextProvider>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorer />
|
||||
</VirtuosoMockContext.Provider>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
@@ -213,7 +241,11 @@ describe('Logs Explorer Tests', () => {
|
||||
});
|
||||
|
||||
test('frequency chart visibility and switch toggle', async () => {
|
||||
const { getByRole, queryByText } = render(<LogsExplorer />);
|
||||
const { getByRole, queryByText } = render(
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorer />
|
||||
</PreferenceContextProvider>,
|
||||
);
|
||||
|
||||
// check the presence of Frequency Chart
|
||||
expect(queryByText('Frequency chart')).toBeInTheDocument();
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEqual, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -35,6 +36,8 @@ function LogsExplorer(): JSX.Element {
|
||||
const [selectedView, setSelectedView] = useState<SELECTED_VIEWS>(
|
||||
SELECTED_VIEWS.SEARCH,
|
||||
);
|
||||
const { preferences, loading: preferencesLoading } = usePreferenceContext();
|
||||
|
||||
const [showFilters, setShowFilters] = useState<boolean>(() => {
|
||||
const localStorageValue = getLocalStorageKey(
|
||||
LOCALSTORAGE.SHOW_LOGS_QUICK_FILTERS,
|
||||
@@ -83,7 +86,6 @@ function LogsExplorer(): JSX.Element {
|
||||
}, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]);
|
||||
|
||||
const {
|
||||
queryData: optionsQueryData,
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
|
||||
@@ -164,12 +166,34 @@ function LogsExplorer(): JSX.Element {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const migratedQuery = migrateOptionsQuery(optionsQueryData);
|
||||
if (!preferences || preferencesLoading) {
|
||||
return;
|
||||
}
|
||||
const migratedQuery = migrateOptionsQuery({
|
||||
selectColumns: preferences.columns || defaultLogsSelectedColumns,
|
||||
maxLines: preferences.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
format: preferences.formatting?.format || defaultOptionsQuery.format,
|
||||
fontSize: preferences.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
version: preferences.formatting?.version,
|
||||
});
|
||||
// Only redirect if the query was actually modified
|
||||
if (!isEqual(migratedQuery, optionsQueryData)) {
|
||||
if (
|
||||
!isEqual(migratedQuery, {
|
||||
selectColumns: preferences?.columns,
|
||||
maxLines: preferences?.formatting?.maxLines,
|
||||
format: preferences?.formatting?.format,
|
||||
fontSize: preferences?.formatting?.fontSize,
|
||||
version: preferences?.formatting?.version,
|
||||
})
|
||||
) {
|
||||
redirectWithOptionsData(migratedQuery);
|
||||
}
|
||||
}, [migrateOptionsQuery, optionsQueryData, redirectWithOptionsData]);
|
||||
}, [
|
||||
migrateOptionsQuery,
|
||||
preferences,
|
||||
redirectWithOptionsData,
|
||||
preferencesLoading,
|
||||
]);
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -4,9 +4,14 @@ import { Compass, TowerControl, Workflow } from 'lucide-react';
|
||||
import LogsExplorer from 'pages/LogsExplorer';
|
||||
import Pipelines from 'pages/Pipelines';
|
||||
import SaveView from 'pages/SaveView';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
|
||||
export const logsExplorer: TabRoutes = {
|
||||
Component: LogsExplorer,
|
||||
Component: (): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorer />
|
||||
</PreferenceContextProvider>
|
||||
),
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Compass size={16} /> Explorer
|
||||
|
||||
@@ -4,6 +4,7 @@ import ExplorerPage from 'container/MetricsExplorer/Explorer';
|
||||
import SummaryPage from 'container/MetricsExplorer/Summary';
|
||||
import { BarChart2, Compass, TowerControl } from 'lucide-react';
|
||||
import SaveView from 'pages/SaveView';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
|
||||
export const Summary: TabRoutes = {
|
||||
Component: SummaryPage,
|
||||
@@ -17,7 +18,11 @@ export const Summary: TabRoutes = {
|
||||
};
|
||||
|
||||
export const Explorer: TabRoutes = {
|
||||
Component: ExplorerPage,
|
||||
Component: (): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerPage />
|
||||
</PreferenceContextProvider>
|
||||
),
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Compass size={16} /> Explorer
|
||||
|
||||
@@ -5,10 +5,15 @@ import SaveView from 'pages/SaveView';
|
||||
import TracesExplorer from 'pages/TracesExplorer';
|
||||
import TracesFunnelDetails from 'pages/TracesFunnelDetails';
|
||||
import TracesFunnels from 'pages/TracesFunnels';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
export const tracesExplorer: TabRoutes = {
|
||||
Component: TracesExplorer,
|
||||
Component: (): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<TracesExplorer />
|
||||
</PreferenceContextProvider>
|
||||
),
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Compass size={16} /> Explorer
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import {
|
||||
FormattingOptions,
|
||||
PreferenceMode,
|
||||
Preferences,
|
||||
} from 'providers/preferences/types';
|
||||
import { MemoryRouter, Route, Switch } from 'react-router-dom';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import {
|
||||
PreferenceContextProvider,
|
||||
usePreferenceContext,
|
||||
} from '../context/PreferenceContextProvider';
|
||||
|
||||
// Mock the usePreferenceSync hook
|
||||
jest.mock('../sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: jest.fn().mockReturnValue({
|
||||
preferences: {
|
||||
columns: [] as BaseAutocompleteData[],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
} as FormattingOptions,
|
||||
} as Preferences,
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Test component that consumes the context
|
||||
function TestConsumer(): JSX.Element {
|
||||
const context = usePreferenceContext();
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="mode">{context.mode}</div>
|
||||
<div data-testid="dataSource">{context.dataSource}</div>
|
||||
<div data-testid="loading">{String(context.loading)}</div>
|
||||
<div data-testid="error">{String(context.error)}</div>
|
||||
<div data-testid="savedViewId">{context.savedViewId || 'no-view-id'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe('PreferenceContextProvider', () => {
|
||||
it('should provide context with direct mode when no viewKey is present', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/logs']}>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/logs"
|
||||
component={(): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<TestConsumer />
|
||||
</PreferenceContextProvider>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('mode')).toHaveTextContent(PreferenceMode.DIRECT);
|
||||
expect(screen.getByTestId('dataSource')).toHaveTextContent('logs');
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
||||
expect(screen.getByTestId('error')).toHaveTextContent('null');
|
||||
expect(screen.getByTestId('savedViewId')).toHaveTextContent('no-view-id');
|
||||
});
|
||||
|
||||
it('should provide context with savedView mode when viewKey is present', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/logs?viewKey="test-view-id"']}>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/logs"
|
||||
component={(): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<TestConsumer />
|
||||
</PreferenceContextProvider>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('mode')).toHaveTextContent('savedView');
|
||||
expect(screen.getByTestId('dataSource')).toHaveTextContent('logs');
|
||||
expect(screen.getByTestId('savedViewId')).toHaveTextContent('test-view-id');
|
||||
});
|
||||
|
||||
it('should set traces dataSource when pathname includes traces', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/traces']}>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/traces"
|
||||
component={(): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<TestConsumer />
|
||||
</PreferenceContextProvider>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('dataSource')).toHaveTextContent('traces');
|
||||
});
|
||||
|
||||
it('should handle invalid viewKey JSON gracefully', () => {
|
||||
// Mock console.error to avoid test output clutter
|
||||
const originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/logs?viewKey=invalid-json']}>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/logs"
|
||||
component={(): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<TestConsumer />
|
||||
</PreferenceContextProvider>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('mode')).toHaveTextContent(PreferenceMode.DIRECT);
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
|
||||
// Restore console.error
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('should throw error when usePreferenceContext is used outside provider', () => {
|
||||
// Suppress the error output for this test
|
||||
const originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
expect(() => {
|
||||
render(<TestConsumer />);
|
||||
}).toThrow(
|
||||
'usePreferenceContext must be used within PreferenceContextProvider',
|
||||
);
|
||||
|
||||
// Restore console.error
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { FormattingOptions } from 'providers/preferences/types';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import logsLoaderConfig from '../configs/logsLoaderConfig';
|
||||
|
||||
// Mock localStorage
|
||||
const mockLocalStorage: Record<string, string> = {};
|
||||
|
||||
jest.mock('api/browser/localstorage/get', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((key: string) => mockLocalStorage[key] || null),
|
||||
}));
|
||||
|
||||
describe('logsLoaderConfig', () => {
|
||||
// Save original location object
|
||||
const originalWindowLocation = window.location;
|
||||
let mockedLocation: Partial<Location>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup a mocked location object
|
||||
mockedLocation = {
|
||||
...originalWindowLocation,
|
||||
search: '',
|
||||
};
|
||||
|
||||
// Mock the window.location property
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: mockedLocation,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Clear mocked localStorage
|
||||
Object.keys(mockLocalStorage).forEach((key) => {
|
||||
delete mockLocalStorage[key];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original location
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: originalWindowLocation,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have priority order: local, url, default', () => {
|
||||
expect(logsLoaderConfig.priority).toEqual(['local', 'url', 'default']);
|
||||
});
|
||||
|
||||
it('should load from localStorage when available', async () => {
|
||||
const mockColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'test-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Set up localStorage mock data with the correct key from LOCALSTORAGE enum
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS] = JSON.stringify({
|
||||
selectColumns: mockColumns,
|
||||
maxLines: 10,
|
||||
format: 'json',
|
||||
fontSize: 'large',
|
||||
version: 2,
|
||||
});
|
||||
|
||||
const result = await logsLoaderConfig.local();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: mockColumns,
|
||||
formatting: {
|
||||
maxLines: 10,
|
||||
format: 'json' as LogViewMode,
|
||||
fontSize: 'large' as FontSize,
|
||||
version: 2,
|
||||
} as FormattingOptions,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid localStorage data gracefully', async () => {
|
||||
// Set up invalid localStorage mock data
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS] = 'invalid-json';
|
||||
|
||||
const result = await logsLoaderConfig.local();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: [] as BaseAutocompleteData[],
|
||||
formatting: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should load from URL when available', async () => {
|
||||
const mockColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'url-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Set up URL search params
|
||||
mockedLocation.search = `?options=${encodeURIComponent(
|
||||
JSON.stringify({
|
||||
selectColumns: mockColumns,
|
||||
maxLines: 5,
|
||||
format: 'raw',
|
||||
fontSize: 'medium',
|
||||
version: 1,
|
||||
}),
|
||||
)}`;
|
||||
|
||||
const result = await logsLoaderConfig.url();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: mockColumns,
|
||||
formatting: {
|
||||
maxLines: 5,
|
||||
format: 'raw' as LogViewMode,
|
||||
fontSize: 'medium' as FontSize,
|
||||
version: 1,
|
||||
} as FormattingOptions,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid URL data gracefully', async () => {
|
||||
// Set up invalid URL search params
|
||||
mockedLocation.search = '?options=invalid-json';
|
||||
|
||||
const result = await logsLoaderConfig.url();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: [] as BaseAutocompleteData[],
|
||||
formatting: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide default values when no other source is available', async () => {
|
||||
const result = await logsLoaderConfig.default();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: defaultLogsSelectedColumns as BaseAutocompleteData[],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table' as LogViewMode,
|
||||
fontSize: 'small' as FontSize,
|
||||
version: 1,
|
||||
} as FormattingOptions,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
FormattingOptions,
|
||||
PreferenceMode,
|
||||
Preferences,
|
||||
} from 'providers/preferences/types';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import getLogsUpdaterConfig from '../configs/logsUpdaterConfig';
|
||||
|
||||
// Mock localStorage
|
||||
const mockLocalStorage: Record<string, string> = {};
|
||||
|
||||
jest.mock('api/browser/localstorage/set', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((key: string, value: string) => {
|
||||
mockLocalStorage[key] = value;
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock localStorage.getItem
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
getItem: jest.fn((key: string) => mockLocalStorage[key] || null),
|
||||
setItem: jest.fn((key: string, value: string) => {
|
||||
mockLocalStorage[key] = value;
|
||||
}),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('logsUpdaterConfig', () => {
|
||||
// Mock redirectWithOptionsData and setSavedViewPreferences
|
||||
const redirectWithOptionsData = jest.fn();
|
||||
const setSavedViewPreferences = jest.fn();
|
||||
|
||||
const mockPreferences: Preferences = {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table' as LogViewMode,
|
||||
fontSize: 'small' as FontSize,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Clear mocked localStorage
|
||||
Object.keys(mockLocalStorage).forEach((key) => {
|
||||
delete mockLocalStorage[key];
|
||||
});
|
||||
});
|
||||
|
||||
it('should update columns in localStorage for direct mode', () => {
|
||||
const logsUpdater = getLogsUpdaterConfig(
|
||||
mockPreferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
);
|
||||
|
||||
const newColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'new-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Set initial localStorage data
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS] = JSON.stringify({
|
||||
selectColumns: [
|
||||
{
|
||||
key: 'old-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
],
|
||||
maxLines: 2,
|
||||
});
|
||||
|
||||
logsUpdater.updateColumns(newColumns, PreferenceMode.DIRECT);
|
||||
|
||||
// Should update URL
|
||||
expect(redirectWithOptionsData).toHaveBeenCalledWith({
|
||||
...defaultOptionsQuery,
|
||||
...mockPreferences.formatting,
|
||||
selectColumns: newColumns,
|
||||
});
|
||||
|
||||
// Should update localStorage
|
||||
const storedData = JSON.parse(
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS],
|
||||
);
|
||||
expect(storedData.selectColumns).toEqual(newColumns);
|
||||
expect(storedData.maxLines).toBe(2); // Should preserve other fields
|
||||
|
||||
// Should not update saved view preferences
|
||||
expect(setSavedViewPreferences).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update columns in savedViewPreferences for savedView mode', () => {
|
||||
const logsUpdater = getLogsUpdaterConfig(
|
||||
mockPreferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
);
|
||||
|
||||
const newColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'new-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
logsUpdater.updateColumns(newColumns, PreferenceMode.SAVED_VIEW);
|
||||
|
||||
// Should not update URL in savedView mode
|
||||
expect(redirectWithOptionsData).not.toHaveBeenCalled();
|
||||
|
||||
// Should not update localStorage in savedView mode
|
||||
expect(mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS]).toBeUndefined();
|
||||
|
||||
// Should update saved view preferences
|
||||
expect(setSavedViewPreferences).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('should update formatting options in localStorage for direct mode', () => {
|
||||
const logsUpdater = getLogsUpdaterConfig(
|
||||
mockPreferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
);
|
||||
|
||||
const newFormatting: FormattingOptions = {
|
||||
maxLines: 5,
|
||||
format: 'json' as LogViewMode,
|
||||
fontSize: 'large' as FontSize,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
// Set initial localStorage data
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS] = JSON.stringify({
|
||||
selectColumns: [
|
||||
{
|
||||
key: 'column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
],
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
});
|
||||
|
||||
logsUpdater.updateFormatting(newFormatting, PreferenceMode.DIRECT);
|
||||
|
||||
// Should always update URL for both modes
|
||||
expect(redirectWithOptionsData).toHaveBeenCalledWith({
|
||||
...defaultOptionsQuery,
|
||||
...mockPreferences.formatting,
|
||||
...newFormatting,
|
||||
});
|
||||
|
||||
// Should update localStorage in direct mode
|
||||
const storedData = JSON.parse(
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS],
|
||||
);
|
||||
expect(storedData.maxLines).toBe(5);
|
||||
expect(storedData.format).toBe('json');
|
||||
expect(storedData.fontSize).toBe('large');
|
||||
expect(storedData.version).toBe(1);
|
||||
expect(storedData.selectColumns).toEqual([
|
||||
{
|
||||
key: 'column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
]); // Should preserve columns
|
||||
});
|
||||
|
||||
it('should not update localStorage for savedView mode in updateFormatting', () => {
|
||||
const logsUpdater = getLogsUpdaterConfig(
|
||||
mockPreferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
);
|
||||
|
||||
const newFormatting: FormattingOptions = {
|
||||
maxLines: 5,
|
||||
format: 'json' as LogViewMode,
|
||||
fontSize: 'large' as FontSize,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
// Set initial localStorage data
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS] = JSON.stringify({
|
||||
selectColumns: [
|
||||
{
|
||||
key: 'column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
],
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
});
|
||||
|
||||
logsUpdater.updateFormatting(newFormatting, PreferenceMode.SAVED_VIEW);
|
||||
|
||||
// Should not override localStorage in savedView mode
|
||||
const storedData = JSON.parse(
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS],
|
||||
);
|
||||
expect(storedData.maxLines).toBe(2); // Should remain the same
|
||||
expect(storedData.format).toBe('table'); // Should remain the same
|
||||
|
||||
// Should update saved view preferences
|
||||
expect(setSavedViewPreferences).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('should initialize localStorage if it does not exist', () => {
|
||||
const logsUpdater = getLogsUpdaterConfig(
|
||||
mockPreferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
);
|
||||
|
||||
const newFormatting: FormattingOptions = {
|
||||
maxLines: 5,
|
||||
format: 'json' as LogViewMode,
|
||||
fontSize: 'large' as FontSize,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
// No initial localStorage data
|
||||
|
||||
logsUpdater.updateFormatting(newFormatting, PreferenceMode.DIRECT);
|
||||
|
||||
// Should create localStorage entry
|
||||
const storedData = JSON.parse(
|
||||
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS],
|
||||
);
|
||||
expect(storedData.maxLines).toBe(5);
|
||||
expect(storedData.format).toBe('json');
|
||||
expect(storedData.fontSize).toBe('large');
|
||||
expect(storedData.version).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
|
||||
|
||||
// Mock localStorage
|
||||
const mockLocalStorage: Record<string, string> = {};
|
||||
|
||||
jest.mock('api/browser/localstorage/get', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((key: string) => mockLocalStorage[key] || null),
|
||||
}));
|
||||
|
||||
describe('tracesLoaderConfig', () => {
|
||||
// Save original location object
|
||||
const originalWindowLocation = window.location;
|
||||
let mockedLocation: Partial<Location>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup a mocked location object
|
||||
mockedLocation = {
|
||||
...originalWindowLocation,
|
||||
search: '',
|
||||
};
|
||||
|
||||
// Mock the window.location property
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: mockedLocation,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Clear mocked localStorage
|
||||
Object.keys(mockLocalStorage).forEach((key) => {
|
||||
delete mockLocalStorage[key];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original location
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: originalWindowLocation,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have priority order: local, url, default', () => {
|
||||
expect(tracesLoaderConfig.priority).toEqual(['local', 'url', 'default']);
|
||||
});
|
||||
|
||||
it('should load from localStorage when available', async () => {
|
||||
const mockColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'test-trace-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Set up localStorage mock data with the correct key from LOCALSTORAGE enum
|
||||
mockLocalStorage[LOCALSTORAGE.TRACES_LIST_OPTIONS] = JSON.stringify({
|
||||
selectColumns: mockColumns,
|
||||
});
|
||||
|
||||
const result = await tracesLoaderConfig.local();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: mockColumns,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid localStorage data gracefully', async () => {
|
||||
// Set up invalid localStorage mock data
|
||||
mockLocalStorage[LOCALSTORAGE.TRACES_LIST_OPTIONS] = 'invalid-json';
|
||||
|
||||
const result = await tracesLoaderConfig.local();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: [] as BaseAutocompleteData[],
|
||||
});
|
||||
});
|
||||
|
||||
it('should load from URL when available', async () => {
|
||||
const mockColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'url-trace-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Set up URL search params
|
||||
mockedLocation.search = `?options=${encodeURIComponent(
|
||||
JSON.stringify({
|
||||
selectColumns: mockColumns,
|
||||
}),
|
||||
)}`;
|
||||
|
||||
const result = await tracesLoaderConfig.url();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: mockColumns,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid URL data gracefully', async () => {
|
||||
// Set up invalid URL search params
|
||||
mockedLocation.search = '?options=invalid-json';
|
||||
|
||||
const result = await tracesLoaderConfig.url();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: [] as BaseAutocompleteData[],
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide default values when no other source is available', async () => {
|
||||
const result = await tracesLoaderConfig.default();
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: defaultTraceSelectedColumns as BaseAutocompleteData[],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig';
|
||||
import { PreferenceMode } from '../types';
|
||||
|
||||
// Mock setLocalStorageKey
|
||||
const mockSetLocalStorageKey = jest.fn();
|
||||
jest.mock('api/browser/localstorage/set', () => ({
|
||||
__esModule: true,
|
||||
default: (key: string, value: string): void =>
|
||||
mockSetLocalStorageKey(key, value),
|
||||
}));
|
||||
|
||||
// Mock localStorage
|
||||
let mockLocalStorage: Record<string, string> = {};
|
||||
Object.defineProperty(global, 'localStorage', {
|
||||
value: {
|
||||
getItem: jest.fn((key: string) => mockLocalStorage[key] || null),
|
||||
setItem: jest.fn((key: string, value: string) => {
|
||||
mockLocalStorage[key] = value;
|
||||
}),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('tracesUpdaterConfig', () => {
|
||||
// Mock functions
|
||||
const mockRedirectWithOptionsData = jest.fn();
|
||||
const mockSetSavedViewPreferences = jest.fn();
|
||||
|
||||
// Test data
|
||||
const mockColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'test-trace-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset mockLocalStorage
|
||||
mockLocalStorage = {};
|
||||
});
|
||||
|
||||
it('should update columns in localStorage and redirect with options in direct mode', () => {
|
||||
const tracesUpdaterConfig = getTracesUpdaterConfig(
|
||||
mockRedirectWithOptionsData,
|
||||
mockSetSavedViewPreferences,
|
||||
);
|
||||
|
||||
tracesUpdaterConfig.updateColumns(mockColumns, PreferenceMode.DIRECT);
|
||||
|
||||
// Should redirect with the updated columns
|
||||
expect(mockRedirectWithOptionsData).toHaveBeenCalledWith({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: mockColumns,
|
||||
});
|
||||
|
||||
// Should set localStorage with the updated columns
|
||||
expect(mockSetLocalStorageKey).toHaveBeenCalledWith(
|
||||
LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
||||
JSON.stringify({ selectColumns: mockColumns }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge with existing localStorage data in direct mode', () => {
|
||||
// Setup existing localStorage data
|
||||
mockLocalStorage[LOCALSTORAGE.TRACES_LIST_OPTIONS] = JSON.stringify({
|
||||
selectColumns: [
|
||||
{
|
||||
key: 'existing-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
],
|
||||
otherProp: 'value',
|
||||
});
|
||||
|
||||
const tracesUpdaterConfig = getTracesUpdaterConfig(
|
||||
mockRedirectWithOptionsData,
|
||||
mockSetSavedViewPreferences,
|
||||
);
|
||||
|
||||
tracesUpdaterConfig.updateColumns(mockColumns, PreferenceMode.DIRECT);
|
||||
|
||||
// Should set localStorage with the updated columns while preserving other props
|
||||
expect(mockSetLocalStorageKey).toHaveBeenCalledWith(
|
||||
LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
||||
JSON.stringify({
|
||||
selectColumns: mockColumns,
|
||||
otherProp: 'value',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update savedViewPreferences in savedView mode', () => {
|
||||
const tracesUpdaterConfig = getTracesUpdaterConfig(
|
||||
mockRedirectWithOptionsData,
|
||||
mockSetSavedViewPreferences,
|
||||
);
|
||||
|
||||
tracesUpdaterConfig.updateColumns(mockColumns, PreferenceMode.SAVED_VIEW);
|
||||
|
||||
// Should not redirect or modify localStorage in savedView mode
|
||||
expect(mockRedirectWithOptionsData).not.toHaveBeenCalled();
|
||||
expect(mockSetLocalStorageKey).not.toHaveBeenCalled();
|
||||
|
||||
// Should update savedViewPreferences
|
||||
expect(mockSetSavedViewPreferences).toHaveBeenCalledWith({
|
||||
columns: mockColumns,
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a no-op updateFormatting method', () => {
|
||||
const tracesUpdaterConfig = getTracesUpdaterConfig(
|
||||
mockRedirectWithOptionsData,
|
||||
mockSetSavedViewPreferences,
|
||||
);
|
||||
|
||||
// Call updateFormatting and verify it does nothing
|
||||
tracesUpdaterConfig.updateFormatting();
|
||||
|
||||
// No API calls should be made
|
||||
expect(mockRedirectWithOptionsData).not.toHaveBeenCalled();
|
||||
expect(mockSetLocalStorageKey).not.toHaveBeenCalled();
|
||||
expect(mockSetSavedViewPreferences).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import logsLoaderConfig from '../configs/logsLoaderConfig';
|
||||
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
|
||||
|
||||
// Mock the config loaders
|
||||
jest.mock('../configs/logsLoaderConfig', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
priority: ['local', 'url', 'default'],
|
||||
local: jest.fn().mockResolvedValue({
|
||||
columns: [{ name: 'local-column' }],
|
||||
formatting: { maxLines: 5, format: 'table', fontSize: 'medium', version: 1 },
|
||||
}),
|
||||
url: jest.fn().mockResolvedValue({
|
||||
columns: [{ name: 'url-column' }],
|
||||
formatting: { maxLines: 3, format: 'table', fontSize: 'small', version: 1 },
|
||||
}),
|
||||
default: jest.fn().mockResolvedValue({
|
||||
columns: [{ name: 'default-column' }],
|
||||
formatting: { maxLines: 2, format: 'table', fontSize: 'small', version: 1 },
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../configs/tracesLoaderConfig', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
priority: ['local', 'url', 'default'],
|
||||
local: jest.fn().mockResolvedValue({
|
||||
columns: [{ name: 'local-trace-column' }],
|
||||
}),
|
||||
url: jest.fn().mockResolvedValue({
|
||||
columns: [{ name: 'url-trace-column' }],
|
||||
}),
|
||||
default: jest.fn().mockResolvedValue({
|
||||
columns: [{ name: 'default-trace-column' }],
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('usePreferenceLoader', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should load logs preferences based on priority order', async () => {
|
||||
const setReSync = jest.fn();
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceLoader({
|
||||
dataSource: DataSource.LOGS,
|
||||
reSync: false,
|
||||
setReSync,
|
||||
}),
|
||||
);
|
||||
|
||||
// Initially it should be loading
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.preferences).toBe(null);
|
||||
expect(result.current.error).toBe(null);
|
||||
|
||||
// Wait for the loader to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// Should have loaded from local storage (highest priority)
|
||||
expect(result.current.preferences).toEqual({
|
||||
columns: [{ name: 'local-column' }],
|
||||
formatting: { maxLines: 5, format: 'table', fontSize: 'medium', version: 1 },
|
||||
});
|
||||
expect(result.current.error).toBe(null);
|
||||
expect(setReSync).not.toHaveBeenCalled(); // Should not call setReSync when reSync is false
|
||||
});
|
||||
|
||||
it('should load traces preferences', async () => {
|
||||
const setReSync = jest.fn();
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceLoader({
|
||||
dataSource: DataSource.TRACES,
|
||||
reSync: false,
|
||||
setReSync,
|
||||
}),
|
||||
);
|
||||
|
||||
// Wait for the loader to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// Should have loaded trace columns
|
||||
expect(result.current.preferences).toEqual({
|
||||
columns: [{ name: 'local-trace-column' }],
|
||||
});
|
||||
expect(setReSync).not.toHaveBeenCalled(); // Should not call setReSync when reSync is false
|
||||
});
|
||||
|
||||
it('should call setReSync when reSync is true', async () => {
|
||||
const setReSync = jest.fn();
|
||||
|
||||
// Test that the hook calls setReSync(false) when reSync is true
|
||||
// We'll unmount quickly to avoid the infinite loop
|
||||
const { unmount } = renderHook(() =>
|
||||
usePreferenceLoader({
|
||||
dataSource: DataSource.LOGS,
|
||||
reSync: true,
|
||||
setReSync,
|
||||
}),
|
||||
);
|
||||
// Wait for the effect to run
|
||||
await waitFor(() => {
|
||||
expect(setReSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Unmount to stop the effect
|
||||
unmount();
|
||||
|
||||
// Should have called setReSync(false) to reset the reSync flag
|
||||
expect(setReSync).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should handle errors during loading', async () => {
|
||||
// Mock an error in the loader using jest.spyOn
|
||||
const localSpy = jest.spyOn(logsLoaderConfig, 'local');
|
||||
localSpy.mockRejectedValueOnce(new Error('Loading failed'));
|
||||
|
||||
const setReSync = jest.fn();
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceLoader({
|
||||
dataSource: DataSource.LOGS,
|
||||
reSync: false,
|
||||
setReSync,
|
||||
}),
|
||||
);
|
||||
|
||||
// Wait for the loader to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// Should have set the error
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect(result.current.error?.message).toBe('Loading failed');
|
||||
expect(result.current.preferences).toBe(null);
|
||||
|
||||
// Restore original implementation
|
||||
localSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
FormattingOptions,
|
||||
PreferenceMode,
|
||||
Preferences,
|
||||
} from 'providers/preferences/types';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
|
||||
|
||||
// Mock the config updaters
|
||||
const mockUpdateColumns = jest.fn();
|
||||
const mockUpdateFormatting = jest.fn();
|
||||
|
||||
jest.mock('../configs/logsUpdaterConfig', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../configs/tracesUpdaterConfig', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock the URL query hook
|
||||
jest.mock('hooks/useUrlQueryData', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockReturnValue({
|
||||
redirectWithQuery: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('usePreferenceUpdater', () => {
|
||||
const mockPreferences: Preferences = {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table' as LogViewMode,
|
||||
fontSize: 'small' as FontSize,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return updateColumns and updateFormatting functions', () => {
|
||||
const setReSync = jest.fn();
|
||||
const setSavedViewPreferences = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceUpdater({
|
||||
dataSource: DataSource.LOGS,
|
||||
mode: PreferenceMode.DIRECT,
|
||||
preferences: mockPreferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should return the update functions
|
||||
expect(typeof result.current.updateColumns).toBe('function');
|
||||
expect(typeof result.current.updateFormatting).toBe('function');
|
||||
});
|
||||
|
||||
it('should call the logs updater for updateColumns with logs dataSource', () => {
|
||||
const setReSync = jest.fn();
|
||||
const setSavedViewPreferences = jest.fn();
|
||||
const newColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'new-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceUpdater({
|
||||
dataSource: DataSource.LOGS,
|
||||
mode: PreferenceMode.DIRECT,
|
||||
preferences: mockPreferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.updateColumns(newColumns);
|
||||
});
|
||||
|
||||
// Should call the logs updater
|
||||
expect(mockUpdateColumns).toHaveBeenCalledWith(
|
||||
newColumns,
|
||||
PreferenceMode.DIRECT,
|
||||
);
|
||||
expect(setReSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should call the logs updater for updateFormatting with logs dataSource', () => {
|
||||
const setReSync = jest.fn();
|
||||
const setSavedViewPreferences = jest.fn();
|
||||
const newFormatting: FormattingOptions = {
|
||||
maxLines: 10,
|
||||
format: 'table' as LogViewMode,
|
||||
fontSize: 'large' as FontSize,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceUpdater({
|
||||
dataSource: DataSource.LOGS,
|
||||
mode: PreferenceMode.DIRECT,
|
||||
preferences: mockPreferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.updateFormatting(newFormatting);
|
||||
});
|
||||
|
||||
// Should call the logs updater
|
||||
expect(mockUpdateFormatting).toHaveBeenCalledWith(
|
||||
newFormatting,
|
||||
PreferenceMode.DIRECT,
|
||||
);
|
||||
expect(setReSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should call the traces updater for updateColumns with traces dataSource', () => {
|
||||
const setReSync = jest.fn();
|
||||
const setSavedViewPreferences = jest.fn();
|
||||
const newColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: 'new-trace-column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceUpdater({
|
||||
dataSource: DataSource.TRACES,
|
||||
mode: PreferenceMode.DIRECT,
|
||||
preferences: mockPreferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.updateColumns(newColumns);
|
||||
});
|
||||
|
||||
// Should call the traces updater
|
||||
expect(mockUpdateColumns).toHaveBeenCalledWith(
|
||||
newColumns,
|
||||
PreferenceMode.DIRECT,
|
||||
);
|
||||
expect(setReSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should call the traces updater for updateFormatting with traces dataSource', () => {
|
||||
const setReSync = jest.fn();
|
||||
const setSavedViewPreferences = jest.fn();
|
||||
const newFormatting: FormattingOptions = {
|
||||
maxLines: 10,
|
||||
format: 'table' as LogViewMode,
|
||||
fontSize: 'large' as FontSize,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceUpdater({
|
||||
dataSource: DataSource.TRACES,
|
||||
mode: PreferenceMode.DIRECT,
|
||||
preferences: mockPreferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.updateFormatting(newFormatting);
|
||||
});
|
||||
|
||||
// Should call the traces updater
|
||||
expect(mockUpdateFormatting).toHaveBeenCalledWith(
|
||||
newFormatting,
|
||||
PreferenceMode.DIRECT,
|
||||
);
|
||||
expect(setReSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should increment reSync counter when updates are called', () => {
|
||||
const setReSync = jest.fn();
|
||||
const setSavedViewPreferences = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePreferenceUpdater({
|
||||
dataSource: DataSource.LOGS,
|
||||
mode: PreferenceMode.DIRECT,
|
||||
preferences: mockPreferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.updateColumns([
|
||||
{
|
||||
key: 'column',
|
||||
type: 'tag',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
expect(setReSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable no-empty */
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { FormattingOptions } from '../types';
|
||||
|
||||
// --- LOGS preferences loader config ---
|
||||
const logsLoaders = {
|
||||
local: async (): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
formatting: FormattingOptions;
|
||||
}> => {
|
||||
const local = getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS);
|
||||
if (local) {
|
||||
try {
|
||||
const parsed = JSON.parse(local);
|
||||
return {
|
||||
columns: parsed.selectColumns || [],
|
||||
formatting: {
|
||||
maxLines: parsed.maxLines ?? 2,
|
||||
format: parsed.format ?? 'table',
|
||||
fontSize: parsed.fontSize ?? 'small',
|
||||
version: parsed.version ?? 1,
|
||||
},
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
return { columns: [], formatting: undefined } as any;
|
||||
},
|
||||
url: async (): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
formatting: FormattingOptions;
|
||||
}> => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
try {
|
||||
const options = JSON.parse(urlParams.get('options') || '{}');
|
||||
return {
|
||||
columns: options.selectColumns || [],
|
||||
formatting: {
|
||||
maxLines: options.maxLines ?? 2,
|
||||
format: options.format ?? 'table',
|
||||
fontSize: options.fontSize ?? 'small',
|
||||
version: options.version ?? 1,
|
||||
},
|
||||
};
|
||||
} catch {}
|
||||
return { columns: [], formatting: undefined } as any;
|
||||
},
|
||||
default: async (): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
formatting: FormattingOptions;
|
||||
}> => ({
|
||||
columns: defaultLogsSelectedColumns as BaseAutocompleteData[],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small' as FontSize,
|
||||
version: 1,
|
||||
},
|
||||
}),
|
||||
priority: ['local', 'url', 'default'] as const,
|
||||
};
|
||||
|
||||
export default logsLoaders;
|
||||
@@ -0,0 +1,85 @@
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { FormattingOptions, PreferenceMode, Preferences } from '../types';
|
||||
|
||||
// --- LOGS preferences updater config ---
|
||||
const getLogsUpdaterConfig = (
|
||||
preferences: Preferences | null,
|
||||
redirectWithOptionsData: (options: OptionsQuery) => void,
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
|
||||
): {
|
||||
updateColumns: (newColumns: BaseAutocompleteData[], mode: string) => void;
|
||||
updateFormatting: (newFormatting: FormattingOptions, mode: string) => void;
|
||||
} => ({
|
||||
updateColumns: (newColumns: BaseAutocompleteData[], mode: string): void => {
|
||||
if (mode === PreferenceMode.SAVED_VIEW) {
|
||||
setSavedViewPreferences((prev) => {
|
||||
if (!prev) {
|
||||
return {
|
||||
columns: newColumns,
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small' as FontSize,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
columns: newColumns,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === PreferenceMode.DIRECT) {
|
||||
// just need to update the columns see for remove props
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
...preferences?.formatting,
|
||||
selectColumns: newColumns,
|
||||
});
|
||||
|
||||
// Also update local storage
|
||||
const local = JSON.parse(
|
||||
localStorage.getItem(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}',
|
||||
);
|
||||
local.selectColumns = newColumns;
|
||||
setLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS, JSON.stringify(local));
|
||||
}
|
||||
},
|
||||
updateFormatting: (newFormatting: FormattingOptions, mode: string): void => {
|
||||
if (mode === PreferenceMode.SAVED_VIEW) {
|
||||
setSavedViewPreferences((prev) => {
|
||||
if (!prev) return { columns: [], formatting: newFormatting };
|
||||
return {
|
||||
...prev,
|
||||
formatting: newFormatting,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === PreferenceMode.DIRECT) {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
...preferences?.formatting,
|
||||
...newFormatting,
|
||||
});
|
||||
|
||||
// Also update local storage
|
||||
const local = JSON.parse(
|
||||
localStorage.getItem(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}',
|
||||
);
|
||||
Object.assign(local, newFormatting);
|
||||
setLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS, JSON.stringify(local));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default getLogsUpdaterConfig;
|
||||
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-empty */
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
// --- TRACES preferences loader config ---
|
||||
const tracesLoaders = {
|
||||
local: async (): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
}> => {
|
||||
const local = getLocalStorageKey(LOCALSTORAGE.TRACES_LIST_OPTIONS);
|
||||
if (local) {
|
||||
try {
|
||||
const parsed = JSON.parse(local);
|
||||
return {
|
||||
columns: parsed.selectColumns || [],
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
return { columns: [] };
|
||||
},
|
||||
url: async (): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
}> => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
try {
|
||||
const options = JSON.parse(urlParams.get('options') || '{}');
|
||||
return {
|
||||
columns: options.selectColumns || [],
|
||||
};
|
||||
} catch {}
|
||||
return { columns: [] };
|
||||
},
|
||||
default: async (): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
}> => ({
|
||||
columns: defaultTraceSelectedColumns as BaseAutocompleteData[],
|
||||
}),
|
||||
priority: ['local', 'url', 'default'] as const,
|
||||
};
|
||||
|
||||
export default tracesLoaders;
|
||||
@@ -0,0 +1,49 @@
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { PreferenceMode, Preferences } from '../types';
|
||||
|
||||
// --- TRACES preferences updater config ---
|
||||
const getTracesUpdaterConfig = (
|
||||
redirectWithOptionsData: (options: OptionsQuery) => void,
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
|
||||
): {
|
||||
updateColumns: (newColumns: BaseAutocompleteData[], mode: string) => void;
|
||||
updateFormatting: () => void;
|
||||
} => ({
|
||||
updateColumns: (newColumns: BaseAutocompleteData[], mode: string): void => {
|
||||
// remove the formatting props
|
||||
if (mode === PreferenceMode.SAVED_VIEW) {
|
||||
setSavedViewPreferences({
|
||||
columns: newColumns,
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small' as FontSize,
|
||||
version: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === PreferenceMode.DIRECT) {
|
||||
// just need to update the columns see for remove props
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newColumns,
|
||||
});
|
||||
|
||||
const local = JSON.parse(
|
||||
localStorage.getItem(LOCALSTORAGE.TRACES_LIST_OPTIONS) || '{}',
|
||||
);
|
||||
local.selectColumns = newColumns;
|
||||
setLocalStorageKey(LOCALSTORAGE.TRACES_LIST_OPTIONS, JSON.stringify(local));
|
||||
}
|
||||
},
|
||||
updateFormatting: (): void => {}, // no-op for traces
|
||||
});
|
||||
|
||||
export default getTracesUpdaterConfig;
|
||||
@@ -0,0 +1,84 @@
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import {
|
||||
PreferenceContextValue,
|
||||
PreferenceMode,
|
||||
} from 'providers/preferences/types';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { usePreferenceSync } from '../sync/usePreferenceSync';
|
||||
|
||||
const PreferenceContext = createContext<PreferenceContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function PreferenceContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const location = useLocation();
|
||||
const params = useUrlQuery();
|
||||
|
||||
let savedViewId = '';
|
||||
const viewKeyParam = params.get('viewKey');
|
||||
if (viewKeyParam) {
|
||||
try {
|
||||
savedViewId = JSON.parse(viewKeyParam);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
let dataSource: DataSource = DataSource.LOGS;
|
||||
if (location.pathname.includes('traces')) dataSource = DataSource.TRACES;
|
||||
|
||||
const {
|
||||
preferences,
|
||||
loading,
|
||||
error,
|
||||
updateColumns,
|
||||
updateFormatting,
|
||||
} = usePreferenceSync({
|
||||
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
|
||||
savedViewId: savedViewId || undefined,
|
||||
dataSource,
|
||||
});
|
||||
|
||||
const value = useMemo<PreferenceContextValue>(
|
||||
() => ({
|
||||
preferences,
|
||||
loading,
|
||||
error,
|
||||
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
|
||||
savedViewId: savedViewId || undefined,
|
||||
dataSource,
|
||||
updateColumns,
|
||||
updateFormatting,
|
||||
}),
|
||||
[
|
||||
savedViewId,
|
||||
dataSource,
|
||||
preferences,
|
||||
loading,
|
||||
error,
|
||||
updateColumns,
|
||||
updateFormatting,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<PreferenceContext.Provider value={value}>
|
||||
{children}
|
||||
</PreferenceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePreferenceContext(): PreferenceContextValue {
|
||||
const ctx = useContext(PreferenceContext);
|
||||
if (!ctx)
|
||||
throw new Error(
|
||||
'usePreferenceContext must be used within PreferenceContextProvider',
|
||||
);
|
||||
return ctx;
|
||||
}
|
||||
108
frontend/src/providers/preferences/loader/usePreferenceLoader.ts
Normal file
108
frontend/src/providers/preferences/loader/usePreferenceLoader.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable no-empty */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import logsLoaderConfig from '../configs/logsLoaderConfig';
|
||||
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
|
||||
import { FormattingOptions, Preferences } from '../types';
|
||||
|
||||
// Generic preferences loader that works with any config
|
||||
async function preferencesLoader<T>(config: {
|
||||
priority: readonly string[];
|
||||
[key: string]: any;
|
||||
}): Promise<T> {
|
||||
const findValidLoader = async (): Promise<T> => {
|
||||
// Try each loader in priority order
|
||||
const results = await Promise.all(
|
||||
config.priority.map(async (source) => ({
|
||||
source,
|
||||
result: await config[source](),
|
||||
})),
|
||||
);
|
||||
|
||||
// Find valid columns and formatting independently
|
||||
const validColumnsResult = results.find(
|
||||
({ result }) => result.columns?.length,
|
||||
);
|
||||
const validFormattingResult = results.find(({ result }) => result.formatting);
|
||||
|
||||
// Combine valid results or fallback to default
|
||||
const finalResult = {
|
||||
columns: validColumnsResult?.result.columns || config.default().columns,
|
||||
formatting:
|
||||
validFormattingResult?.result.formatting || config.default().formatting,
|
||||
};
|
||||
|
||||
return finalResult as T;
|
||||
};
|
||||
|
||||
return findValidLoader();
|
||||
}
|
||||
|
||||
// Use the generic loader with specific configs
|
||||
async function logsPreferencesLoader(): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
formatting: FormattingOptions;
|
||||
}> {
|
||||
return preferencesLoader(logsLoaderConfig);
|
||||
}
|
||||
|
||||
async function tracesPreferencesLoader(): Promise<{
|
||||
columns: BaseAutocompleteData[];
|
||||
}> {
|
||||
return preferencesLoader(tracesLoaderConfig);
|
||||
}
|
||||
|
||||
export function usePreferenceLoader({
|
||||
dataSource,
|
||||
reSync,
|
||||
setReSync,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
reSync: boolean;
|
||||
setReSync: (value: boolean) => void;
|
||||
}): {
|
||||
preferences: Preferences | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
} {
|
||||
const [preferences, setPreferences] = useState<Preferences | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
async function loadPreferences(): Promise<void> {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
const { columns, formatting } = await logsPreferencesLoader();
|
||||
setPreferences({ columns, formatting });
|
||||
}
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
const { columns } = await tracesPreferencesLoader();
|
||||
setPreferences({ columns });
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e as Error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// Reset reSync back to false after loading is complete
|
||||
if (reSync) {
|
||||
setReSync(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only load preferences on initial mount or when reSync is true
|
||||
if (loading || reSync) {
|
||||
loadPreferences();
|
||||
}
|
||||
}, [dataSource, reSync, setReSync, loading]);
|
||||
|
||||
return { preferences, loading, error };
|
||||
}
|
||||
84
frontend/src/providers/preferences/sync/usePreferenceSync.ts
Normal file
84
frontend/src/providers/preferences/sync/usePreferenceSync.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { defaultSelectedColumns as defaultTracesSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
|
||||
import { FormattingOptions, PreferenceMode, Preferences } from '../types';
|
||||
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
|
||||
|
||||
export function usePreferenceSync({
|
||||
mode,
|
||||
dataSource,
|
||||
savedViewId,
|
||||
}: {
|
||||
mode: PreferenceMode;
|
||||
dataSource: DataSource;
|
||||
savedViewId: string | undefined;
|
||||
}): {
|
||||
preferences: Preferences | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
updateColumns: (newColumns: BaseAutocompleteData[]) => void;
|
||||
updateFormatting: (newFormatting: FormattingOptions) => void;
|
||||
} {
|
||||
const { data: viewsData } = useGetAllViews(dataSource);
|
||||
|
||||
const [
|
||||
savedViewPreferences,
|
||||
setSavedViewPreferences,
|
||||
] = useState<Preferences | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const extraData = viewsData?.data?.data?.find(
|
||||
(view) => view.id === savedViewId,
|
||||
)?.extraData;
|
||||
|
||||
const parsedExtraData = JSON.parse(extraData || '{}');
|
||||
let columns: BaseAutocompleteData[] = [];
|
||||
let formatting: FormattingOptions | undefined;
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
columns = parsedExtraData?.selectColumns || defaultLogsSelectedColumns;
|
||||
formatting = {
|
||||
maxLines: parsedExtraData?.maxLines ?? 2,
|
||||
format: parsedExtraData?.format ?? 'table',
|
||||
fontSize: parsedExtraData?.fontSize ?? 'small',
|
||||
version: parsedExtraData?.version ?? 1,
|
||||
};
|
||||
}
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
columns = parsedExtraData?.selectColumns || defaultTracesSelectedColumns;
|
||||
}
|
||||
setSavedViewPreferences({ columns, formatting });
|
||||
}, [viewsData, dataSource, savedViewId, mode]);
|
||||
|
||||
// We are using a reSync state because we have URL updates as well as local storage updates
|
||||
// and we want to make sure we are always using the latest preferences
|
||||
const [reSync, setReSync] = useState(false);
|
||||
const { preferences, loading, error } = usePreferenceLoader({
|
||||
dataSource,
|
||||
reSync,
|
||||
setReSync,
|
||||
});
|
||||
|
||||
const { updateColumns, updateFormatting } = usePreferenceUpdater({
|
||||
dataSource,
|
||||
mode,
|
||||
preferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
});
|
||||
|
||||
return {
|
||||
preferences:
|
||||
mode === PreferenceMode.SAVED_VIEW && savedViewId
|
||||
? savedViewPreferences
|
||||
: preferences,
|
||||
loading,
|
||||
error,
|
||||
updateColumns,
|
||||
updateFormatting,
|
||||
};
|
||||
}
|
||||
32
frontend/src/providers/preferences/types/index.ts
Normal file
32
frontend/src/providers/preferences/types/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export enum PreferenceMode {
|
||||
SAVED_VIEW = 'savedView',
|
||||
DIRECT = 'direct',
|
||||
}
|
||||
|
||||
export interface PreferenceContextValue {
|
||||
preferences: Preferences | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
mode: PreferenceMode;
|
||||
savedViewId?: string;
|
||||
dataSource: DataSource;
|
||||
updateColumns: (newColumns: BaseAutocompleteData[]) => void;
|
||||
updateFormatting: (newFormatting: FormattingOptions) => void;
|
||||
}
|
||||
|
||||
export interface FormattingOptions {
|
||||
maxLines?: number;
|
||||
format?: LogViewMode;
|
||||
fontSize?: FontSize;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export interface Preferences {
|
||||
columns: BaseAutocompleteData[];
|
||||
formatting?: FormattingOptions;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
defaultOptionsQuery,
|
||||
URL_OPTIONS,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import getLogsUpdaterConfig from '../configs/logsUpdaterConfig';
|
||||
import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig';
|
||||
import { FormattingOptions, Preferences } from '../types';
|
||||
|
||||
const metricsUpdater = {
|
||||
updateColumns: (): void => {}, // no-op for metrics
|
||||
updateFormatting: (): void => {}, // no-op for metrics
|
||||
};
|
||||
|
||||
const getUpdaterConfig = (
|
||||
preferences: Preferences | null,
|
||||
redirectWithOptionsData: (options: OptionsQuery) => void,
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
|
||||
): Record<
|
||||
DataSource,
|
||||
{
|
||||
updateColumns: (newColumns: BaseAutocompleteData[], mode: string) => void;
|
||||
updateFormatting: (newFormatting: FormattingOptions, mode: string) => void;
|
||||
}
|
||||
> => ({
|
||||
[DataSource.LOGS]: getLogsUpdaterConfig(
|
||||
preferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
),
|
||||
[DataSource.TRACES]: getTracesUpdaterConfig(
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
),
|
||||
[DataSource.METRICS]: metricsUpdater,
|
||||
});
|
||||
|
||||
export function usePreferenceUpdater({
|
||||
dataSource,
|
||||
mode,
|
||||
preferences,
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
mode: string;
|
||||
preferences: Preferences | null;
|
||||
setReSync: Dispatch<SetStateAction<boolean>>;
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>;
|
||||
}): {
|
||||
updateColumns: (newColumns: BaseAutocompleteData[]) => void;
|
||||
updateFormatting: (newFormatting: FormattingOptions) => void;
|
||||
} {
|
||||
const {
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
const updater = getUpdaterConfig(
|
||||
preferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
)[dataSource];
|
||||
|
||||
return {
|
||||
updateColumns: (newColumns: BaseAutocompleteData[]): void => {
|
||||
updater.updateColumns(newColumns, mode);
|
||||
setReSync(true);
|
||||
},
|
||||
updateFormatting: (newFormatting: FormattingOptions): void => {
|
||||
updater.updateFormatting(newFormatting, mode);
|
||||
setReSync(true);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
@@ -32,10 +33,11 @@ type Module struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
orgSetter organization.Setter
|
||||
analytics analytics.Analytics
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
// This module is a WIP, don't take inspiration from this.
|
||||
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics) user.Module {
|
||||
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics, licensing licensing.Licensing) user.Module {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
|
||||
return &Module{
|
||||
store: store,
|
||||
@@ -44,6 +46,7 @@ func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emai
|
||||
settings: settings,
|
||||
orgSetter: orgSetter,
|
||||
analytics: analytics,
|
||||
licensing: licensing,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +168,14 @@ func (m *Module) CreateUserWithPassword(ctx context.Context, user *types.User, p
|
||||
},
|
||||
)
|
||||
|
||||
license, err := m.licensing.GetActive(ctx, valuer.MustNewUUID(user.OrgID))
|
||||
if err == nil {
|
||||
m.analytics.Send(ctx, analyticstypes.Group{
|
||||
UserId: user.ID.String(),
|
||||
GroupId: license.ID.StringValue(),
|
||||
})
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/analytics/analyticstest"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
@@ -40,7 +42,10 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(err)
|
||||
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||
require.Nil(apiErr)
|
||||
|
||||
@@ -97,7 +102,9 @@ func TestAgentCheckIns(t *testing.T) {
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(err)
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||
require.Nil(apiErr)
|
||||
|
||||
@@ -193,7 +200,9 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(err)
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||
require.Nil(apiErr)
|
||||
|
||||
@@ -221,7 +230,9 @@ func TestConfigureService(t *testing.T) {
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(err)
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||
require.Nil(apiErr)
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/analytics/analyticstest"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||
@@ -33,7 +35,9 @@ func TestIntegrationLifecycle(t *testing.T) {
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(store, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(err)
|
||||
modules := signoz.NewModules(store, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||
if apiErr != nil {
|
||||
t.Fatalf("could not create test user: %v", apiErr)
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/analytics/analyticstest"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -317,7 +319,10 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(testDB, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
modules := signoz.NewModules(testDB, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
handlers := signoz.NewHandlers(modules)
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/analytics/analyticstest"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
@@ -493,7 +495,9 @@ func NewTestbedWithoutOpamp(t *testing.T, sqlStore sqlstore.SQLStore) *LogPipeli
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(t, err)
|
||||
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
handlers := signoz.NewHandlers(modules)
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/analytics/analyticstest"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -378,7 +380,10 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(testDB, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
modules := signoz.NewModules(testDB, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
handlers := signoz.NewHandlers(modules)
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
@@ -584,7 +586,9 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
analytics := analyticstest.New()
|
||||
modules := signoz.NewModules(testDB, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(t, err)
|
||||
modules := signoz.NewModules(testDB, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
handlers := signoz.NewHandlers(modules)
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||
@@ -33,7 +35,9 @@ func TestNewHandlers(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
modules := NewModules(sqlstore, jwt, emailing, providerSettings, orgGetter, alertmanager, nil)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(t, err)
|
||||
modules := NewModules(sqlstore, jwt, emailing, providerSettings, orgGetter, alertmanager, nil, licensing)
|
||||
|
||||
handlers := NewHandlers(modules)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/apdex"
|
||||
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
@@ -46,10 +47,11 @@ func NewModules(
|
||||
orgGetter organization.Getter,
|
||||
alertmanager alertmanager.Alertmanager,
|
||||
analytics analytics.Analytics,
|
||||
licensing licensing.Licensing,
|
||||
) Modules {
|
||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
||||
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), jwt, emailing, providerSettings, orgSetter, analytics)
|
||||
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), jwt, emailing, providerSettings, orgSetter, analytics, licensing)
|
||||
return Modules{
|
||||
OrgGetter: orgGetter,
|
||||
OrgSetter: orgSetter,
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||
@@ -33,7 +35,10 @@ func TestNewModules(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||
emailing := emailingtest.New()
|
||||
modules := NewModules(sqlstore, jwt, emailing, providerSettings, orgGetter, alertmanager, nil)
|
||||
licensing, err := nooplicensing.New(context.TODO(), providerSettings, licensing.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
modules := NewModules(sqlstore, jwt, emailing, providerSettings, orgGetter, alertmanager, nil, licensing)
|
||||
|
||||
reflectVal := reflect.ValueOf(modules)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
|
||||
@@ -246,7 +246,7 @@ func New(
|
||||
}
|
||||
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics)
|
||||
modules := NewModules(sqlstore, jwt, emailing, providerSettings, orgGetter, alertmanager, analytics, licensing)
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
handlers := NewHandlers(modules)
|
||||
|
||||
Reference in New Issue
Block a user