mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-14 08:21:33 +08:00

* feat: dashboard panel grouping initial setup * feat: added panel map to the dashboard response and subsequent types for the same * feat: added panel map to the dashboard response and subsequent types for the same * feat: added settings modal * feat: handle panel collapse and open changes * feat: handle creating panel map when dashboard layout changes * feat: handle creating panel map when dashboard layout changes * feat: refactor code * feat: handle multiple collapsable rows * fix: type issues * feat: handle row collapse button and scroll * feat: handle y axis movement for rows * feat: handle delete row * feat: handle settings name change * feat: disable collapse/uncollapse when dashboard loading to avoid async states * feat: decrease the height of the grouping row * fix: row height management * fix: handle empty row case * feat: remove resize handle from the row * feat: handle re-arrangement of panels * feat: increase height of default new widget * feat: added safety checks
557 lines
15 KiB
TypeScript
557 lines
15 KiB
TypeScript
/* eslint-disable sonarjs/cognitive-complexity */
|
|
import { LockFilled, WarningOutlined } from '@ant-design/icons';
|
|
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
|
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
import { FeatureKeys } from 'constants/features';
|
|
import { QueryParams } from 'constants/query';
|
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
|
import ROUTES from 'constants/routes';
|
|
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
|
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
|
import { useNotifications } from 'hooks/useNotifications';
|
|
import useUrlQuery from 'hooks/useUrlQuery';
|
|
import history from 'lib/history';
|
|
import { defaultTo, isUndefined } from 'lodash-es';
|
|
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
import {
|
|
getNextWidgets,
|
|
getPreviousWidgets,
|
|
getSelectedWidgetIndex,
|
|
} from 'providers/Dashboard/util';
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useSelector } from 'react-redux';
|
|
import { generatePath, useParams } from 'react-router-dom';
|
|
import { AppState } from 'store/reducers';
|
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
|
import { IField } from 'types/api/logs/fields';
|
|
import { EQueryType } from 'types/common/dashboard';
|
|
import { DataSource } from 'types/common/queryBuilder';
|
|
import AppReducer from 'types/reducer/app';
|
|
|
|
import LeftContainer from './LeftContainer';
|
|
import QueryTypeTag from './LeftContainer/QueryTypeTag';
|
|
import RightContainer from './RightContainer';
|
|
import { ThresholdProps } from './RightContainer/Threshold/types';
|
|
import TimeItems, { timePreferance } from './RightContainer/timeItems';
|
|
import {
|
|
ButtonContainer,
|
|
Container,
|
|
LeftContainerWrapper,
|
|
PanelContainer,
|
|
RightContainerWrapper,
|
|
} from './styles';
|
|
import { NewWidgetProps } from './types';
|
|
import {
|
|
getDefaultWidgetData,
|
|
getIsQueryModified,
|
|
handleQueryChange,
|
|
} from './utils';
|
|
|
|
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|
const {
|
|
selectedDashboard,
|
|
setSelectedDashboard,
|
|
setToScrollWidgetId,
|
|
} = useDashboard();
|
|
|
|
const { t } = useTranslation(['dashboard']);
|
|
|
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
|
|
|
const {
|
|
currentQuery,
|
|
stagedQuery,
|
|
redirectWithQueryBuilderData,
|
|
supersetQuery,
|
|
} = useQueryBuilder();
|
|
|
|
const isQueryModified = useMemo(
|
|
() => getIsQueryModified(currentQuery, stagedQuery),
|
|
[currentQuery, stagedQuery],
|
|
);
|
|
|
|
const { featureResponse } = useSelector<AppState, AppReducer>(
|
|
(state) => state.app,
|
|
);
|
|
|
|
const { widgets = [] } = selectedDashboard?.data || {};
|
|
|
|
const query = useUrlQuery();
|
|
|
|
const { dashboardId } = useParams<DashboardWidgetPageParams>();
|
|
|
|
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
const widgetId = query.get('widgetId');
|
|
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
|
const isWidgetNotPresent = isUndefined(selectedWidget);
|
|
if (isWidgetNotPresent) {
|
|
setIsNewDashboard(true);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const getWidget = useCallback(() => {
|
|
const widgetId = query.get('widgetId');
|
|
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
|
return defaultTo(
|
|
selectedWidget,
|
|
getDefaultWidgetData(widgetId || '', selectedGraph),
|
|
) as Widgets;
|
|
}, [query, selectedGraph, widgets]);
|
|
|
|
const [selectedWidget, setSelectedWidget] = useState(getWidget());
|
|
|
|
const [title, setTitle] = useState<string>(
|
|
selectedWidget?.title?.toString() || '',
|
|
);
|
|
const [description, setDescription] = useState<string>(
|
|
selectedWidget?.description || '',
|
|
);
|
|
const [yAxisUnit, setYAxisUnit] = useState<string>(
|
|
selectedWidget?.yAxisUnit || 'none',
|
|
);
|
|
|
|
const [stacked, setStacked] = useState<boolean>(
|
|
selectedWidget?.isStacked || false,
|
|
);
|
|
const [opacity, setOpacity] = useState<string>(selectedWidget?.opacity || '1');
|
|
const [thresholds, setThresholds] = useState<ThresholdProps[]>(
|
|
selectedWidget?.thresholds || [],
|
|
);
|
|
const [selectedNullZeroValue, setSelectedNullZeroValue] = useState<string>(
|
|
selectedWidget?.nullZeroValues || 'zero',
|
|
);
|
|
const [isFillSpans, setIsFillSpans] = useState<boolean>(
|
|
selectedWidget?.fillSpans || false,
|
|
);
|
|
const [saveModal, setSaveModal] = useState(false);
|
|
const [discardModal, setDiscardModal] = useState(false);
|
|
|
|
const [softMin, setSoftMin] = useState<number | null>(
|
|
selectedWidget?.softMin === null || selectedWidget?.softMin === undefined
|
|
? null
|
|
: selectedWidget?.softMin || 0,
|
|
);
|
|
|
|
const [selectedLogFields, setSelectedLogFields] = useState<IField[] | null>(
|
|
selectedWidget?.selectedLogFields || null,
|
|
);
|
|
|
|
const [selectedTracesFields, setSelectedTracesFields] = useState(
|
|
selectedWidget?.selectedTracesFields || null,
|
|
);
|
|
|
|
const [softMax, setSoftMax] = useState<number | null>(
|
|
selectedWidget?.softMax === null || selectedWidget?.softMax === undefined
|
|
? null
|
|
: selectedWidget?.softMax || 0,
|
|
);
|
|
|
|
useEffect(() => {
|
|
setSelectedWidget((prev) => {
|
|
if (!prev) {
|
|
return prev;
|
|
}
|
|
return {
|
|
...prev,
|
|
query: currentQuery,
|
|
title,
|
|
description,
|
|
isStacked: stacked,
|
|
opacity,
|
|
nullZeroValues: selectedNullZeroValue,
|
|
yAxisUnit,
|
|
thresholds,
|
|
softMin,
|
|
softMax,
|
|
fillSpans: isFillSpans,
|
|
selectedLogFields,
|
|
selectedTracesFields,
|
|
};
|
|
});
|
|
}, [
|
|
currentQuery,
|
|
description,
|
|
isFillSpans,
|
|
opacity,
|
|
selectedLogFields,
|
|
selectedNullZeroValue,
|
|
selectedTracesFields,
|
|
softMax,
|
|
softMin,
|
|
stacked,
|
|
thresholds,
|
|
title,
|
|
yAxisUnit,
|
|
]);
|
|
|
|
const closeModal = (): void => {
|
|
setSaveModal(false);
|
|
setDiscardModal(false);
|
|
};
|
|
|
|
const [graphType, setGraphType] = useState(selectedGraph);
|
|
|
|
const getSelectedTime = useCallback(
|
|
() =>
|
|
TimeItems.find(
|
|
(e) => e.enum === (selectedWidget?.timePreferance || 'GLOBAL_TIME'),
|
|
),
|
|
[selectedWidget],
|
|
);
|
|
|
|
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
|
name: getSelectedTime()?.name || '',
|
|
enum: selectedWidget?.timePreferance || 'GLOBAL_TIME',
|
|
});
|
|
|
|
const updateDashboardMutation = useUpdateDashboard();
|
|
|
|
const { afterWidgets, preWidgets } = useMemo(() => {
|
|
if (!selectedDashboard) {
|
|
return {
|
|
selectedWidget: {} as Widgets,
|
|
preWidgets: [],
|
|
afterWidgets: [],
|
|
};
|
|
}
|
|
|
|
const widgetId = query.get('widgetId');
|
|
|
|
const selectedWidgetIndex = getSelectedWidgetIndex(
|
|
selectedDashboard,
|
|
widgetId,
|
|
);
|
|
|
|
const preWidgets = getPreviousWidgets(selectedDashboard, selectedWidgetIndex);
|
|
|
|
const afterWidgets = getNextWidgets(selectedDashboard, selectedWidgetIndex);
|
|
|
|
const selectedWidget = (selectedDashboard.data.widgets || [])[
|
|
selectedWidgetIndex || 0
|
|
];
|
|
|
|
return { selectedWidget, preWidgets, afterWidgets };
|
|
}, [selectedDashboard, query]);
|
|
|
|
const { notifications } = useNotifications();
|
|
|
|
const onClickSaveHandler = useCallback(() => {
|
|
if (!selectedDashboard) {
|
|
return;
|
|
}
|
|
|
|
const widgetId = query.get('widgetId');
|
|
let updatedLayout = selectedDashboard.data.layout || [];
|
|
if (isNewDashboard) {
|
|
updatedLayout = [
|
|
{
|
|
i: widgetId || '',
|
|
w: 6,
|
|
x: 0,
|
|
h: 6,
|
|
y: 0,
|
|
},
|
|
...updatedLayout,
|
|
];
|
|
}
|
|
const dashboard: Dashboard = {
|
|
...selectedDashboard,
|
|
uuid: selectedDashboard.uuid,
|
|
data: {
|
|
...selectedDashboard.data,
|
|
widgets: [
|
|
...preWidgets,
|
|
{
|
|
...(selectedWidget || ({} as Widgets)),
|
|
description: selectedWidget?.description || '',
|
|
timePreferance: selectedTime.enum,
|
|
isStacked: selectedWidget?.isStacked || false,
|
|
opacity: selectedWidget?.opacity || '1',
|
|
nullZeroValues: selectedWidget?.nullZeroValues || 'zero',
|
|
title: selectedWidget?.title,
|
|
yAxisUnit: selectedWidget?.yAxisUnit,
|
|
panelTypes: graphType,
|
|
query: currentQuery,
|
|
thresholds: selectedWidget?.thresholds,
|
|
softMin: selectedWidget?.softMin || 0,
|
|
softMax: selectedWidget?.softMax || 0,
|
|
fillSpans: selectedWidget?.fillSpans,
|
|
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
|
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
|
},
|
|
...afterWidgets,
|
|
],
|
|
layout: [...updatedLayout],
|
|
},
|
|
};
|
|
|
|
updateDashboardMutation.mutateAsync(dashboard, {
|
|
onSuccess: () => {
|
|
setSelectedDashboard(dashboard);
|
|
setToScrollWidgetId(selectedWidget?.id || '');
|
|
featureResponse.refetch();
|
|
history.push({
|
|
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
|
});
|
|
},
|
|
onError: () => {
|
|
notifications.error({
|
|
message: SOMETHING_WENT_WRONG,
|
|
});
|
|
},
|
|
});
|
|
}, [
|
|
selectedDashboard,
|
|
query,
|
|
isNewDashboard,
|
|
preWidgets,
|
|
selectedWidget,
|
|
selectedTime.enum,
|
|
graphType,
|
|
currentQuery,
|
|
afterWidgets,
|
|
updateDashboardMutation,
|
|
setSelectedDashboard,
|
|
setToScrollWidgetId,
|
|
featureResponse,
|
|
dashboardId,
|
|
notifications,
|
|
]);
|
|
|
|
const onClickDiscardHandler = useCallback(() => {
|
|
if (isQueryModified) {
|
|
setDiscardModal(true);
|
|
return;
|
|
}
|
|
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
|
}, [dashboardId, isQueryModified]);
|
|
|
|
const discardChanges = useCallback(() => {
|
|
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
|
}, [dashboardId]);
|
|
|
|
const setGraphHandler = (type: PANEL_TYPES): void => {
|
|
const updatedQuery = handleQueryChange(type as any, supersetQuery);
|
|
setGraphType(type);
|
|
redirectWithQueryBuilderData(
|
|
updatedQuery,
|
|
{ [QueryParams.graphType]: type },
|
|
undefined,
|
|
true,
|
|
);
|
|
};
|
|
|
|
const onSaveDashboard = useCallback((): void => {
|
|
setSaveModal(true);
|
|
}, []);
|
|
|
|
const isQueryBuilderActive = useIsFeatureDisabled(
|
|
FeatureKeys.QUERY_BUILDER_PANELS,
|
|
);
|
|
|
|
const isNewTraceLogsAvailable =
|
|
isQueryBuilderActive &&
|
|
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
|
currentQuery.builder.queryData.find(
|
|
(query) => query.dataSource !== DataSource.METRICS,
|
|
) !== undefined;
|
|
|
|
const isSaveDisabled = useMemo(() => {
|
|
// new created dashboard
|
|
if (selectedWidget?.id === 'empty') {
|
|
return isNewTraceLogsAvailable;
|
|
}
|
|
|
|
const isTraceOrLogsQueryBuilder =
|
|
currentQuery.builder.queryData.find(
|
|
(query) =>
|
|
query.dataSource === DataSource.TRACES ||
|
|
query.dataSource === DataSource.LOGS,
|
|
) !== undefined;
|
|
|
|
if (isTraceOrLogsQueryBuilder) {
|
|
return false;
|
|
}
|
|
|
|
return isNewTraceLogsAvailable;
|
|
}, [
|
|
currentQuery.builder.queryData,
|
|
selectedWidget?.id,
|
|
isNewTraceLogsAvailable,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard);
|
|
registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler);
|
|
|
|
return (): void => {
|
|
deregisterShortcut(DashboardShortcuts.SaveChanges);
|
|
deregisterShortcut(DashboardShortcuts.DiscardChanges);
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [onSaveDashboard]);
|
|
|
|
return (
|
|
<Container>
|
|
<Flex justify="space-between" align="center">
|
|
<FacingIssueBtn
|
|
attributes={{
|
|
uuid: selectedDashboard?.uuid,
|
|
title: selectedDashboard?.data.title,
|
|
panelType: graphType,
|
|
widgetId: query.get('widgetId'),
|
|
queryType: currentQuery.queryType,
|
|
screen: 'Dashboard list page',
|
|
}}
|
|
eventName="Dashboard: Facing Issues in dashboard"
|
|
buttonText="Facing Issues in dashboard"
|
|
message={`Hi Team,
|
|
|
|
I am facing issues configuring dashboard in SigNoz. Here are my dashboard details
|
|
|
|
Name: ${selectedDashboard?.data.title || ''}
|
|
Panel type: ${graphType}
|
|
Dashboard Id: ${selectedDashboard?.uuid || ''}
|
|
|
|
Thanks`}
|
|
/>
|
|
<ButtonContainer>
|
|
{isSaveDisabled && (
|
|
<Tooltip title={MESSAGE.PANEL}>
|
|
<Button
|
|
icon={<LockFilled />}
|
|
type="primary"
|
|
disabled={isSaveDisabled}
|
|
onClick={onSaveDashboard}
|
|
>
|
|
Save Changes
|
|
</Button>
|
|
</Tooltip>
|
|
)}
|
|
|
|
{!isSaveDisabled && (
|
|
<Button
|
|
type="primary"
|
|
data-testid="new-widget-save"
|
|
loading={updateDashboardMutation.isLoading}
|
|
disabled={isSaveDisabled}
|
|
onClick={onSaveDashboard}
|
|
>
|
|
Save Changes
|
|
</Button>
|
|
)}
|
|
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
|
|
</ButtonContainer>
|
|
</Flex>
|
|
|
|
<PanelContainer>
|
|
<LeftContainerWrapper flex={5}>
|
|
{selectedWidget && (
|
|
<LeftContainer
|
|
selectedGraph={graphType}
|
|
selectedLogFields={selectedLogFields}
|
|
setSelectedLogFields={setSelectedLogFields}
|
|
selectedTracesFields={selectedTracesFields}
|
|
setSelectedTracesFields={setSelectedTracesFields}
|
|
selectedWidget={selectedWidget}
|
|
selectedTime={selectedTime}
|
|
/>
|
|
)}
|
|
</LeftContainerWrapper>
|
|
|
|
<RightContainerWrapper flex={1}>
|
|
<RightContainer
|
|
setGraphHandler={setGraphHandler}
|
|
title={title}
|
|
setTitle={setTitle}
|
|
description={description}
|
|
setDescription={setDescription}
|
|
stacked={stacked}
|
|
setStacked={setStacked}
|
|
opacity={opacity}
|
|
yAxisUnit={yAxisUnit}
|
|
setOpacity={setOpacity}
|
|
selectedNullZeroValue={selectedNullZeroValue}
|
|
setSelectedNullZeroValue={setSelectedNullZeroValue}
|
|
selectedGraph={graphType}
|
|
setSelectedTime={setSelectedTime}
|
|
selectedTime={selectedTime}
|
|
setYAxisUnit={setYAxisUnit}
|
|
thresholds={thresholds}
|
|
setThresholds={setThresholds}
|
|
selectedWidget={selectedWidget}
|
|
isFillSpans={isFillSpans}
|
|
setIsFillSpans={setIsFillSpans}
|
|
softMin={softMin}
|
|
setSoftMin={setSoftMin}
|
|
softMax={softMax}
|
|
setSoftMax={setSoftMax}
|
|
/>
|
|
</RightContainerWrapper>
|
|
</PanelContainer>
|
|
<Modal
|
|
title={
|
|
isQueryModified ? (
|
|
<Space>
|
|
<WarningOutlined style={{ fontSize: '16px', color: '#fdd600' }} />
|
|
Unsaved Changes
|
|
</Space>
|
|
) : (
|
|
'Save Widget'
|
|
)
|
|
}
|
|
focusTriggerAfterClose
|
|
forceRender
|
|
destroyOnClose
|
|
closable
|
|
onCancel={closeModal}
|
|
onOk={onClickSaveHandler}
|
|
confirmLoading={updateDashboardMutation.isLoading}
|
|
centered
|
|
open={saveModal}
|
|
width={600}
|
|
>
|
|
{!isQueryModified ? (
|
|
<Typography>
|
|
{t('your_graph_build_with')}{' '}
|
|
<QueryTypeTag queryType={currentQuery.queryType} />
|
|
{t('dashboard_ok_confirm')}
|
|
</Typography>
|
|
) : (
|
|
<Typography>{t('dashboard_unsave_changes')} </Typography>
|
|
)}
|
|
</Modal>
|
|
<Modal
|
|
title={
|
|
<Space>
|
|
<WarningOutlined style={{ fontSize: '16px', color: '#fdd600' }} />
|
|
Unsaved Changes
|
|
</Space>
|
|
}
|
|
focusTriggerAfterClose
|
|
forceRender
|
|
destroyOnClose
|
|
closable
|
|
onCancel={closeModal}
|
|
onOk={discardChanges}
|
|
centered
|
|
open={discardModal}
|
|
width={600}
|
|
>
|
|
<Typography>{t('dashboard_unsave_changes')}</Typography>
|
|
</Modal>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
export default NewWidget;
|