signoz/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx
Vikrant Gupta 3bb9e05681
chore(dashboard): make dashboard schema production ready (#8092)
* chore(dashboard): intial commit

* chore(dashboard): bring all the code in module

* chore(dashboard): remove lock unlock from ee codebase

* chore(dashboard): go deps

* chore(dashboard): fix lint

* chore(dashboard): implement the store

* chore(dashboard): add migration

* chore(dashboard): fix lint

* chore(dashboard): api and frontend changes

* chore(dashboard): frontend changes for new dashboards

* chore(dashboard): fix test cases

* chore(dashboard): add lock unlock APIs

* chore(dashboard): add lock unlock APIs

* chore(dashboard): move integrations controller out from module

* chore(dashboard): move integrations controller out from module

* chore(dashboard): move integrations controller out from module

* chore(dashboard): rename migration file

* chore(dashboard): surface errors for lock/unlock dashboard

* chore(dashboard): some testing cleanups

* chore(dashboard): fix postgres migrations

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-06-02 22:41:38 +05:30

419 lines
11 KiB
TypeScript

import '../GridCardLayout.styles.scss';
import { Skeleton, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
import { ToggleGraphProps } from 'components/Graph/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import {
getCustomTimeRangeWindowSweepInMS,
getStartAndEndTimesInMilliseconds,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
Dispatch,
RefObject,
SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Props } from 'types/api/dashboard/update';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { useGraphClickToShowButton } from '../useGraphClickToShowButton';
import useNavigateToExplorerPages from '../useNavigateToExplorerPages';
import WidgetHeader from '../WidgetHeader';
import FullView from './FullView';
import { Modal } from './styles';
import { WidgetGraphComponentProps } from './types';
import { getLocalStorageGraphVisibilityState, handleGraphClick } from './utils';
function WidgetGraphComponent({
widget,
queryResponse,
errorMessage,
version,
threshold,
headerMenuList,
isWarning,
isFetchingResponse,
setRequestData,
onClickHandler,
onDragSelect,
customOnDragSelect,
customTooltipElement,
openTracesButton,
onOpenTraceBtnClick,
customSeries,
customErrorMessage,
customOnRowClick,
customTimeRangeWindowForCoRelation,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
const { notifications } = useNotifications();
const { pathname, search } = useLocation();
const params = useUrlQuery();
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
const lineChartRef = useRef<ToggleGraphProps>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data?.result?.length || 0).fill(true),
);
const graphRef = useRef<HTMLDivElement>(null);
const [
currentGraphRef,
setCurrentGraphRef,
] = useState<RefObject<HTMLDivElement> | null>(graphRef);
useEffect(() => {
if (!lineChartRef.current) return;
graphVisibility.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const tableProcessedDataRef = useRef<RowData[]>([]);
const navigateToExplorerPages = useNavigateToExplorerPages();
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
const onToggleModal = useCallback(
(func: Dispatch<SetStateAction<boolean>>) => {
func((value) => !value);
},
[],
);
const updateDashboardMutation = useUpdateDashboard();
const onDeleteHandler = (): void => {
if (!selectedDashboard) return;
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
(e) => e.id !== widget.id,
);
const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
const updatedSelectedDashboard: Props = {
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
layout: updatedLayout,
},
id: selectedDashboard.id,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
}
setDeleteModal(false);
},
});
};
const onCloneHandler = async (): Promise<void> => {
if (!selectedDashboard) return;
const uuid = v4();
// this is added to make sure the cloned panel is of the same dimensions as the original one
const originalPanelLayout = selectedDashboard.data.layout?.find(
(l) => l.i === widget.id,
);
const newLayoutItem = placeWidgetAtBottom(
uuid,
selectedDashboard?.data.layout || [],
originalPanelLayout?.w || 6,
originalPanelLayout?.h || 6,
);
const layout = [...(selectedDashboard.data.layout || []), newLayoutItem];
updateDashboardMutation.mutateAsync(
{
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
layout,
widgets: [
...(selectedDashboard.data.widgets || []),
{
...{
...widget,
id: uuid,
},
},
],
},
},
{
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
}
notifications.success({
message: 'Panel cloned successfully, redirecting to new copy.',
});
const queryParams = {
graphType: widget?.panelTypes,
widgetId: uuid,
};
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
},
},
);
};
const handleOnView = (): void => {
const queryParams = {
[QueryParams.expandedWidgetId]: widget.id,
};
const updatedSearch = createQueryParams(queryParams);
const existingSearch = new URLSearchParams(search);
const isExpandedWidgetIdPresent = existingSearch.has(
QueryParams.expandedWidgetId,
);
if (isExpandedWidgetIdPresent) {
existingSearch.delete(QueryParams.expandedWidgetId);
}
const separator = existingSearch.toString() ? '&' : '';
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
safeNavigate({
pathname,
search: newSearch,
});
};
const handleOnDelete = (): void => {
onToggleModal(setDeleteModal);
};
const onDeleteModelHandler = (): void => {
onToggleModal(setDeleteModal);
};
const onToggleModelHandler = (): void => {
const existingSearchParams = new URLSearchParams(search);
existingSearchParams.delete(QueryParams.expandedWidgetId);
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
if (queryResponse.data?.payload) {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload?.data?.result,
name: widget.id,
});
setGraphVisibility(localStoredVisibilityState);
}
safeNavigate({
pathname,
search: createQueryParams(updatedQueryParams),
});
};
const [searchTerm, setSearchTerm] = useState<string>('');
const graphClick = useGraphClickToShowButton({
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
isButtonEnabled: (widget?.query?.builder?.queryData ?? []).some(
(q) =>
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
),
buttonClassName: 'view-onclick-show-button',
});
const navigateToExplorer = useNavigateToExplorer();
const graphClickHandler = (
xValue: number,
yValue: number,
mouseX: number,
mouseY: number,
metric?: { [key: string]: string },
queryData?: { queryName: string; inFocusOrNot: boolean },
): void => {
const customTracesTimeRange = getCustomTimeRangeWindowSweepInMS(
customTimeRangeWindowForCoRelation,
);
const { start, end } = getStartAndEndTimesInMilliseconds(
xValue,
customTracesTimeRange,
);
handleGraphClick({
xValue,
yValue,
mouseX,
mouseY,
metric,
queryData,
widget,
navigateToExplorerPages,
navigateToExplorer,
notifications,
graphClick,
...(customTimeRangeWindowForCoRelation
? { customTracesTimeRange: { start, end } }
: {}),
});
};
const { truncatedText, fullText } = useGetResolvedText({
text: widget.title as string,
maxLength: 100,
});
return (
<div
style={{
height: '100%',
}}
onMouseOver={(): void => {
setHovered(true);
}}
onFocus={(): void => {
setHovered(true);
}}
onMouseOut={(): void => {
setHovered(false);
}}
onBlur={(): void => {
setHovered(false);
}}
id={widget.id}
>
<Modal
destroyOnClose
onCancel={onDeleteModelHandler}
open={deleteModal}
confirmLoading={updateDashboardMutation.isLoading}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
centered
>
<Typography>Are you sure you want to delete this widget</Typography>
</Modal>
<Modal
title={
<Tooltip title={fullText} placement="top">
<span>{truncatedText || fullText || 'View'}</span>
</Tooltip>
}
footer={[]}
centered
open={isFullViewOpen}
onCancel={onToggleModelHandler}
width="85%"
destroyOnClose
className="widget-full-view"
>
<FullView
name={`${widget.id}expanded`}
version={version}
originalName={widget.id}
widget={widget}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
tableProcessedDataRef={tableProcessedDataRef}
onClickHandler={onClickHandler ?? graphClickHandler}
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
/>
</Modal>
<div className="drag-handle">
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={handleOnView}
onDelete={handleOnDelete}
onClone={onCloneHandler}
queryResponse={queryResponse}
errorMessage={errorMessage}
threshold={threshold}
headerMenuList={headerMenuList}
isWarning={isWarning}
isFetchingResponse={isFetchingResponse}
tableProcessedDataRef={tableProcessedDataRef}
setSearchTerm={setSearchTerm}
/>
</div>
{queryResponse.error && customErrorMessage && (
<div className="error-message-container">
<Typography.Text type="warning">{customErrorMessage}</Typography.Text>
</div>
)}
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
<Skeleton />
)}
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
<div
className={cx('widget-graph-container', widget.panelTypes)}
ref={graphRef}
>
<PanelWrapper
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}
setGraphVisibility={setGraphVisibility}
graphVisibility={graphVisibility}
onClickHandler={onClickHandler ?? graphClickHandler}
onDragSelect={onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
searchTerm={searchTerm}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customOnRowClick={customOnRowClick}
/>
</div>
)}
</div>
);
}
WidgetGraphComponent.defaultProps = {
yAxisUnit: undefined,
setLayout: undefined,
onClickHandler: undefined,
customTimeRangeWindowForCoRelation: undefined,
};
export default WidgetGraphComponent;