mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 22:46:06 +08:00
feat: suggest and allow variables in panel title (#7898)
* feat: suggest and allow variables in panel title * feat: refined the logic for suggestion and addition with $ * feat: added logic for panel title resolved string and added test cases * feat: added support to full view
This commit is contained in:
parent
03600f4d6f
commit
f10f7a806f
@ -1,6 +1,6 @@
|
|||||||
import '../GridCardLayout.styles.scss';
|
import '../GridCardLayout.styles.scss';
|
||||||
|
|
||||||
import { Skeleton, Typography } from 'antd';
|
import { Skeleton, Tooltip, Typography } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
@ -9,6 +9,7 @@ import { QueryParams } from 'constants/query';
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
||||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||||
|
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
@ -293,6 +294,11 @@ function WidgetGraphComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { truncatedText, fullText } = useGetResolvedText({
|
||||||
|
text: widget.title as string,
|
||||||
|
maxLength: 100,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -326,7 +332,11 @@ function WidgetGraphComponent({
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={widget?.title || 'View'}
|
title={
|
||||||
|
<Tooltip title={fullText} placement="top">
|
||||||
|
<span>{truncatedText || fullText || 'View'}</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
footer={[]}
|
footer={[]}
|
||||||
centered
|
centered
|
||||||
open={isFullViewOpen}
|
open={isFullViewOpen}
|
||||||
|
@ -16,6 +16,7 @@ import { Dropdown, Input, MenuProps, Tooltip, Typography } from 'antd';
|
|||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
|
||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
@ -205,6 +206,11 @@ function WidgetHeader({
|
|||||||
[updatedMenuList, onMenuItemSelectHandler],
|
[updatedMenuList, onMenuItemSelectHandler],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { truncatedText, fullText } = useGetResolvedText({
|
||||||
|
text: widget.title as string,
|
||||||
|
maxLength: 100,
|
||||||
|
});
|
||||||
|
|
||||||
if (widget.id === PANEL_TYPES.EMPTY_WIDGET) {
|
if (widget.id === PANEL_TYPES.EMPTY_WIDGET) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -237,13 +243,15 @@ function WidgetHeader({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="widget-header-title-container">
|
<div className="widget-header-title-container">
|
||||||
<Typography.Text
|
<Tooltip title={fullText} placement="top">
|
||||||
ellipsis
|
<Typography.Text
|
||||||
data-testid={title}
|
ellipsis
|
||||||
className="widget-header-title"
|
data-testid={title}
|
||||||
>
|
className="widget-header-title"
|
||||||
{title}
|
>
|
||||||
</Typography.Text>
|
{truncatedText}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
{widget.description && (
|
{widget.description && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={widget.description}
|
title={widget.description}
|
||||||
|
@ -61,7 +61,6 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px; /* 128.571% */
|
line-height: 18px; /* 128.571% */
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-input {
|
.description-input {
|
||||||
|
@ -2,7 +2,16 @@
|
|||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
import './RightContainer.styles.scss';
|
import './RightContainer.styles.scss';
|
||||||
|
|
||||||
import { Input, InputNumber, Select, Space, Switch, Typography } from 'antd';
|
import type { InputRef } from 'antd';
|
||||||
|
import {
|
||||||
|
AutoComplete,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||||
import GraphTypes, {
|
import GraphTypes, {
|
||||||
@ -11,15 +20,19 @@ import GraphTypes, {
|
|||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { ConciergeBell, LineChart, Plus, Spline } from 'lucide-react';
|
import { ConciergeBell, LineChart, Plus, Spline } from 'lucide-react';
|
||||||
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { ColumnUnit, Widgets } from 'types/api/dashboard/getAll';
|
import { ColumnUnit, Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
||||||
import {
|
import {
|
||||||
@ -47,6 +60,11 @@ enum LogScale {
|
|||||||
LOGARITHMIC = 'logarithmic',
|
LOGARITHMIC = 'logarithmic',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VariableOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function RightContainer({
|
function RightContainer({
|
||||||
description,
|
description,
|
||||||
@ -81,6 +99,12 @@ function RightContainer({
|
|||||||
isLogScale,
|
isLogScale,
|
||||||
setIsLogScale,
|
setIsLogScale,
|
||||||
}: RightContainerProps): JSX.Element {
|
}: RightContainerProps): JSX.Element {
|
||||||
|
const { selectedDashboard } = useDashboard();
|
||||||
|
const [inputValue, setInputValue] = useState(title);
|
||||||
|
const [autoCompleteOpen, setAutoCompleteOpen] = useState(false);
|
||||||
|
const [cursorPos, setCursorPos] = useState(0);
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
const onChangeHandler = useCallback(
|
const onChangeHandler = useCallback(
|
||||||
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
|
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
|
||||||
setFunc(value);
|
setFunc(value);
|
||||||
@ -112,6 +136,66 @@ function RightContainer({
|
|||||||
|
|
||||||
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
|
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
|
||||||
|
|
||||||
|
// Get dashboard variables
|
||||||
|
const dashboardVariables = useMemo<VariableOption[]>(() => {
|
||||||
|
if (!selectedDashboard?.data?.variables) return [];
|
||||||
|
return Object.entries(selectedDashboard.data.variables).map(([, value]) => ({
|
||||||
|
value: value.name || '',
|
||||||
|
label: value.name || '',
|
||||||
|
}));
|
||||||
|
}, [selectedDashboard?.data?.variables]);
|
||||||
|
|
||||||
|
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
||||||
|
setCursorPos(pos);
|
||||||
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
|
setAutoCompleteOpen(lastDollar !== -1 && pos >= lastDollar + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = (value: string): void => {
|
||||||
|
setInputValue(value);
|
||||||
|
onChangeHandler(setTitle, value);
|
||||||
|
setTimeout(() => {
|
||||||
|
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
||||||
|
updateCursorAndDropdown(value, pos);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputCursor = (): void => {
|
||||||
|
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
||||||
|
updateCursorAndDropdown(inputValue, pos);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (selectedValue: string): void => {
|
||||||
|
const pos = cursorPos;
|
||||||
|
const value = inputValue;
|
||||||
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
|
const textBeforeDollar = value.substring(0, lastDollar);
|
||||||
|
const textAfterDollar = value.substring(lastDollar + 1);
|
||||||
|
const match = textAfterDollar.match(/^([a-zA-Z0-9_.]*)/);
|
||||||
|
const rest = textAfterDollar.substring(match ? match[1].length : 0);
|
||||||
|
const newValue = `${textBeforeDollar}$${selectedValue}${rest}`;
|
||||||
|
setInputValue(newValue);
|
||||||
|
onChangeHandler(setTitle, newValue);
|
||||||
|
setAutoCompleteOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
const newCursor = `${textBeforeDollar}$${selectedValue}`.length;
|
||||||
|
inputRef.current?.input?.setSelectionRange(newCursor, newCursor);
|
||||||
|
setCursorPos(newCursor);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOption = (
|
||||||
|
inputValue: string,
|
||||||
|
option?: VariableOption,
|
||||||
|
): boolean => {
|
||||||
|
const pos = cursorPos;
|
||||||
|
const value = inputValue;
|
||||||
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
|
if (lastDollar === -1) return false;
|
||||||
|
const afterDollar = value.substring(lastDollar + 1, pos).toLowerCase();
|
||||||
|
return option?.value.toLowerCase().startsWith(afterDollar) || false;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const queryContainsMetricsDataSource = currentQuery.builder.queryData.some(
|
const queryContainsMetricsDataSource = currentQuery.builder.queryData.some(
|
||||||
(query) => query.dataSource === DataSource.METRICS,
|
(query) => query.dataSource === DataSource.METRICS,
|
||||||
@ -148,12 +232,25 @@ function RightContainer({
|
|||||||
</section>
|
</section>
|
||||||
<section className="name-description">
|
<section className="name-description">
|
||||||
<Typography.Text className="typography">Name</Typography.Text>
|
<Typography.Text className="typography">Name</Typography.Text>
|
||||||
<Input
|
<AutoComplete
|
||||||
|
options={dashboardVariables}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={onInputChange}
|
||||||
|
onSelect={onSelect}
|
||||||
|
filterOption={filterOption}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
placeholder="Enter the panel name here..."
|
placeholder="Enter the panel name here..."
|
||||||
onChange={(event): void => onChangeHandler(setTitle, event.target.value)}
|
open={autoCompleteOpen}
|
||||||
value={title}
|
>
|
||||||
rootClassName="name-input"
|
<Input
|
||||||
/>
|
rootClassName="name-input"
|
||||||
|
ref={inputRef}
|
||||||
|
onSelect={handleInputCursor}
|
||||||
|
onClick={handleInputCursor}
|
||||||
|
onBlur={(): void => setAutoCompleteOpen(false)}
|
||||||
|
/>
|
||||||
|
</AutoComplete>
|
||||||
<Typography.Text className="typography">Description</Typography.Text>
|
<Typography.Text className="typography">Description</Typography.Text>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="Enter the panel description here..."
|
placeholder="Enter the panel description here..."
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
|
import useGetResolvedText from '../useGetResolvedText';
|
||||||
|
|
||||||
|
// Mock the useDashboard hook
|
||||||
|
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||||
|
useDashboard: function useDashboardMock(): any {
|
||||||
|
return {
|
||||||
|
selectedDashboard: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
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;
|
||||||
|
variables?: Record<string, string | number | boolean>;
|
||||||
|
dashboardVariables?: Record<string, any>;
|
||||||
|
maxLength?: number;
|
||||||
|
matcher?: string;
|
||||||
|
}): any => 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`,
|
||||||
|
);
|
||||||
|
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle text with maxLength truncation', () => {
|
||||||
|
const text = TEXT_TEMPLATE;
|
||||||
|
const variables = {
|
||||||
|
'service.name': SERVICE_VAR,
|
||||||
|
severity: SEVERITY_VAR,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHookWithProps({ text, variables, maxLength: 20 });
|
||||||
|
|
||||||
|
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]]';
|
||||||
|
const variables = {
|
||||||
|
'service.name': SERVICE_VAR,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHookWithProps({ text, variables });
|
||||||
|
|
||||||
|
expect(result.current.truncatedText).toBe(
|
||||||
|
'Logs in test, app +2, test, app +2, test, app +2',
|
||||||
|
);
|
||||||
|
expect(result.current.fullText).toBe(
|
||||||
|
'Logs in test, app, frontend, env, test, app, frontend, env, test, app, frontend, env',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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, variables, matcher: '#' });
|
||||||
|
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
156
frontend/src/hooks/dashboard/useGetResolvedText.tsx
Normal file
156
frontend/src/hooks/dashboard/useGetResolvedText.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// 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 { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
interface UseGetResolvedTextProps {
|
||||||
|
text: string;
|
||||||
|
variables?: Record<string, string | number | boolean>;
|
||||||
|
maxLength?: number;
|
||||||
|
matcher?: string;
|
||||||
|
maxValues?: number; // Maximum number of values to show before adding +n more
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResolvedTextResult {
|
||||||
|
fullText: string;
|
||||||
|
truncatedText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useGetResolvedText({
|
||||||
|
text,
|
||||||
|
variables,
|
||||||
|
maxLength,
|
||||||
|
matcher = '$',
|
||||||
|
maxValues = 2, // Default to showing 2 values before +n more
|
||||||
|
}: UseGetResolvedTextProps): ResolvedTextResult {
|
||||||
|
const { selectedDashboard } = useDashboard();
|
||||||
|
|
||||||
|
const processedDashboardVariables = useMemo(() => {
|
||||||
|
if (variables) return variables;
|
||||||
|
if (!selectedDashboard?.data.variables) return {};
|
||||||
|
|
||||||
|
return Object.entries(selectedDashboard.data.variables).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;
|
||||||
|
}, {});
|
||||||
|
}, [variables, selectedDashboard?.data.variables]);
|
||||||
|
|
||||||
|
// 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}([\\w.]+)`, // matcher + var.name
|
||||||
|
`\\[\\[\\s*([^\\s\\]]+)\\s*\\]\\]`, // [[var]]
|
||||||
|
];
|
||||||
|
return new RegExp(variablePatterns.join('|'), 'g');
|
||||||
|
}, [matcher]);
|
||||||
|
|
||||||
|
const extractVarName = useCallback(
|
||||||
|
(match: string): string => {
|
||||||
|
// Extract variable name from different formats
|
||||||
|
if (match.startsWith('{{')) {
|
||||||
|
const dotMatch = match.match(/\{\{\s*\.([^}]+)\}\}/);
|
||||||
|
if (dotMatch) return dotMatch[1].trim();
|
||||||
|
const normalMatch = match.match(/\{\{\s*([^}]+)\}\}/);
|
||||||
|
if (normalMatch) return normalMatch[1].trim();
|
||||||
|
} else if (match.startsWith('[[')) {
|
||||||
|
const bracketMatch = match.match(/\[\[\s*([^\]]+)\]\]/);
|
||||||
|
if (bracketMatch) return bracketMatch[1].trim();
|
||||||
|
} else if (match.startsWith(matcher)) {
|
||||||
|
return match.substring(matcher.length);
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
},
|
||||||
|
[matcher],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fullText = useMemo(
|
||||||
|
() =>
|
||||||
|
text.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],
|
||||||
|
);
|
||||||
|
|
||||||
|
const truncatedText = useMemo(() => {
|
||||||
|
const result = text.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]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullText,
|
||||||
|
truncatedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useGetResolvedText;
|
Loading…
x
Reference in New Issue
Block a user