feat: preserve last used saved view in explorer pages (#5453)

* feat: preserve last used saved view in explorer pages
This commit is contained in:
Shaheer Kochai 2024-09-10 11:31:43 +04:30 committed by GitHub
parent ee1e2b824f
commit 3c151e3adb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 120 additions and 7 deletions

View File

@ -19,5 +19,6 @@ export enum LOCALSTORAGE {
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR', SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES', PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1', THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS', SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
} }

View File

@ -19,6 +19,7 @@ import axios from 'axios';
import cx from 'classnames'; import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils'; import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -48,6 +49,7 @@ import {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
useCallback, useCallback,
useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@ -61,7 +63,9 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
import { PreservedViewsTypes } from './constants';
import ExplorerOptionsHideArea from './ExplorerOptionsHideArea'; import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
import { PreservedViewsInLocalStorage } from './types';
import { import {
DATASOURCE_VS_ROUTES, DATASOURCE_VS_ROUTES,
generateRGBAFromHex, generateRGBAFromHex,
@ -90,6 +94,12 @@ function ExplorerOptions({
const history = useHistory(); const history = useHistory();
const ref = useRef<RefSelectProps>(null); const ref = useRef<RefSelectProps>(null);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const isLogsExplorer = sourcepage === DataSource.LOGS;
const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS;
const PRESERVED_VIEW_TYPE = isLogsExplorer
? PreservedViewsTypes.LOGS
: PreservedViewsTypes.TRACES;
const onModalToggle = useCallback((value: boolean) => { const onModalToggle = useCallback((value: boolean) => {
setIsExport(value); setIsExport(value);
@ -107,7 +117,7 @@ function ExplorerOptions({
logEvent('Traces Explorer: Save view clicked', { logEvent('Traces Explorer: Save view clicked', {
panelType, panelType,
}); });
} else if (sourcepage === DataSource.LOGS) { } else if (isLogsExplorer) {
logEvent('Logs Explorer: Save view clicked', { logEvent('Logs Explorer: Save view clicked', {
panelType, panelType,
}); });
@ -141,7 +151,7 @@ function ExplorerOptions({
logEvent('Traces Explorer: Create alert', { logEvent('Traces Explorer: Create alert', {
panelType, panelType,
}); });
} else if (sourcepage === DataSource.LOGS) { } else if (isLogsExplorer) {
logEvent('Logs Explorer: Create alert', { logEvent('Logs Explorer: Create alert', {
panelType, panelType,
}); });
@ -166,7 +176,7 @@ function ExplorerOptions({
logEvent('Traces Explorer: Add to dashboard clicked', { logEvent('Traces Explorer: Add to dashboard clicked', {
panelType, panelType,
}); });
} else if (sourcepage === DataSource.LOGS) { } else if (isLogsExplorer) {
logEvent('Logs Explorer: Add to dashboard clicked', { logEvent('Logs Explorer: Add to dashboard clicked', {
panelType, panelType,
}); });
@ -265,6 +275,31 @@ function ExplorerOptions({
[viewsData, handleExplorerTabChange], [viewsData, handleExplorerTabChange],
); );
const updatePreservedViewInLocalStorage = (option: {
key: string;
value: string;
}): void => {
// Retrieve stored views from local storage
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
// Initialize or parse the stored views
const updatedViews: PreservedViewsInLocalStorage = storedViews
? JSON.parse(storedViews)
: {};
// Update the views with the new selection
updatedViews[PRESERVED_VIEW_TYPE] = {
key: option.key,
value: option.value,
};
// Save the updated views back to local storage
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(updatedViews),
);
};
const handleSelect = ( const handleSelect = (
value: string, value: string,
option: { key: string; value: string }, option: { key: string; value: string },
@ -277,18 +312,42 @@ function ExplorerOptions({
panelType, panelType,
viewName: option?.value, viewName: option?.value,
}); });
} else if (sourcepage === DataSource.LOGS) { } else if (isLogsExplorer) {
logEvent('Logs Explorer: Select view', { logEvent('Logs Explorer: Select view', {
panelType, panelType,
viewName: option?.value, viewName: option?.value,
}); });
} }
updatePreservedViewInLocalStorage(option);
if (ref.current) { if (ref.current) {
ref.current.blur(); ref.current.blur();
} }
}; };
const removeCurrentViewFromLocalStorage = (): void => {
// Retrieve stored views from local storage
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
if (storedViews) {
// Parse the stored views
const parsedViews = JSON.parse(storedViews);
// Remove the current view type from the parsed views
delete parsedViews[PRESERVED_VIEW_TYPE];
// Update local storage with the modified views
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(parsedViews),
);
}
};
const handleClearSelect = (): void => { const handleClearSelect = (): void => {
removeCurrentViewFromLocalStorage();
history.replace(DATASOURCE_VS_ROUTES[sourcepage]); history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
}; };
@ -323,7 +382,7 @@ function ExplorerOptions({
panelType, panelType,
viewName: newViewName, viewName: newViewName,
}); });
} else if (sourcepage === DataSource.LOGS) { } else if (isLogsExplorer) {
logEvent('Logs Explorer: Save view successful', { logEvent('Logs Explorer: Save view successful', {
panelType, panelType,
viewName: newViewName, viewName: newViewName,
@ -358,6 +417,44 @@ function ExplorerOptions({
const isEditDeleteSupported = allowedRoles.includes(role as string); const isEditDeleteSupported = allowedRoles.includes(role as string);
const [
isRecentlyUsedSavedViewSelected,
setIsRecentlyUsedSavedViewSelected,
] = useState(false);
useEffect(() => {
const parsedPreservedView = JSON.parse(
localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
);
const preservedView = parsedPreservedView[PRESERVED_VIEW_TYPE] || {};
let timeoutId: string | number | NodeJS.Timeout | undefined;
if (
!!preservedView?.key &&
viewsData?.data?.data &&
!(!!viewName || !!viewKey) &&
!isRecentlyUsedSavedViewSelected
) {
// prevent the race condition with useShareBuilderUrl
timeoutId = setTimeout(() => {
onMenuItemSelectHandler({ key: preservedView.key });
}, 0);
setIsRecentlyUsedSavedViewSelected(false);
}
return (): void => clearTimeout(timeoutId);
}, [
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
PRESERVED_VIEW_TYPE,
isRecentlyUsedSavedViewSelected,
onMenuItemSelectHandler,
viewKey,
viewName,
viewsData?.data?.data,
]);
return ( return (
<div className="explorer-options-container"> <div className="explorer-options-container">
{isQueryUpdated && !isExplorerOptionHidden && ( {isQueryUpdated && !isExplorerOptionHidden && (
@ -476,12 +573,12 @@ function ExplorerOptions({
<Tooltip <Tooltip
title={ title={
<div> <div>
{sourcepage === DataSource.LOGS {isLogsExplorer
? 'Learn more about Logs explorer ' ? 'Learn more about Logs explorer '
: 'Learn more about Traces explorer '} : 'Learn more about Traces explorer '}
<Typography.Link <Typography.Link
href={ href={
sourcepage === DataSource.LOGS isLogsExplorer
? 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar' ? 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar'
: 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar' : 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar'
} }

View File

@ -0,0 +1,4 @@
export enum PreservedViewsTypes {
LOGS = 'logs',
TRACES = 'traces',
}

View File

@ -8,6 +8,8 @@ import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types'; import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder'; import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import { PreservedViewsTypes } from './constants';
export interface SaveNewViewHandlerProps { export interface SaveNewViewHandlerProps {
viewName: string; viewName: string;
compositeQuery: ICompositeMetricQuery; compositeQuery: ICompositeMetricQuery;
@ -26,3 +28,11 @@ export interface SaveNewViewHandlerProps {
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData']; redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
setNewViewName: Dispatch<SetStateAction<string>>; setNewViewName: Dispatch<SetStateAction<string>>;
} }
export type PreservedViewType =
| PreservedViewsTypes.LOGS
| PreservedViewsTypes.TRACES;
export type PreservedViewsInLocalStorage = Partial<
Record<PreservedViewType, { key: string; value: string }>
>;

View File

@ -140,6 +140,7 @@ function TimeSeriesView({
className="graph-container" className="graph-container"
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
ref={graphRef} ref={graphRef}
data-testid="time-series-graph"
> >
{isLoading && {isLoading &&
(dataSource === DataSource.LOGS ? <LogsLoading /> : <TracesLoading />)} (dataSource === DataSource.LOGS ? <LogsLoading /> : <TracesLoading />)}