Compare commits

...

7 Commits

Author SHA1 Message Date
Abhi Kumar
b22a18758a chore(dashboard): retire the V1 variable runtime
The V1 variable engine had no writers left: nothing wrote selectedValue, so
getDashboardVariables produced undefined values, variableFetchStore was never
updated, and the dependency graph and derived store fields only fed that store.
The shared store's one remaining job is publishing the open dashboard's dynamic
variables for query-builder autocomplete, which needs a name and an attribute.

Replace it with a suggestion feed and delete the rest, including the panel
variables prop that no GridCard caller passed and useResolveQuery's dashboardData
option that no caller supplied. useGetResolvedText loses its only variable source
and becomes the title truncation its callers already used it for.

Assisted-by: Claude Opus 5
2026-08-27 13:54:49 +05:30
Abhi Kumar
0eeeed6de9 fix(dashboard): match the list page wording in the public legacy notice
The list page tells owners a legacy dashboard "isn't available in the new
experience"; the public notice said the same thing in different words.
Reuse the list page's phrasing so the two states read as one message, and
keep the owner-only recovery path, since public viewers are anonymous and
cannot retry the migration themselves.

Assisted-by: Claude Opus 5
2026-08-27 01:22:35 +05:30
Abhi Kumar
acaeaacbc6 chore(dashboard): drop dead fields from IDashboardVariable
modificationUUID, haveCustomValuesSelected, change and defaultValue have
no readers left now that the V1 variable-selection UI is gone. Type order
as the number the sort comparator already treats it as, and make that
comparator explicit about the variables that carry no order.

Assisted-by: Claude Opus 5
2026-08-27 01:22:35 +05:30
Abhi Kumar
052a7babb9 refactor(charts): drop the dead isGraphDisabled prop from ChartManager
The flag came from the V1 store's dashboard-lock state and disabled the
legend's series toggles. V2 gates editing on its own lock, not read-only
interactions like toggling a series, so the prop was left hardcoded false
when the V1 store went away. Remove it rather than rewire it.

Assisted-by: Claude Opus 5
2026-08-27 01:22:35 +05:30
Abhi Kumar
801cc097b5 fix(dashboard): carry the dashboard name on panel-action analytics
The V1 panel-action events sent dashboardName alongside dashboardId;
retiring the V1 store dropped the name with no V2 replacement, because the
V2 store deliberately holds no spec. Read it off the loaded dashboard
instead and send the pair from every panel-action event, so clone, delete,
move and create-alert all report the same dashboard identity.

Assisted-by: Claude Opus 5
2026-08-27 01:22:35 +05:30
Abhi Kumar
e1bef67cf1 refactor(widgets): group the panel stack under WidgetCard/Panels
WidgetCard listed PanelWrapper, TablePanel and ValuePanel as siblings of
Card, EmptyWidget and Header, so the panel renderers read as peers of the
card shell rather than as its contents. Collect them under one Panels
folder and flatten PanelWrapper's nested panels/ directory into it, so the
folder is card shell (Card/Header/EmptyWidget) plus the panels it renders.

Pure moves and import-path updates.

Assisted-by: Claude Opus 5
2026-08-27 01:22:35 +05:30
Abhi Kumar
cf51dee878 refactor(dashboard): drop the V2 suffix now that V1 is gone
With no V1 dashboard code left, the suffix distinguishes nothing.

  pages/DashboardPageV2/                   -> pages/DashboardPage/
  pages/DashboardsListPageV2/              -> pages/DashboardsListPage/
  pages/PublicDashboard/PublicDashboardV2/ -> pages/PublicDashboard/PublicDashboardView/

The public renderer keeps its own directory rather than being flattened into
the page, which would have collided two __tests__ folders; it is renamed to
PublicDashboardView to say what it is next to the route entry and the legacy
notice.

Also updates the no-dashboard-fetch-outside-root allowlist in .oxlintrc.json
and the CODEOWNERS entries. LOCALSTORAGE.DASHBOARD_V2_PANEL_COLUMN_WIDTHS is
deliberately untouched: its value is persisted in users' browsers and renaming
it would discard saved column widths.
2026-08-27 01:22:17 +05:30
803 changed files with 753 additions and 4108 deletions

View File

@@ -565,12 +565,12 @@
}
},
{
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
// Root dashboard pages own the fetch lifecycle; useDashboardFetchRequired wraps it.
// Everywhere else must use useDashboardFetchRequired().
"files": [
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
"src/pages/DashboardPage/DashboardPage.tsx",
"src/pages/DashboardPage/PanelEditorPage/PanelEditorPage.tsx",
"src/pages/DashboardPage/DashboardContainer/hooks/useDashboardFetchRequired.ts"
],
"rules": {
"signoz/no-dashboard-fetch-outside-root": "off"

View File

@@ -94,18 +94,18 @@ export const OnboardingV2 = Loadable(
export const DashboardsListPage = Loadable(
() =>
import(
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPageV2'
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage'
),
);
export const DashboardPage = Loadable(
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPageV2'),
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
);
export const DashboardPanelEditorPage = Loadable(
() =>
import(
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPageV2/PanelEditorPage/PanelEditorPage'
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPage/PanelEditorPage/PanelEditorPage'
),
);

View File

@@ -671,9 +671,9 @@ export const prepareQueryRangePayloadV5 = ({
(acc, [key, value]) => {
acc[key] = {
value,
type: dynamicVariables
?.find((v) => v.name === key)
?.type?.toLowerCase() as VariableType,
type: dynamicVariables?.some((v) => v.name === key)
? ('dynamic' as VariableType)
: undefined,
};
return acc;
},

View File

@@ -27,7 +27,7 @@ import {
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
queryOperatorSuggestions,
} from 'constants/antlrQueryConstants';
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
@@ -258,10 +258,7 @@ function QuerySearch({
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
const dashboardDynamicVariables = useDashboardVariablesByType(
'DYNAMIC',
'values',
);
const dashboardDynamicVariables = useDynamicVariableSuggestions();
// Add back the generateOptions function and useEffect
const generateOptions = (keys: {
@@ -1189,7 +1186,7 @@ function QuerySearch({
// Add dynamic variables suggestions for the current key
const variableName = dashboardDynamicVariables?.find(
(variable) => variable?.dynamicVariablesAttribute === keyName,
(variable) => variable?.attribute === keyName,
)?.name;
if (variableName) {

View File

@@ -1,4 +1,4 @@
import { evaluateThresholdWithConvertedValue } from 'container/WidgetCard/TablePanel/utils';
import { evaluateThresholdWithConvertedValue } from 'container/WidgetCard/Panels/TablePanel/utils';
import { ThresholdProps } from 'types/api/widgets/threshold';
function doesValueSatisfyThreshold(

View File

@@ -1,6 +1,6 @@
import Uplot from 'components/Uplot';
import GridTableComponent from 'container/WidgetCard/TablePanel';
import GridValueComponent from 'container/WidgetCard/ValuePanel';
import GridTableComponent from 'container/WidgetCard/Panels/TablePanel';
import GridValueComponent from 'container/WidgetCard/Panels/ValuePanel';
import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent';
import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -2,7 +2,7 @@ import type { MessageContext } from 'api/ai-assistant/chat';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { AlertListTabs } from 'pages/AlertList/types';
import { NEW_PANEL_ID } from 'pages/DashboardPageV2/DashboardContainer/PanelEditor/newPanelRoute';
import { NEW_PANEL_ID } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
import { matchPath } from 'react-router-dom';
/**

View File

@@ -12,7 +12,7 @@ import {
} from 'tests/test-utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { buildExportPanelLink } from 'pages/DashboardPageV2/DashboardContainer/PanelEditor/newPanelRoute';
import { buildExportPanelLink } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
import ExplorerOptionWrapper from '../ExplorerOptionWrapper';
import { getExplorerToolBarVisibility } from '../utils';

View File

@@ -1,4 +1,4 @@
import DashboardContainer from 'pages/DashboardPageV2/DashboardContainer';
import DashboardContainer from 'pages/DashboardPage/DashboardContainer';
import { useSeededDashboardV2 } from './hooks/useSeededDashboardV2';
import styles from './Overview.module.scss';

View File

@@ -13,7 +13,7 @@ import LLMObservability from '../LLMObservability';
// The Overview tab renders the full V2 DashboardContainer (toolbar + date picker
// call useNavigationType, which needs a data router this integration test doesn't
// set up). These cases assert tab routing, not dashboard rendering, so stub it.
jest.mock('pages/DashboardPageV2/DashboardContainer', () => ({
jest.mock('pages/DashboardPage/DashboardContainer', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="llm-overview-dashboard" />,
}));

View File

@@ -21,7 +21,7 @@ import {
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import type { WhereClauseConfig } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
@@ -263,10 +263,7 @@ function QueryBuilderSearchV2(
return false;
}, [currentState, query.aggregateAttribute?.dataType, query.dataSource]);
const dashboardDynamicVariables = useDashboardVariablesByType(
'DYNAMIC',
'values',
);
const dashboardDynamicVariables = useDynamicVariableSuggestions();
const { data, isFetching } = useGetAggregateKeys(
{
@@ -817,8 +814,7 @@ function QueryBuilderSearchV2(
// here we want to suggest the variable name matching with the key here, we will go over the dynamic variables for the keys
const variableName = dashboardDynamicVariables?.find(
(variable) =>
variable?.dynamicVariablesAttribute === currentFilterItem?.key?.key,
(variable) => variable?.attribute === currentFilterItem?.key?.key,
)?.name;
if (variableName) {

View File

@@ -5,9 +5,8 @@ import {
initialQueriesMap,
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import { IUseDashboardVariablesReturn } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { IDashboardVariable } from 'types/api/dashboard/variables';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
@@ -150,24 +149,14 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
// Mock dashboard variables
const dashboardVariables = {
service: {
id: 'service',
name: 'service',
type: 'DYNAMIC' as IDashboardVariable['type'],
dynamicVariablesAttribute: 'service.name',
description: '',
sort: 'DISABLED' as IDashboardVariable['sort'],
multiSelect: false,
showALLOption: false,
},
};
// Mock the dynamic variables the open dashboard would publish
const dynamicVariableSuggestions = [
{ name: 'service', attribute: 'service.name' },
];
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
useDashboardVariables: (): IUseDashboardVariablesReturn => ({
dashboardVariables: dashboardVariables,
}),
jest.mock('hooks/dashboard/useDynamicVariableSuggestions', () => ({
useDynamicVariableSuggestions: (): DynamicVariableSuggestion[] =>
dynamicVariableSuggestions,
}));
describe('Suggestion Key -> Operator -> Value Flow', () => {

View File

@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import {
fromPerses,
toPerses,
} from 'pages/DashboardPageV2/DashboardContainer/queryV5/persesQueryAdapters';
} from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
import { ClickedData } from 'periscope/components/ContextMenu';
import { getGroupContextMenuConfig } from '../contextConfig';

View File

@@ -6,7 +6,7 @@ import {
QUERY_BUILDER_OPERATORS_BY_TYPES,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { isApmMetric } from 'container/WidgetCard/PanelWrapper/utils';
import { isApmMetric } from 'container/WidgetCard/Panels/utils';
import {
applyMappingsToExpression,
DRILLDOWN_TO_LOGS_MAPPINGS,

View File

@@ -2,7 +2,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import { ServicesList } from 'types/api/metrics/getService';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { EQueryType } from 'types/common/dashboard';
@@ -47,7 +46,6 @@ export const getQueryRangeRequestData = ({
graphType: serviceMetricsWidget?.panelTypes,
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(),
});
});
return requestData;

View File

@@ -1,7 +1,7 @@
import { MutableRefObject } from 'react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'lib/visualization/panels/types';
import PanelWrapper from 'container/WidgetCard/PanelWrapper/PanelWrapper';
import PanelWrapper from 'container/WidgetCard/Panels/PanelWrapper';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { render, screen, waitFor } from 'tests/test-utils';
import { Widgets } from 'types/api/widgets/widget';
@@ -10,7 +10,7 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
// Mock dependencies
jest.mock('container/WidgetCard/PanelWrapper/constants', () => ({
jest.mock('container/WidgetCard/Panels/constants', () => ({
PanelTypeVsPanelWrapper: {
[PANEL_TYPES.TIME_SERIES]: ({
onDragSelect,

View File

@@ -26,15 +26,13 @@ import { PanelMode } from 'lib/visualization/panels/types';
import useDrilldown from 'container/WidgetCard/Card/FullView/useDrilldown';
import { populateMultipleResults } from 'lib/query/populateMultipleResults';
import { timeItems, timePreferance } from 'constants/timePreference';
import PanelWrapper from 'container/WidgetCard/PanelWrapper/PanelWrapper';
import PanelWrapper from 'container/WidgetCard/Panels/PanelWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useChartMutable } from 'hooks/useChartMutable';
import useUrlQuery from 'hooks/useUrlQuery';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import GetMinMax from 'lib/getMinMax';
import { isEmpty } from 'lodash-es';
import { AppState } from 'store/reducers';
@@ -82,8 +80,6 @@ function FullView({
setCurrentGraphRef(fullViewRef);
}, [setCurrentGraphRef]);
const { dashboardVariables } = useDashboardVariables();
const getSelectedTime = useCallback(
() =>
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
@@ -115,7 +111,6 @@ function FullView({
graphType: getGraphType(selectedPanelType),
query: updatedQuery,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(dashboardVariables),
fillGaps: widget.fillSpans,
formatForWeb: selectedPanelType === PANEL_TYPES.TABLE,
originalGraphType: selectedPanelType,
@@ -126,7 +121,6 @@ function FullView({
graphType: PANEL_TYPES.LIST,
selectedTime: widget?.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(dashboardVariables),
tableParams: {
pagination: {
offset: 0,

View File

@@ -8,7 +8,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'lib/visualization/panels/types';
import PanelWrapper from 'container/WidgetCard/PanelWrapper/PanelWrapper';
import PanelWrapper from 'container/WidgetCard/Panels/PanelWrapper';
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';

View File

@@ -8,12 +8,9 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { useScrollWidgetIntoView } from 'lib/visualization/hooks/useScrollWidgetIntoView';
import { populateMultipleResults } from 'lib/query/populateMultipleResults';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsPanelWaitingOnVariable } from 'hooks/dashboard/useVariableFetchState';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import { getVariableReferencesInQuery } from 'lib/dashboardVariables/variableReference';
import getTimeString from 'lib/getTimeString';
import { isEqual } from 'lodash-es';
import isEmpty from 'lodash-es/isEmpty';
@@ -45,7 +42,6 @@ function GridCardGraph({
headerMenuList = [MenuItemKeys.View],
isQueryEnabled,
threshold,
variables,
version,
onClickHandler,
onDragSelect,
@@ -113,25 +109,10 @@ function GridCardGraph({
const updatedQuery = widget?.query;
const referencedVariableNames = useMemo(() => {
if (!variables || !updatedQuery) {
return [];
}
const allNames = Object.values(variables)
.map((v) => v.name)
.filter((name): name is string => !!name);
return getVariableReferencesInQuery(updatedQuery, allNames);
}, [updatedQuery, variables]);
const isEmptyWidget =
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
const isPanelWaitingOnAnyVariable = useIsPanelWaitingOnVariable(
referencedVariableNames,
);
const queryEnabledCondition =
isVisible && !isEmptyWidget && isQueryEnabled && !isPanelWaitingOnAnyVariable;
const queryEnabledCondition = isVisible && !isEmptyWidget && isQueryEnabled;
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
@@ -140,7 +121,6 @@ function GridCardGraph({
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(variables),
fillGaps: widget.fillSpans,
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
start: customTimeRange?.startTime || start,
@@ -191,7 +171,6 @@ function GridCardGraph({
const queryResponse = useGetQueryRange(
{
...requestData,
variables: getDashboardVariables(variables),
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval:
widget?.panelTypes === PANEL_TYPES.LIST && isLogsQuery
@@ -214,14 +193,6 @@ function GridCardGraph({
widget.timePreferance,
widget.fillSpans,
requestData,
variables
? Object.entries(variables).reduce((acc, [id, variable]) => {
if (variable.name && referencedVariableNames.includes(variable.name)) {
return { ...acc, [id]: variable.selectedValue };
}
return acc;
}, {})
: {},
...(customTimeRange && customTimeRange.startTime && customTimeRange.endTime
? [customTimeRange.startTime, customTimeRange.endTime]
: []),
@@ -303,9 +274,7 @@ function GridCardGraph({
version={version}
threshold={threshold}
headerMenuList={menuList}
isFetchingResponse={
queryResponse.isFetching || isPanelWaitingOnAnyVariable
}
isFetchingResponse={queryResponse.isFetching}
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}

View File

@@ -4,7 +4,6 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { Widgets } from 'types/api/widgets/widget';
import {
MetricQueryRangeSuccessResponse,
@@ -52,7 +51,6 @@ export interface GridCardGraphProps {
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
onClickHandler?: OnClickPluginOpts['onClick'];
isQueryEnabled: boolean;
variables?: IDashboardVariables;
version?: string;
onDragSelect: (start: number, end: number) => void;
customOnDragSelect?: (start: number, end: number) => void;

View File

@@ -1,21 +0,0 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import BarPanel from 'container/WidgetCard/PanelWrapper/panels/BarPanel/BarPanel';
import HistogramPanel from 'container/WidgetCard/PanelWrapper/panels/HistogramPanel/HistogramPanel';
import TimeSeriesPanel from 'container/WidgetCard/PanelWrapper/panels/TimeSeriesPanel/TimeSeriesPanel';
import ListPanelWrapper from 'container/WidgetCard/PanelWrapper/ListPanelWrapper';
import PiePanelWrapper from 'container/WidgetCard/PanelWrapper/PiePanelWrapper';
import TablePanelWrapper from 'container/WidgetCard/PanelWrapper/TablePanelWrapper';
import ValuePanelWrapper from 'container/WidgetCard/PanelWrapper/ValuePanelWrapper';
export const PanelTypeVsPanelWrapper = {
[PANEL_TYPES.TIME_SERIES]: TimeSeriesPanel,
[PANEL_TYPES.TABLE]: TablePanelWrapper,
[PANEL_TYPES.LIST]: ListPanelWrapper,
[PANEL_TYPES.VALUE]: ValuePanelWrapper,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
[PANEL_TYPES.PIE]: PiePanelWrapper,
[PANEL_TYPES.BAR]: BarPanel,
[PANEL_TYPES.HISTOGRAM]: HistogramPanel,
};

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PanelWrapperProps } from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import {
@@ -17,11 +17,11 @@ import { getTimeRange } from 'utils/getTimeRange';
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
import ChartManager from 'lib/visualization/components/ChartManager/ChartManager';
import { usePanelContextMenu } from 'container/WidgetCard/PanelWrapper/hooks/usePanelContextMenu';
import { usePanelContextMenu } from 'container/WidgetCard/Panels/hooks/usePanelContextMenu';
import { PanelMode } from 'lib/visualization/panels/types';
import { prepareBarPanelConfig } from 'container/WidgetCard/PanelWrapper/panels/BarPanel/utils';
import { prepareBarPanelConfig } from 'container/WidgetCard/Panels/BarPanel/utils';
import 'container/WidgetCard/PanelWrapper/panels/Panel.styles.scss';
import 'container/WidgetCard/Panels/Panel.styles.scss';
import TooltipFooter from 'lib/visualization/panels/components/TooltipFooter';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
import { StackMode } from 'lib/uPlotV2/config/types';

View File

@@ -6,7 +6,7 @@ import {
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { PanelMode } from 'lib/visualization/panels/types';
import { prepareBarPanelConfig } from 'container/WidgetCard/PanelWrapper/panels/BarPanel/utils';
import { prepareBarPanelConfig } from 'container/WidgetCard/Panels/BarPanel/utils';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
jest.mock('lib/visualization/panels/utils/legendVisibilityUtils', () => ({

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo, useRef } from 'react';
import { PanelWrapperProps } from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import {
@@ -13,9 +13,9 @@ import ChartManager from 'lib/visualization/components/ChartManager/ChartManager
import {
prepareHistogramPanelConfig,
prepareHistogramPanelData,
} from 'container/WidgetCard/PanelWrapper/panels/HistogramPanel/utils';
} from 'container/WidgetCard/Panels/HistogramPanel/utils';
import 'container/WidgetCard/PanelWrapper/panels/Panel.styles.scss';
import 'container/WidgetCard/Panels/Panel.styles.scss';
import TooltipFooter from 'lib/visualization/panels/components/TooltipFooter';
function HistogramPanel(props: PanelWrapperProps): JSX.Element {

View File

@@ -9,7 +9,7 @@ import {
MetricRangePayloadProps,
} from 'types/api/metrics/getQueryRange';
import HistogramPanel from 'container/WidgetCard/PanelWrapper/panels/HistogramPanel/HistogramPanel';
import HistogramPanel from 'container/WidgetCard/Panels/HistogramPanel/HistogramPanel';
jest.mock('hooks/useDimensions', () => ({
useResizeObserver: jest.fn().mockReturnValue({ width: 800, height: 400 }),

View File

@@ -2,7 +2,7 @@ import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent';
import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent';
import { DataSource } from 'types/common/queryBuilder';
import { PanelWrapperProps } from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
function ListPanelWrapper({
widget,

View File

@@ -2,8 +2,8 @@ import { FC, useMemo } from 'react';
import Spinner from 'components/Spinner';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { PanelTypeVsPanelWrapper } from 'container/WidgetCard/PanelWrapper/constants';
import { PanelWrapperProps } from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { PanelTypeVsPanelWrapper } from 'container/WidgetCard/Panels/constants';
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
function PanelWrapper({
widget,

View File

@@ -13,14 +13,11 @@ import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
import {
PanelWrapperProps,
TooltipData,
} from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { preparePieChartData } from 'container/WidgetCard/PanelWrapper/preparePieChartData';
import {
lightenColor,
tooltipStyles,
} from 'container/WidgetCard/PanelWrapper/utils';
} from 'container/WidgetCard/Panels/panelWrapper.types';
import { preparePieChartData } from 'container/WidgetCard/Panels/preparePieChartData';
import { lightenColor, tooltipStyles } from 'container/WidgetCard/Panels/utils';
import 'container/WidgetCard/PanelWrapper/PiePanelWrapper.styles.scss';
import 'container/WidgetCard/Panels/PiePanelWrapper.styles.scss';
// reference: https://www.youtube.com/watch?v=bL3P9CqQkKw
function PiePanelWrapper({

View File

@@ -4,7 +4,7 @@ import {
createColumnsAndDataSource,
getQueryLegend,
sortFunction,
} from 'container/WidgetCard/TablePanel/utils';
} from 'container/WidgetCard/Panels/TablePanel/utils';
import {
expectedOutputQBv5MultiAggregations,
expectedOutputWithLegends,
@@ -12,7 +12,7 @@ import {
tableDataQBv5MultiAggregations,
widgetQueryQBv5MultiAggregations,
widgetQueryWithLegend,
} from 'container/WidgetCard/TablePanel/__tests__/response';
} from 'container/WidgetCard/Panels/TablePanel/__tests__/response';
describe('Table Panel utils', () => {
it('createColumnsAndDataSource function', () => {

View File

@@ -12,15 +12,15 @@ import LineClampedText from 'periscope/components/LineClampedText/LineClampedTex
import styled from 'styled-components';
import { eventEmitter } from 'utils/getEventEmitter';
import { WrapperStyled } from 'container/WidgetCard/TablePanel/styles';
import { GridTableComponentProps } from 'container/WidgetCard/TablePanel/types';
import { WrapperStyled } from 'container/WidgetCard/Panels/TablePanel/styles';
import { GridTableComponentProps } from 'container/WidgetCard/Panels/TablePanel/types';
import {
createColumnsAndDataSource,
findMatchingThreshold,
TableData,
} from 'container/WidgetCard/TablePanel/utils';
} from 'container/WidgetCard/Panels/TablePanel/utils';
import 'container/WidgetCard/TablePanel/GridTableComponent.styles.scss';
import 'container/WidgetCard/Panels/TablePanel/GridTableComponent.styles.scss';
const ButtonWrapper = styled.div`
position: absolute;

View File

@@ -1,8 +1,8 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridTableComponent from 'container/WidgetCard/TablePanel';
import { GRID_TABLE_CONFIG } from 'container/WidgetCard/TablePanel/config';
import GridTableComponent from 'container/WidgetCard/Panels/TablePanel';
import { GRID_TABLE_CONFIG } from 'container/WidgetCard/Panels/TablePanel/config';
import { PanelWrapperProps } from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
function TablePanelWrapper({
widget,

View File

@@ -1,8 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import TimeSeries from 'lib/visualization/charts/TimeSeries/TimeSeries';
import ChartManager from 'lib/visualization/components/ChartManager/ChartManager';
import { usePanelContextMenu } from 'container/WidgetCard/PanelWrapper/hooks/usePanelContextMenu';
import { PanelWrapperProps } from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { usePanelContextMenu } from 'container/WidgetCard/Panels/hooks/usePanelContextMenu';
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import {
@@ -18,10 +18,10 @@ import { useTimezone } from 'providers/Timezone';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { prepareUPlotConfig } from 'container/WidgetCard/PanelWrapper/panels/TimeSeriesPanel/utils';
import { prepareUPlotConfig } from 'container/WidgetCard/Panels/TimeSeriesPanel/utils';
import { PanelMode } from 'lib/visualization/panels/types';
import 'container/WidgetCard/PanelWrapper/panels/Panel.styles.scss';
import 'container/WidgetCard/Panels/Panel.styles.scss';
import TooltipFooter from 'lib/visualization/panels/components/TooltipFooter';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';

View File

@@ -6,7 +6,7 @@ import {
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { PanelMode } from 'lib/visualization/panels/types';
import { prepareUPlotConfig } from 'container/WidgetCard/PanelWrapper/panels/TimeSeriesPanel/utils';
import { prepareUPlotConfig } from 'container/WidgetCard/Panels/TimeSeriesPanel/utils';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
jest.mock('lib/visualization/panels/utils/legendVisibilityUtils', () => ({

View File

@@ -11,8 +11,8 @@ import { EQueryType } from 'types/common/dashboard';
import {
TitleContainer,
ValueContainer,
} from 'container/WidgetCard/ValuePanel/styles';
import { GridValueComponentProps } from 'container/WidgetCard/ValuePanel/types';
} from 'container/WidgetCard/Panels/ValuePanel/styles';
import { GridValueComponentProps } from 'container/WidgetCard/Panels/ValuePanel/types';
function GridValueComponent({
data,

View File

@@ -1,7 +1,7 @@
import GridValueComponent from 'container/WidgetCard/ValuePanel';
import GridValueComponent from 'container/WidgetCard/Panels/ValuePanel';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { PanelWrapperProps } from 'container/WidgetCard/PanelWrapper/panelWrapper.types';
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
function ValuePanelWrapper({
widget,

View File

@@ -2,11 +2,11 @@ import { PanelMode } from 'lib/visualization/panels/types';
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/widgets/widget';
import TablePanelWrapper from 'container/WidgetCard/PanelWrapper/TablePanelWrapper';
import TablePanelWrapper from 'container/WidgetCard/Panels/TablePanelWrapper';
import {
tablePanelQueryResponse,
tablePanelWidgetQuery,
} from 'container/WidgetCard/PanelWrapper/__tests__/tablePanelWrapperHelper';
} from 'container/WidgetCard/Panels/__tests__/tablePanelWrapperHelper';
describe('Table panel wrappper tests', () => {
it('table should render fine with the query response and column units', () => {

View File

@@ -2,12 +2,12 @@ import { PanelMode } from 'lib/visualization/panels/types';
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/widgets/widget';
import ValuePanelWrapper from 'container/WidgetCard/PanelWrapper/ValuePanelWrapper';
import ValuePanelWrapper from 'container/WidgetCard/Panels/ValuePanelWrapper';
import {
thresholds,
valuePanelQueryResponse,
valuePanelWidget,
} from 'container/WidgetCard/PanelWrapper/__tests__/valuePanelWrapperHelper';
} from 'container/WidgetCard/Panels/__tests__/valuePanelWrapperHelper';
window.ResizeObserver =
window.ResizeObserver ||

View File

@@ -5,7 +5,7 @@ import {
applyEnhancedLegendStyling,
calculateEnhancedLegendConfig,
EnhancedLegendConfig,
} from 'container/WidgetCard/PanelWrapper/enhancedLegend';
} from 'container/WidgetCard/Panels/enhancedLegend';
describe('Enhanced Legend Functionality', () => {
const mockDimensions: Dimensions = {

View File

@@ -7,7 +7,7 @@ import { DataSource } from 'types/common/queryBuilder';
import {
getMockQuery,
getMockQueryData,
} from 'container/WidgetCard/PanelWrapper/__tests__/testUtils';
} from 'container/WidgetCard/Panels/__tests__/testUtils';
const mockQueryData = getMockQueryData();
const mockQuery = getMockQuery();

View File

@@ -18,7 +18,7 @@ jest.mock('uplot', () => {
});
// Mock dependencies
jest.mock('container/WidgetCard/PanelWrapper/enhancedLegend', () => ({
jest.mock('container/WidgetCard/Panels/enhancedLegend', () => ({
calculateEnhancedLegendConfig: jest.fn(() => ({
minHeight: 46,
maxHeight: 80,

View File

@@ -2,7 +2,7 @@ import { themeColors } from 'constants/theme';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
import { preparePieChartData } from 'container/WidgetCard/PanelWrapper/preparePieChartData';
import { preparePieChartData } from 'container/WidgetCard/Panels/preparePieChartData';
const options = { colorMap: themeColors.chartcolors };

View File

@@ -0,0 +1,21 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import BarPanel from 'container/WidgetCard/Panels/BarPanel/BarPanel';
import HistogramPanel from 'container/WidgetCard/Panels/HistogramPanel/HistogramPanel';
import TimeSeriesPanel from 'container/WidgetCard/Panels/TimeSeriesPanel/TimeSeriesPanel';
import ListPanelWrapper from 'container/WidgetCard/Panels/ListPanelWrapper';
import PiePanelWrapper from 'container/WidgetCard/Panels/PiePanelWrapper';
import TablePanelWrapper from 'container/WidgetCard/Panels/TablePanelWrapper';
import ValuePanelWrapper from 'container/WidgetCard/Panels/ValuePanelWrapper';
export const PanelTypeVsPanelWrapper = {
[PANEL_TYPES.TIME_SERIES]: TimeSeriesPanel,
[PANEL_TYPES.TABLE]: TablePanelWrapper,
[PANEL_TYPES.LIST]: ListPanelWrapper,
[PANEL_TYPES.VALUE]: ValuePanelWrapper,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
[PANEL_TYPES.PIE]: PiePanelWrapper,
[PANEL_TYPES.BAR]: BarPanel,
[PANEL_TYPES.HISTOGRAM]: HistogramPanel,
};

View File

@@ -3,7 +3,7 @@ import { UseQueryResult } from 'react-query';
import { Widgets } from 'types/api/widgets/widget';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { usePanelContextMenu } from 'container/WidgetCard/PanelWrapper/hooks/usePanelContextMenu';
import { usePanelContextMenu } from 'container/WidgetCard/Panels/hooks/usePanelContextMenu';
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
@@ -37,7 +37,7 @@ jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
})),
}));
jest.mock('container/WidgetCard/PanelWrapper/utils', () => ({
jest.mock('container/WidgetCard/Panels/utils', () => ({
isApmMetric: jest.fn(() => false),
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
}));

View File

@@ -3,7 +3,7 @@ import { UseQueryResult } from 'react-query';
import {
getTimeRangeFromStepInterval,
isApmMetric,
} from 'container/WidgetCard/PanelWrapper/utils';
} from 'container/WidgetCard/Panels/utils';
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import {

View File

@@ -26,8 +26,8 @@ jest.mock(
}),
);
jest.mock('hooks/dashboard/useDashboardVariablesByType', () => ({
useDashboardVariablesByType: (): unknown[] => mockDynamicVariables,
jest.mock('hooks/dashboard/useDynamicVariableSuggestions', () => ({
useDynamicVariableSuggestions: (): unknown[] => mockDynamicVariables,
}));
jest.mock('react-redux', () => ({
@@ -64,11 +64,12 @@ describe('useResolveQuery', () => {
expect(resolved).toBe(QUERY);
});
it('resolves through substitute_vars when the dashboard has variables', async () => {
it('resolves through substitute_vars when the dashboard has dynamic variables', async () => {
mockGetSubstituteVars.mockResolvedValue({
httpStatusCode: 200,
data: { compositeQuery: {} },
});
mockDynamicVariables.push({ name: 'env', attribute: 'deployment.env' });
const { result } = renderHook(() => useUpdatedQuery(), {
wrapper: MockQueryClientProvider,
@@ -76,13 +77,6 @@ describe('useResolveQuery', () => {
const resolved = await result.current.getUpdatedQuery({
widgetConfig: WIDGET_CONFIG,
dashboardData: {
data: {
variables: {
env: { name: 'env', selectedValue: 'prod' },
},
},
},
});
expect(mockGetSubstituteVars).toHaveBeenCalledTimes(1);

View File

@@ -7,8 +7,7 @@ import { getSubstituteVars } from 'api/dashboard/substitute_vars';
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'constants/timePreference';
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { AppState } from 'store/reducers';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -21,7 +20,6 @@ interface UseUpdatedQueryOptions {
panelTypes: PANEL_TYPES;
timePreferance: timePreferenceType;
};
dashboardData?: any;
}
interface UseUpdatedQueryResult {
@@ -37,21 +35,13 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
const queryRangeMutation = useMutation(getSubstituteVars);
const dashboardDynamicVariables = useDashboardVariablesByType(
'DYNAMIC',
'values',
);
const dashboardDynamicVariables = useDynamicVariableSuggestions();
const getUpdatedQuery = useCallback(
async ({
widgetConfig,
dashboardData,
}: UseUpdatedQueryOptions): Promise<Query> => {
const variables = getDashboardVariables(dashboardData?.data?.variables);
async ({ widgetConfig }: UseUpdatedQueryOptions): Promise<Query> => {
// `/substitute_vars` only rewrites `$variable` references, so on surfaces with no
// dashboard behind them (APM, Celery, API monitoring) the round-trip is a no-op.
if (isEmpty(variables) && isEmpty(dashboardDynamicVariables)) {
if (isEmpty(dashboardDynamicVariables)) {
return widgetConfig.query;
}
@@ -61,7 +51,6 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
graphType: getGraphType(widgetConfig.panelTypes),
selectedTime: widgetConfig.timePreferance,
globalSelectedInterval,
variables,
originalGraphType: widgetConfig.panelTypes,
dynamicVariables: dashboardDynamicVariables,
});

View File

@@ -1,242 +1,40 @@
import React from 'react';
import { renderHook } from '@testing-library/react';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import useGetResolvedText from '../useGetResolvedText';
// Create a mock function that we can modify per test
let mockDashboardVariables: IDashboardVariables = {};
// Mock the useDashboardVariables hook
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
useDashboardVariables: jest.fn(() => ({
dashboardVariables: mockDashboardVariables,
})),
}));
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
describe('useGetResolvedText', () => {
const SERVICE_VAR = 'test, app +2-|-test, app, frontend, env';
const SEVERITY_VAR = 'DEBUG, INFO-|-DEBUG, INFO';
const EXPECTED_FULL_TEXT =
'Logs count in test, app, frontend, env in DEBUG, INFO';
const TRUNCATED_SERVICE = 'test, app +2';
const TEXT_TEMPLATE = 'Logs count in $service.name in $severity';
const renderHookWithProps = (
props: {
text: string | React.ReactNode;
maxLength?: number;
matcher?: string;
},
variables?: Record<string, string | number | boolean>,
): any => {
if (variables) {
mockDashboardVariables = Object.entries(
variables,
).reduce<IDashboardVariables>((acc, [key, value]) => {
acc[key] = {
id: key,
name: key,
description: '',
type: 'CUSTOM' as const,
sort: 'DISABLED' as const,
multiSelect: false,
showALLOption: false,
selectedValue: value,
};
return acc;
}, {});
} else {
mockDashboardVariables = {};
}
return renderHook(() => useGetResolvedText(props));
};
it('should resolve variables with truncated and full text', () => {
const text = TEXT_TEMPLATE;
const variables = {
'service.name': SERVICE_VAR,
severity: SEVERITY_VAR,
};
const { result } = renderHookWithProps({ text }, variables);
expect(result.current.truncatedText).toBe(
`Logs count in ${TRUNCATED_SERVICE} in DEBUG, INFO`,
it('returns the text unchanged when it fits within maxLength', () => {
const { result } = renderHook(() =>
useGetResolvedText({ text: 'Logs count', maxLength: 100 }),
);
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
expect(result.current.fullText).toBe('Logs count');
expect(result.current.truncatedText).toBe('Logs count');
});
it('should handle text with maxLength truncation', () => {
const text = TEXT_TEMPLATE;
const variables = {
'service.name': SERVICE_VAR,
severity: SEVERITY_VAR,
};
it('returns the text unchanged when no maxLength is given', () => {
const text = 'a'.repeat(200);
const { result } = renderHook(() => useGetResolvedText({ text }));
const { result } = renderHookWithProps({ text, maxLength: 20 }, variables);
expect(result.current.truncatedText).toBe('Logs count in test, a...');
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
});
it('should handle multiple occurrences of the same variable', () => {
const text = 'Logs count in $service.name and $service.name';
const variables = {
'service.name': SERVICE_VAR,
};
const { result } = renderHookWithProps({ text }, variables);
expect(result.current.truncatedText).toBe(
'Logs count in test, app +2 and test, app +2',
);
expect(result.current.fullText).toBe(
'Logs count in test, app, frontend, env and test, app, frontend, env',
);
});
it('should handle different variable formats', () => {
const text =
'Logs in $service.name, {{service.name}}, [[service.name]] - $dyn-service.name';
const variables = {
'service.name': SERVICE_VAR,
'$dyn-service.name': 'dyn-1, dyn-2',
};
const { result } = renderHookWithProps({ text }, variables);
expect(result.current.truncatedText).toBe(
'Logs in test, app +2, test, app +2, test, app +2 - dyn-1, dyn-2',
);
expect(result.current.fullText).toBe(
'Logs in test, app, frontend, env, test, app, frontend, env, test, app, frontend, env - dyn-1, dyn-2',
);
});
it('should handle custom matcher', () => {
const text = 'Logs count in #service.name in #severity';
const variables = {
'service.name': SERVICE_VAR,
severity: SEVERITY_VAR,
};
const { result } = renderHookWithProps({ text, matcher: '#' }, variables);
expect(result.current.truncatedText).toBe(
'Logs count in test, app +2 in DEBUG, INFO',
);
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
});
it('should handle non-string variable values', () => {
const text = 'Count: $count, Active: $active';
const variables = {
count: 42,
active: true,
};
const { result } = renderHookWithProps({ text }, variables);
expect(result.current.fullText).toBe('Count: 42, Active: true');
expect(result.current.truncatedText).toBe('Count: 42, Active: true');
});
it('should keep original text for undefined variables', () => {
const text = 'Logs count in $service.name in $unknown';
const variables = {
'service.name': SERVICE_VAR,
};
const { result } = renderHookWithProps({ text }, variables);
expect(result.current.truncatedText).toBe(
'Logs count in test, app +2 in $unknown',
);
expect(result.current.fullText).toBe(
'Logs count in test, app, frontend, env in $unknown',
);
});
it('should handle non-string text input (ReactNode)', () => {
const reactNodeText = <div>Test ReactNode</div>;
const variables = {
'service.name': SERVICE_VAR,
};
const { result } = renderHookWithProps(
{
text: reactNodeText,
},
variables,
);
// Should return the ReactNode unchanged
expect(result.current.fullText).toBe(reactNodeText);
expect(result.current.truncatedText).toBe(reactNodeText);
});
it('should handle number input', () => {
const text = 123;
const variables = {
'service.name': SERVICE_VAR,
};
const { result } = renderHookWithProps(
{
text,
},
variables,
);
// Should return the number unchanged
expect(result.current.fullText).toBe(text);
expect(result.current.truncatedText).toBe(text);
});
it('should handle boolean input', () => {
const text = true;
const variables = {
'service.name': SERVICE_VAR,
};
const { result } = renderHookWithProps(
{
text,
},
variables,
it('truncates to maxLength with an ellipsis and keeps the full text', () => {
const { result } = renderHook(() =>
useGetResolvedText({ text: 'Logs count in production', maxLength: 20 }),
);
// Should return the boolean unchanged
expect(result.current.fullText).toBe(text);
expect(result.current.truncatedText).toBe(text);
expect(result.current.truncatedText).toBe('Logs count in pro...');
expect(result.current.truncatedText).toHaveLength(20);
expect(result.current.fullText).toBe('Logs count in production');
});
it('should handle complex variable names with improved patterns', () => {
const text = 'API: $api.v1.endpoint Config: $config.database.host';
const variables = {
'api.v1.endpoint': '/users',
'config.database.host': 'localhost:5432',
};
const { result } = renderHookWithProps({ text }, variables);
expect(result.current.fullText).toBe('API: /users Config: localhost:5432');
expect(result.current.truncatedText).toBe(
'API: /users Config: localhost:5432',
it('passes non-string content through untouched', () => {
const node = <span>title</span>;
const { result } = renderHook(() =>
useGetResolvedText({ text: node, maxLength: 2 }),
);
});
it('should stop at punctuation boundaries correctly', () => {
const text = 'Status: $service.name, Error: $error.type;';
const variables = {
'service.name': 'web-api',
'error.type': 'timeout',
};
const { result } = renderHookWithProps({ text }, variables);
expect(result.current.fullText).toBe('Status: web-api, Error: timeout;');
expect(result.current.truncatedText).toBe('Status: web-api, Error: timeout;');
expect(result.current.fullText).toBe(node);
expect(result.current.truncatedText).toBe(node);
});
});

View File

@@ -1,351 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { dashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import { IDashboardVariablesStoreState } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import {
VariableFetchState,
variableFetchStore,
} from 'providers/Dashboard/store/variableFetchStore';
import { IDashboardVariable } from 'types/api/dashboard/variables';
import { useIsPanelWaitingOnVariable } from '../useVariableFetchState';
function makeVariable(
overrides: Partial<IDashboardVariable> & { id: string },
): IDashboardVariable {
return {
name: overrides.id,
description: '',
type: 'QUERY',
sort: 'DISABLED',
multiSelect: false,
showALLOption: false,
...overrides,
};
}
function resetStores(): void {
variableFetchStore.set(() => ({
states: {},
lastUpdated: {},
cycleIds: {},
}));
dashboardVariablesStore.set(() => ({
dashboardId: '',
variables: {},
sortedVariablesArray: [],
dependencyData: null,
variableTypes: {},
dynamicVariableOrder: [],
}));
}
function setFetchStates(states: Record<string, VariableFetchState>): void {
variableFetchStore.set(() => ({
states,
lastUpdated: {},
cycleIds: {},
}));
}
function setDashboardVariables(
overrides: Partial<IDashboardVariablesStoreState>,
): void {
dashboardVariablesStore.set(() => ({
dashboardId: '',
variables: {},
sortedVariablesArray: [],
dependencyData: null,
variableTypes: {},
dynamicVariableOrder: [],
...overrides,
}));
}
describe('useIsPanelWaitingOnVariable', () => {
beforeEach(() => {
resetStores();
});
it('should return false when variableNames is empty', () => {
const { result } = renderHook(() => useIsPanelWaitingOnVariable([]));
expect(result.current).toBe(false);
});
it('should return false when all referenced variables are idle', () => {
setFetchStates({ a: 'idle', b: 'idle' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: 'val1' }),
b: makeVariable({ id: 'b', selectedValue: 'val2' }),
},
variableTypes: { a: 'QUERY', b: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a', 'b']));
expect(result.current).toBe(false);
});
it('should return true when a variable is loading with empty selectedValue', () => {
setFetchStates({ a: 'loading' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: undefined }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(true);
});
it('should return true when a variable is waiting with empty selectedValue', () => {
setFetchStates({ a: 'waiting' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: '' }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(true);
});
it('should return true when a variable is revalidating with empty selectedValue', () => {
setFetchStates({ a: 'revalidating' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: undefined }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(true);
});
it('should return false when a variable is loading but has a selectedValue', () => {
setFetchStates({ a: 'loading' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: 'some-value' }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(false);
});
it('should return false for DYNAMIC variable with allSelected=true that is loading but has a selectedValue', () => {
setFetchStates({ dyn: 'loading' });
setDashboardVariables({
variables: {
dyn: makeVariable({
id: 'dyn',
type: 'DYNAMIC',
selectedValue: 'some-val',
allSelected: true,
}),
},
variableTypes: { dyn: 'DYNAMIC' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
expect(result.current).toBe(false);
});
it('should return false for DYNAMIC variable with allSelected=true that is waiting but has a selectedValue', () => {
setFetchStates({ dyn: 'waiting' });
setDashboardVariables({
variables: {
dyn: makeVariable({
id: 'dyn',
type: 'DYNAMIC',
selectedValue: 'val',
allSelected: true,
}),
},
variableTypes: { dyn: 'DYNAMIC' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
expect(result.current).toBe(false);
});
it('should return false for DYNAMIC variable with allSelected=true that is idle', () => {
setFetchStates({ dyn: 'idle' });
setDashboardVariables({
variables: {
dyn: makeVariable({
id: 'dyn',
type: 'DYNAMIC',
selectedValue: 'val',
allSelected: true,
}),
},
variableTypes: { dyn: 'DYNAMIC' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
expect(result.current).toBe(false);
});
it('should return false for non-DYNAMIC variable with allSelected=false and non-empty value that is loading', () => {
setFetchStates({ a: 'loading' });
setDashboardVariables({
variables: {
a: makeVariable({
id: 'a',
selectedValue: 'val',
allSelected: false,
}),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(false);
});
it('should return true if any one of multiple variables is blocking', () => {
setFetchStates({ a: 'idle', b: 'loading' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: 'val' }),
b: makeVariable({ id: 'b', selectedValue: undefined }),
},
variableTypes: { a: 'QUERY', b: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a', 'b']));
expect(result.current).toBe(true);
});
it('should return false when variable has no entry in fetch store (treated as idle)', () => {
setFetchStates({}); // no state entry for 'a'
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: 'val' }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(false);
});
it('should return false when variable is in error state with empty selectedValue', () => {
setFetchStates({ a: 'error' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: undefined }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(false);
});
it('should react to store updates', () => {
setFetchStates({ a: 'loading' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: undefined }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(true);
// Simulate variable fetch completing
act(() => {
variableFetchStore.update((d) => {
d.states.a = 'idle';
});
});
expect(result.current).toBe(false);
});
it('should handle DYNAMIC variable with allSelected=false and empty selectedValue as blocking', () => {
setFetchStates({ dyn: 'loading' });
setDashboardVariables({
variables: {
dyn: makeVariable({
id: 'dyn',
type: 'DYNAMIC',
selectedValue: undefined,
allSelected: false,
}),
},
variableTypes: { dyn: 'DYNAMIC' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
expect(result.current).toBe(true);
});
it('should handle variable with array selectedValue as non-blocking when loading', () => {
setFetchStates({ a: 'loading' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: ['val1', 'val2'] }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(false);
});
it('should handle variable with empty array selectedValue as blocking when loading', () => {
setFetchStates({ a: 'loading' });
setDashboardVariables({
variables: {
a: makeVariable({ id: 'a', selectedValue: [] }),
},
variableTypes: { a: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
expect(result.current).toBe(true);
});
it('should find variable by name when store key differs from variable name', () => {
setFetchStates({ myVar: 'loading' });
setDashboardVariables({
variables: {
'uuid-abc-123': makeVariable({
id: 'uuid-abc-123',
name: 'myVar',
selectedValue: undefined,
}),
},
variableTypes: { myVar: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['myVar']));
expect(result.current).toBe(true);
});
it('should respect selectedValue when store key differs from variable name', () => {
// When the variable has a value, it should not block even if loading
setFetchStates({ myVar: 'loading' });
setDashboardVariables({
variables: {
'uuid-abc-123': makeVariable({
id: 'uuid-abc-123',
name: 'myVar',
selectedValue: 'production',
}),
},
variableTypes: { myVar: 'QUERY' },
});
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['myVar']));
expect(result.current).toBe(false);
});
});

View File

@@ -1,7 +1,6 @@
import { useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -42,38 +41,10 @@ function useContextVariables({
// ! To be noted: This customVariables is not Dashboard Custom Variables
customVariables,
}: UseContextVariablesProps): UseContextVariablesResult {
const { dashboardVariables } = useDashboardVariables();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
// Extract dashboard variables
const processedDashboardVariables = useMemo(() => {
return Object.entries(dashboardVariables)
.filter(([, value]) => value.name)
.map(([, value]) => {
let processedValue: string | number | boolean;
let isArray = false;
if (Array.isArray(value.selectedValue)) {
processedValue = value.selectedValue.join(', ');
isArray = true;
} else if (value.selectedValue != null) {
processedValue = value.selectedValue;
} else {
processedValue = '';
}
return {
name: value.name || '',
value: processedValue,
source: 'dashboard' as const,
isArray,
originalValue: value.selectedValue,
};
});
}, [dashboardVariables]);
// Extract global variables
const globalVariables = useMemo(
() => [
@@ -109,12 +80,8 @@ function useContextVariables({
// Combine all variables
const allVariables = useMemo(
() => [
...processedDashboardVariables,
...globalVariables,
...customVariablesList,
],
[processedDashboardVariables, globalVariables, customVariablesList],
() => [...globalVariables, ...customVariablesList],
[globalVariables, customVariablesList],
);
// Create processed variables with truncation logic

View File

@@ -1,40 +0,0 @@
import { useCallback, useRef, useSyncExternalStore } from 'react';
import { dashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
IDashboardVariablesStoreState,
IUseDashboardVariablesReturn,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
/**
* Generic selector hook for dashboard variables store
* Allows granular subscriptions to any part of the store state
*
* @example
* ! Select top-level field
* const variables = useDashboardVariablesSelector(s => s.variables);
*
* ! Select specific variable
* const fooVar = useDashboardVariablesSelector(s => s.variables['foo']);
*
* ! Select derived value
* const hasVariables = useDashboardVariablesSelector(s => Object.keys(s.variables).length > 0);
*/
export const useDashboardVariablesSelector = <T>(
selector: (state: IDashboardVariablesStoreState) => T,
): T => {
const selectorRef = useRef(selector);
selectorRef.current = selector;
const getSnapshot = useCallback(
() => selectorRef.current(dashboardVariablesStore.getSnapshot()),
[],
);
return useSyncExternalStore(dashboardVariablesStore.subscribe, getSnapshot);
};
export const useDashboardVariables = (): IUseDashboardVariablesReturn => {
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
return { dashboardVariables };
};

View File

@@ -1,30 +0,0 @@
import { useMemo } from 'react';
import {
IDashboardVariable,
TVariableQueryType,
} from 'types/api/dashboard/variables';
import { useDashboardVariables } from './useDashboardVariables';
export function useDashboardVariablesByType(
variableType: TVariableQueryType,
returnType: 'values',
): IDashboardVariable[];
export function useDashboardVariablesByType(
variableType: TVariableQueryType,
returnType?: 'entries',
): [string, IDashboardVariable][];
export function useDashboardVariablesByType(
variableType: TVariableQueryType,
returnType?: 'values' | 'entries',
): IDashboardVariable[] | [string, IDashboardVariable][] {
const { dashboardVariables } = useDashboardVariables();
return useMemo(() => {
const entries = Object.entries(dashboardVariables || {}).filter(
(entry): entry is [string, IDashboardVariable] =>
Boolean(entry[1].name) && entry[1].type === variableType,
);
return returnType === 'values' ? entries.map(([, value]) => value) : entries;
}, [dashboardVariables, variableType, returnType]);
}

View File

@@ -0,0 +1,17 @@
import { useSyncExternalStore } from 'react';
import {
DynamicVariableSuggestion,
dynamicVariableSuggestionsStore,
} from 'providers/Dashboard/store/dynamicVariableSuggestions';
/**
* Dynamic variables published by the dashboard currently open, so the query
* builder can offer `$variable` as a value for the key each one backs. Empty on
* surfaces with no dashboard behind them (APM, Celery, messaging queues).
*/
export function useDynamicVariableSuggestions(): DynamicVariableSuggestion[] {
return useSyncExternalStore(
dynamicVariableSuggestionsStore.subscribe,
dynamicVariableSuggestionsStore.getSnapshot,
);
}

View File

@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import type { PANEL_TYPES } from 'constants/queryBuilder';
import { buildExportPanelLink } from 'pages/DashboardPageV2/DashboardContainer/PanelEditor/newPanelRoute';
import { buildExportPanelLink } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
interface ExportToDashboardLinkParams {

View File

@@ -1,18 +1,8 @@
// this hook is used to get the resolved text of a variable, lets say we have a text - "Logs count in $service.name in $severity and $service.name and $severity $service.name"
// and the values of service.name and severity are "service1" and "error" respectively, then the resolved text should be "Logs count in service1 in error and service1 and error service1"
// is case of the multiple variables value, make them comma separated
// also have a prop saying max length post that you should truncate the text with "..."
// return value should be a full text string, and a truncated text string (if max length is provided)
import { ReactNode, useCallback, useMemo } from 'react';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { ReactNode, useMemo } from 'react';
interface UseGetResolvedTextProps {
text: string | ReactNode;
variables?: Record<string, string | number | boolean>;
maxLength?: number;
matcher?: string;
maxValues?: number; // Maximum number of values to show before adding +n more
}
interface ResolvedTextResult {
@@ -20,173 +10,23 @@ interface ResolvedTextResult {
truncatedText: string | ReactNode;
}
/**
* Returns a panel title alongside a copy truncated to `maxLength`, so a card can
* show the short form and keep the full string for its tooltip. Non-string content
* passes through untouched.
*/
function useGetResolvedText({
text,
maxLength,
matcher = '$',
maxValues = 2, // Default to showing 2 values before +n more
}: UseGetResolvedTextProps): ResolvedTextResult {
const { dashboardVariables } = useDashboardVariables();
const isString = typeof text === 'string';
const processedDashboardVariables = useMemo(() => {
return Object.entries(dashboardVariables).reduce<
Record<string, string | number | boolean>
>((acc, [, value]) => {
if (!value.name) {
return acc;
}
// Handle array values
if (Array.isArray(value.selectedValue)) {
acc[value.name] = value.selectedValue.join(', ');
} else if (value.selectedValue != null) {
acc[value.name] = value.selectedValue;
}
return acc;
}, {});
}, [dashboardVariables]);
// Process array values to add +n more notation for truncated text
const processedVariables = useMemo(() => {
const result: Record<string, string> = {};
Object.entries(processedDashboardVariables).forEach(([key, value]) => {
// If the value contains array data (comma-separated string), format it with +n more
if (
typeof value === 'string' &&
!value.includes('-|-') &&
value.includes(',')
) {
const values = value.split(',').map((v) => v.trim());
if (values.length > maxValues) {
const visibleValues = values.slice(0, maxValues);
const remainingCount = values.length - maxValues;
result[key] = `${visibleValues.join(
', ',
)} +${remainingCount}-|-${values.join(', ')}`;
} else {
result[key] = `${values.join(', ')}-|-${values.join(', ')}`;
}
} else {
// For values already formatted with -|- or non-array values
result[key] = String(value);
}
});
return result;
}, [processedDashboardVariables, maxValues]);
const combinedPattern = useMemo(() => {
const escapedMatcher = matcher.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const variablePatterns = [
`\\{\\{\\s*?\\.([^\\s}]+?)\\s*?\\}\\}`, // {{.var}}
`\\{\\{\\s*([^\\s}]+?)\\s*\\}\\}`, // {{var}}
`${escapedMatcher}([^\\s.,;)\\]}>]+(?:\\.[^\\s.,;)\\]}>]+)*)`, // $var.name.path - allows dots but stops at punctuation
`\\[\\[\\s*([^\\s\\]]+?)\\s*\\]\\]`, // [[var]]
];
return new RegExp(variablePatterns.join('|'), 'g');
}, [matcher]);
const extractVarName = useCallback(
(match: string): string => {
// Extract variable name from different formats
const varNamePattern = '[a-zA-Z_\\-][a-zA-Z0-9_.\\-]*';
if (match.startsWith('{{')) {
const dotMatch = match.match(
new RegExp(`\\{\\{\\s*\\.(${varNamePattern})\\s*\\}\\}`),
);
if (dotMatch) {
return dotMatch[1].trim();
}
const normalMatch = match.match(
new RegExp(`\\{\\{\\s*(${varNamePattern})\\s*\\}\\}`),
);
if (normalMatch) {
return normalMatch[1].trim();
}
} else if (match.startsWith('[[')) {
const bracketMatch = match.match(
new RegExp(`\\[\\[\\s*(${varNamePattern})\\s*\\]\\]`),
);
if (bracketMatch) {
return bracketMatch[1].trim();
}
} else if (match.startsWith(matcher)) {
// For $ variables, we always want to strip the prefix
// unless the full match exists in processedVariables
const withoutPrefix = match.substring(matcher.length).trim();
const fullMatch = match.trim();
// If the full match (with prefix) exists, use it
if (processedVariables[fullMatch] !== undefined) {
return fullMatch;
}
// Otherwise return without prefix
return withoutPrefix;
}
return match;
},
[matcher, processedVariables],
);
const fullText = useMemo(() => {
if (!isString) {
return text;
}
return (text as string)?.replace(combinedPattern, (match) => {
const varName = extractVarName(match);
const value = processedVariables[varName];
if (value != null) {
const parts = value.split('-|-');
return parts.length > 1 ? parts[1] : value;
}
return match;
});
}, [text, processedVariables, combinedPattern, extractVarName, isString]);
const truncatedText = useMemo(() => {
if (!isString) {
if (typeof text !== 'string' || !maxLength || text.length <= maxLength) {
return text;
}
return `${text.substring(0, maxLength - 3)}...`;
}, [text, maxLength]);
const result = (text as string)?.replace(combinedPattern, (match) => {
const varName = extractVarName(match);
const value = processedVariables[varName];
if (value != null) {
const parts = value.split('-|-');
return parts[0] || value;
}
return match;
});
if (maxLength && result.length > maxLength) {
// For the specific test case
if (maxLength === 20 && result.startsWith('Logs count in')) {
return 'Logs count in test, a...';
}
// General case
return `${result.substring(0, maxLength - 3)}...`;
}
return result;
}, [
text,
processedVariables,
combinedPattern,
maxLength,
extractVarName,
isString,
]);
return {
fullText,
truncatedText,
};
return { fullText: text, truncatedText };
}
export default useGetResolvedText;

View File

@@ -1,151 +0,0 @@
import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';
import isEmpty from 'lodash-es/isEmpty';
import {
IVariableFetchStoreState,
VariableFetchState,
variableFetchStore,
} from 'providers/Dashboard/store/variableFetchStore';
import { useDashboardVariablesSelector } from './useDashboardVariables';
/**
* Generic selector hook for the variable fetch store.
* Same pattern as useDashboardVariablesSelector.
*/
const useVariableFetchSelector = <T>(
selector: (state: IVariableFetchStoreState) => T,
): T => {
const selectorRef = useRef(selector);
selectorRef.current = selector;
const getSnapshot = useCallback(
() => selectorRef.current(variableFetchStore.getSnapshot()),
[],
);
return useSyncExternalStore(variableFetchStore.subscribe, getSnapshot);
};
interface UseVariableFetchStateReturn {
/** The current fetch state for this variable */
variableFetchState: VariableFetchState;
/** Current fetch cycle — include in react-query keys to auto-cancel stale requests */
variableFetchCycleId: number;
/** True if this variable is idle (not waiting and not fetching) */
isVariableSettled: boolean;
/** True if this variable is actively fetching (loading or revalidating) */
isVariableFetching: boolean;
/** True if this variable has completed at least one fetch cycle */
hasVariableFetchedOnce: boolean;
/** True if any parent variable hasn't settled yet */
isVariableWaitingForDependencies: boolean;
/** Message describing what this variable is waiting on, or null if not waiting */
variableDependencyWaitMessage?: string;
}
/**
* Per-variable hook that exposes the fetch state of a single variable.
* Reusable by both variable input components and panel components.
*
* Subscribes to both variableFetchStore (for states) and
* dashboardVariablesStore (for parent graph) to compute derived values.
*/
export function useVariableFetchState(
variableName: string,
): UseVariableFetchStateReturn {
// This variable's fetch state (loading, waiting, idle, etc.)
const variableFetchState = useVariableFetchSelector(
(s) => s.states[variableName] || 'idle',
) as VariableFetchState;
// All variable states — needed to check if parent variables are still in-flight
const allStates = useVariableFetchSelector((s) => s.states);
// Parent dependency graph — maps each variable to its direct parents
// e.g. { "childVariable": ["parentVariable"] } means "childVariable" depends on "parentVariable"
const parentGraph = useDashboardVariablesSelector(
(s) => s.dependencyData?.parentDependencyGraph,
);
// Timestamp of last successful fetch — 0 means never fetched
const lastUpdated = useVariableFetchSelector(
(s) => s.lastUpdated[variableName] || 0,
);
// Per-variable cycle counter — used as part of react-query keys
// so changing it auto-cancels stale requests for this variable only
const variableFetchCycleId = useVariableFetchSelector(
(s) => s.cycleIds[variableName] || 0,
);
const isVariableSettled = variableFetchState === 'idle';
const isVariableFetching =
variableFetchState === 'loading' || variableFetchState === 'revalidating';
// True after at least one successful fetch — used to show stale data while revalidating
const hasVariableFetchedOnce = lastUpdated > 0;
// Variable type — needed to differentiate waiting messages
const variableType = useDashboardVariablesSelector(
(s) => s.variableTypes[variableName],
);
// Parent variable names that haven't settled yet
const unsettledParents = useMemo(() => {
const parents = parentGraph?.[variableName] || [];
return parents.filter((p) => (allStates[p] || 'idle') !== 'idle');
}, [parentGraph, variableName, allStates]);
const isVariableWaitingForDependencies = unsettledParents.length > 0;
const variableDependencyWaitMessage = useMemo(() => {
if (variableFetchState !== 'waiting') {
return;
}
if (variableType === 'DYNAMIC') {
return 'Waiting for all query variable options to load.';
}
if (unsettledParents.length === 0) {
return;
}
const quoted = unsettledParents.map((p) => `"${p}"`);
const names =
quoted.length > 1
? `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`
: quoted[0];
return `Waiting for options of ${names} to load.`;
}, [variableFetchState, variableType, unsettledParents]);
return {
variableFetchState,
isVariableSettled,
isVariableWaitingForDependencies,
variableDependencyWaitMessage,
isVariableFetching,
hasVariableFetchedOnce,
variableFetchCycleId,
};
}
export function useIsPanelWaitingOnVariable(variableNames: string[]): boolean {
const states = useVariableFetchSelector((s) => s.states);
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
return variableNames.some((name) => {
const variableFetchState = states[name];
const variableData = Object.values(dashboardVariables).find(
(v) => v.name === name,
);
const { selectedValue } = variableData || {};
const isVariableInFetchingOrWaitingState =
variableFetchState === 'loading' ||
variableFetchState === 'revalidating' ||
variableFetchState === 'waiting';
return isEmpty(selectedValue) ? isVariableInFetchingOrWaitingState : false;
});
}

View File

@@ -32,12 +32,8 @@ jest.mock(
}),
);
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
useDashboardVariables: (): unknown => ({ dashboardVariables: {} }),
}));
jest.mock('hooks/dashboard/useDashboardVariablesByType', () => ({
useDashboardVariablesByType: (): unknown => ({}),
jest.mock('hooks/dashboard/useDynamicVariableSuggestions', () => ({
useDynamicVariableSuggestions: (): unknown[] => [],
}));
jest.mock('hooks/useNotifications', () => ({
@@ -46,10 +42,6 @@ jest.mock('hooks/useNotifications', () => ({
}),
}));
jest.mock('lib/dashboardVariables/getDashboardVariables', () => ({
getDashboardVariables: (): unknown => ({}),
}));
jest.mock('utils/getGraphType', () => ({
getGraphType: jest.fn().mockReturnValue('time_series'),
}));

View File

@@ -11,10 +11,8 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { MenuItemKeys } from 'container/WidgetCard/Header/contants';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import { useNotifications } from 'hooks/useNotifications';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { isEmpty } from 'lodash-es';
import { AppState } from 'store/reducers';
@@ -26,7 +24,7 @@ import { getGraphType } from 'utils/getGraphType';
/**
* @deprecated V1-only. V2 dashboards seed alerts from a panel via
* `useCreateAlertFromPanel` / `buildCreateAlertUrl`
* (pages/DashboardPageV2/.../Panel). Do not use in new code.
* (pages/DashboardPage/.../Panel). Do not use in new code.
*/
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const queryRangeMutation = useMutation(getSubstituteVars);
@@ -38,11 +36,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const { notifications } = useNotifications();
const { dashboardVariables } = useDashboardVariables();
const dashboardDynamicVariables = useDashboardVariablesByType(
'DYNAMIC',
'values',
);
const dashboardDynamicVariables = useDynamicVariableSuggestions();
return useCallback(() => {
if (!widget) {
@@ -68,7 +62,6 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
globalSelectedInterval,
graphType: getGraphType(widget.panelTypes),
selectedTime: widget.timePreferance,
variables: getDashboardVariables(dashboardVariables),
originalGraphType: widget.panelTypes,
dynamicVariables: dashboardDynamicVariables,
});
@@ -107,7 +100,6 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
globalSelectedInterval,
notifications,
queryRangeMutation,
dashboardVariables,
dashboardDynamicVariables,
widget,
]);

View File

@@ -5,7 +5,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { MAX_QUERY_RETRIES } from 'constants/reactQuery';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { updateBarStepInterval } from 'container/WidgetCard/utils';
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import {
GetMetricQueryRange,
GetQueryResultsProps,
@@ -33,10 +33,7 @@ export const useGetQueryRange: UseGetQueryRange = (
options,
headers,
) => {
const dashboardDynamicVariables = useDashboardVariablesByType(
'DYNAMIC',
'values',
);
const dashboardDynamicVariables = useDynamicVariableSuggestions();
const newRequestData: GetQueryResultsProps = useMemo(() => {
const firstQueryData = requestData.query.builder?.queryData[0];

View File

@@ -17,8 +17,8 @@ import {
import { Pagination } from 'hooks/queryPagination';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import { isEmpty } from 'lodash-es';
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
import { SuccessResponseV2, Warning } from 'types/api';
import { IDashboardVariable } from 'types/api/dashboard/variables';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import {
@@ -179,7 +179,7 @@ export const getLegend = (
export async function GetMetricQueryRange(
props: GetQueryResultsProps,
version: string,
dynamicVariables?: IDashboardVariable[],
dynamicVariables?: DynamicVariableSuggestion[],
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<MetricQueryRangeSuccessResponse> {
@@ -364,5 +364,5 @@ export interface GetQueryResultsProps {
end?: number;
step?: number;
originalGraphType?: PANEL_TYPES;
dynamicVariables?: IDashboardVariable[];
dynamicVariables?: DynamicVariableSuggestion[];
}

View File

@@ -1,239 +0,0 @@
import { textContainsVariableReference } from 'lib/dashboardVariables/variableReference';
import { IDependencyData } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariable } from 'types/api/dashboard/variables';
/**
* Inter-variable dependency graph over the shared dashboard-variables store. A
* QUERY variable "depends on" another when its query text references that
* variable, so changing a value must refetch its dependents.
*
* Keyed on `IDashboardVariable`. The V2 editor has a parallel implementation
* over its own flat form model in
* `pages/DashboardPageV2/DashboardContainer/VariablesBar/utils/variableDependencies.ts`.
*/
export type VariableGraph = Record<string, string[]>;
/** Names of QUERY variables whose query references `variableName`. */
const getDependentVariablesBasedOnVariableName = (
variableName: string,
variables: IDashboardVariable[],
): string[] => {
if (!variables || !Array.isArray(variables)) {
return [];
}
return variables
.map((variable) => {
if (variable.type === 'QUERY') {
const queryValue = variable.queryValue || '';
if (textContainsVariableReference(queryValue, variableName)) {
return variable.name;
}
}
return null;
})
.filter((val): val is string => val !== null);
};
/** variable name → its direct dependents (children). */
export const buildDependencies = (
variables: IDashboardVariable[],
): VariableGraph => {
const graph: VariableGraph = {};
// Initialize empty arrays for all variables first
variables.forEach((variable) => {
if (variable.name) {
graph[variable.name] = [];
}
});
// For each QUERY variable, add it as a dependent to its referenced variables
variables.forEach((variable) => {
if (variable.name) {
const dependentVariables = getDependentVariablesBasedOnVariableName(
variable.name,
variables,
);
// For each referenced variable, add the current query as a dependent
graph[variable.name] = dependentVariables;
}
});
return graph;
};
/** Invert a child graph into a parent graph. */
export const buildParentDependencyGraph = (
graph: VariableGraph,
): VariableGraph => {
const parentGraph: VariableGraph = {};
// Initialize empty arrays for all nodes
Object.keys(graph).forEach((node) => {
parentGraph[node] = [];
});
// For each node and its children in the original graph
Object.entries(graph).forEach(([node, children]) => {
// For each child, add the current node as its parent
children.forEach((child) => {
if (!parentGraph[child]) {
parentGraph[child] = [];
}
parentGraph[child].push(node);
});
});
return parentGraph;
};
const collectCyclePath = (
graph: VariableGraph,
start: string,
end: string,
): string[] => {
const path: string[] = [];
let current = start;
const findParent = (node: string): string | undefined =>
Object.keys(graph).find((key) => graph[key]?.includes(node));
while (current !== end) {
const parent = findParent(current);
if (!parent) {
break;
}
path.push(parent);
current = parent;
}
return [start, ...path];
};
const detectCycle = (
graph: VariableGraph,
node: string,
visited: Set<string>,
recStack: Set<string>,
): string[] | null => {
if (!visited.has(node)) {
visited.add(node);
recStack.add(node);
const neighbors = graph[node] || [];
let cycleNodes: string[] | null = null;
neighbors.some((neighbor) => {
if (!visited.has(neighbor)) {
const foundCycle = detectCycle(graph, neighbor, visited, recStack);
if (foundCycle) {
cycleNodes = foundCycle;
return true;
}
} else if (recStack.has(neighbor)) {
// Found a cycle, collect the cycle nodes
cycleNodes = collectCyclePath(graph, node, neighbor);
return true;
}
return false;
});
if (cycleNodes) {
return cycleNodes;
}
}
recStack.delete(node);
return null;
};
/** Topological order, parent graph, transitive descendants and cycle info. */
export const buildDependencyGraph = (
dependencies: VariableGraph,
// eslint-disable-next-line sonarjs/cognitive-complexity
): IDependencyData => {
const inDegree: Record<string, number> = {};
const adjList: VariableGraph = {};
// Initialize in-degree and adjacency list
Object.keys(dependencies).forEach((node) => {
if (!inDegree[node]) {
inDegree[node] = 0;
}
if (!adjList[node]) {
adjList[node] = [];
}
dependencies[node]?.forEach((child) => {
if (!inDegree[child]) {
inDegree[child] = 0;
}
inDegree[child]++;
adjList[node].push(child);
});
});
// Detect cycles
const visited = new Set<string>();
const recStack = new Set<string>();
let cycleNodes: string[] | undefined;
Object.keys(dependencies).some((node) => {
if (!visited.has(node)) {
const foundCycle = detectCycle(dependencies, node, visited, recStack);
if (foundCycle) {
cycleNodes = foundCycle;
return true;
}
}
return false;
});
// Topological sort using Kahn's Algorithm
const queue: string[] = Object.keys(inDegree).filter(
(node) => inDegree[node] === 0,
);
const topologicalOrder: string[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined) {
break;
}
topologicalOrder.push(current);
adjList[current]?.forEach((neighbor) => {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) {
queue.push(neighbor);
}
});
}
const hasCycle = topologicalOrder.length !== Object.keys(dependencies)?.length;
// Pre-compute transitive descendants by walking topological order in reverse.
// Each node's transitive descendants = direct children + their transitive descendants.
const transitiveDescendants: VariableGraph = {};
for (let i = topologicalOrder.length - 1; i >= 0; i--) {
const node = topologicalOrder[i];
const desc = new Set<string>();
for (const child of adjList[node] || []) {
desc.add(child);
for (const d of transitiveDescendants[child] || []) {
desc.add(d);
}
}
transitiveDescendants[node] = Array.from(desc);
}
return {
order: topologicalOrder,
graph: adjList,
parentDependencyGraph: buildParentDependencyGraph(adjList),
transitiveDescendants,
hasCycle,
cycleNodes,
};
};

View File

@@ -1,41 +0,0 @@
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import store from 'store';
export const getDashboardVariables = (
variables?: IDashboardVariables,
): Record<string, unknown> => {
if (!variables) {
return {};
}
try {
const { globalTime } = store.getState();
const { start, end } = getStartEndRangeTime({
type: 'GLOBAL_TIME',
interval: globalTime.selectedTime,
});
const variablesTuple: Record<string, unknown> = {
SIGNOZ_START_TIME: parseInt(start, 10) * 1e3,
SIGNOZ_END_TIME: parseInt(end, 10) * 1e3,
};
Object.entries(variables).forEach(([, value]) => {
if (value?.name) {
variablesTuple[value.name] =
value?.type === 'DYNAMIC' &&
value?.allSelected &&
value?.showALLOption &&
value?.multiSelect
? '__all__'
: value?.selectedValue;
}
});
return variablesTuple;
} catch (e) {
console.error(e);
}
return {};
};

View File

@@ -7,7 +7,7 @@ import { ThresholdProps } from 'types/api/widgets/threshold';
import {
applyEnhancedLegendStyling,
calculateEnhancedLegendConfig,
} from 'container/WidgetCard/PanelWrapper/enhancedLegend';
} from 'container/WidgetCard/Panels/enhancedLegend';
import { Dimensions } from 'hooks/useDimensions';
import { getLegend } from 'lib/dashboard/getQueryResults';
import { convertValue } from 'lib/getConvertedValue';

View File

@@ -117,7 +117,6 @@ export default function ChartManager({
onToggleSeriesOnOff: handleToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit,
isGraphDisabled: false,
decimalPrecision,
}),
[

View File

@@ -16,7 +16,6 @@ export interface GetChartManagerColumnsParams {
onToggleSeriesVisibility: (index: number) => void;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
isGraphDisabled?: boolean;
}
export function getChartManagerColumns({
@@ -26,7 +25,6 @@ export function getChartManagerColumns({
onToggleSeriesVisibility,
yAxisUnit,
decimalPrecision = PrecisionOptionsEnum.TWO,
isGraphDisabled,
}: GetChartManagerColumnsParams): ColumnType<ExtendedChartDataset>[] {
return [
{
@@ -39,7 +37,6 @@ export function getChartManagerColumns({
data={tableDataSet}
graphVisibilityState={graphVisibilityState}
index={record.index}
disabled={isGraphDisabled}
checkBoxOnChangeHandler={(_e, idx): void => onToggleSeriesOnOff(idx)}
/>
),
@@ -53,7 +50,6 @@ export function getChartManagerColumns({
<SeriesLabel
label={label ?? ''}
labelIndex={record.index}
disabled={isGraphDisabled}
onClick={onToggleSeriesVisibility}
/>
),

View File

@@ -28,7 +28,7 @@ import { cloneDashboardV2 } from 'api/generated/services/dashboard';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';

View File

@@ -11,7 +11,7 @@ import { useDeleteConfirm } from 'components/DeleteConfirmModal/useDeleteConfirm
import ROUTES from 'constants/routes';
import { useDashboardPreferencesStore } from 'hooks/dashboard/useDashboardPreference';
import history from 'lib/history';
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';

View File

@@ -15,7 +15,7 @@ import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { isEmpty } from 'lodash-es';
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import { linkifyText } from 'utils/linkifyText';
import { openInNewTab } from 'utils/navigation';

View File

@@ -8,7 +8,7 @@ import cx from 'classnames';
import { Drawer } from 'antd';
import logEvent from 'api/common/logEvent';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import { useCopyToClipboard } from 'react-use';
import { toast } from '@signozhq/ui/sonner';

Some files were not shown because too many files have changed in this diff Show More