feat: celery - misc feedback fixes (#7019)

* feat: celery - misc feedback fixes

* feat: enabled better sharing and auto-refresh

* feat: made task name filter more visible
This commit is contained in:
SagarRajput-7 2025-02-04 09:41:27 +05:30 committed by GitHub
parent 3b550c485d
commit cf95b15ba1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 352 additions and 298 deletions

View File

@ -1,7 +1,6 @@
import './CeleryOverviewConfigOptions.styles.scss'; import './CeleryOverviewConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Row, Select, Spin } from 'antd';
import { Button, Row, Select, Spin, Tooltip } from 'antd';
import { import {
getValuesFromQueryParams, getValuesFromQueryParams,
setQueryParamsFromOptions, setQueryParamsFromOptions,
@ -10,10 +9,7 @@ import { useCeleryFilterOptions } from 'components/CeleryTask/useCeleryFilterOpt
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon'; import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
interface SelectOptionConfig { interface SelectOptionConfig {
placeholder: string; placeholder: string;
@ -66,10 +62,6 @@ function FilterSelect({
} }
function CeleryOverviewConfigOptions(): JSX.Element { function CeleryOverviewConfigOptions(): JSX.Element {
const [isURLCopied, setIsURLCopied] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
const selectConfigs: SelectOptionConfig[] = [ const selectConfigs: SelectOptionConfig[] = [
{ {
placeholder: 'Service Name', placeholder: 'Service Name',
@ -98,14 +90,6 @@ function CeleryOverviewConfigOptions(): JSX.Element {
}, },
]; ];
const handleShareURL = (): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
};
return ( return (
<div className="celery-overview-filters"> <div className="celery-overview-filters">
<Row className="celery-filters"> <Row className="celery-filters">
@ -118,19 +102,6 @@ function CeleryOverviewConfigOptions(): JSX.Element {
/> />
))} ))}
</Row> </Row>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={handleShareURL}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div> </div>
); );
} }

View File

@ -37,7 +37,6 @@
font-weight: 600; font-weight: 600;
line-height: 18px; /* 163.636% */ line-height: 18px; /* 163.636% */
letter-spacing: 0.44px; letter-spacing: 0.44px;
text-transform: uppercase;
&::before { &::before {
background-color: transparent; background-color: transparent;

View File

@ -218,20 +218,26 @@ function getColumns(data: RowData[]): TableColumnsType<RowData> {
showTitle: false, showTitle: false,
}, },
width: 200, width: 200,
sorter: (a: RowData, b: RowData): number => sorter: (a: RowData, b: RowData): number => {
String(a.error_percentage).localeCompare(String(b.error_percentage)), const aValue = Number(a.error_percentage);
const bValue = Number(b.error_percentage);
return aValue - bValue;
},
render: ProgressRender, render: ProgressRender,
}, },
{ {
title: 'LATENCY (P95)', title: 'LATENCY (P95) in ms',
dataIndex: 'p95_latency', dataIndex: 'p95_latency',
key: 'p95_latency', key: 'p95_latency',
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
}, },
width: 100, width: 100,
sorter: (a: RowData, b: RowData): number => sorter: (a: RowData, b: RowData): number => {
String(a.p95_latency).localeCompare(String(b.p95_latency)), const aValue = Number(a.p95_latency);
const bValue = Number(b.p95_latency);
return aValue - bValue;
},
render: (value: number | string): string => { render: (value: number | string): string => {
if (!isNumber(value)) return value.toString(); if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3); return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
@ -245,8 +251,11 @@ function getColumns(data: RowData[]): TableColumnsType<RowData> {
showTitle: false, showTitle: false,
}, },
width: 100, width: 100,
sorter: (a: RowData, b: RowData): number => sorter: (a: RowData, b: RowData): number => {
String(a.throughput).localeCompare(String(b.throughput)), const aValue = Number(a.throughput);
const bValue = Number(b.throughput);
return aValue - bValue;
},
render: (value: number | string): string => { render: (value: number | string): string => {
if (!isNumber(value)) return value.toString(); if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3); return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);

View File

@ -2,24 +2,16 @@ import './CeleryTaskDetail.style.scss';
import { Color, Spacing } from '@signozhq/design-tokens'; import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd'; import { Divider, Drawer, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph'; import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph';
import { createFiltersFromData } from '../CeleryUtils';
import { useNavigateToTraces } from '../useNavigateToTraces'; import { useNavigateToTraces } from '../useNavigateToTraces';
export type CeleryTaskData = { export type CeleryTaskData = {
@ -39,40 +31,6 @@ export type CeleryTaskDetailProps = {
drawerOpen: boolean; drawerOpen: boolean;
}; };
const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}));
};
export default function CeleryTaskDetail({ export default function CeleryTaskDetail({
widgetData, widgetData,
taskData, taskData,
@ -85,7 +43,7 @@ export default function CeleryTaskDetail({
!!taskData.entity && !!taskData.timeRange[0] && drawerOpen; !!taskData.entity && !!taskData.timeRange[0] && drawerOpen;
const formatTimestamp = (timestamp: number): string => const formatTimestamp = (timestamp: number): string =>
dayjs(timestamp * 1000).format('MM-DD-YYYY hh:mm A'); dayjs(timestamp).format('DD-MM-YYYY hh:mm A');
const [totalTask, setTotalTask] = useState(0); const [totalTask, setTotalTask] = useState(0);
@ -93,52 +51,9 @@ export default function CeleryTaskDetail({
setTotalTask((graphData?.result?.[0] as any)?.table?.rows.length); setTotalTask((graphData?.result?.[0] as any)?.table?.rows.length);
}; };
// set time range
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startTime = taskData.timeRange[0]; const startTime = taskData.timeRange[0];
const endTime = taskData.timeRange[1]; const endTime = taskData.timeRange[1];
const urlQuery = useUrlQuery();
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, startTime.toString());
urlQuery.set(QueryParams.endTime, endTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
if (startTime !== endTime) {
dispatch(UpdateTimeInterval('custom', [startTime, endTime]));
}
return (): void => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
if (selectedTime !== 'custom') {
dispatch(UpdateTimeInterval(selectedTime));
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
dispatch(UpdateTimeInterval('custom', [minTime / 1e6, maxTime / 1e6]));
urlQuery.set(QueryParams.startTime, Math.floor(minTime / 1e6).toString());
urlQuery.set(QueryParams.endTime, Math.floor(maxTime / 1e6).toString());
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const navigateToTrace = useNavigateToTraces(); const navigateToTrace = useNavigateToTraces();
return ( return (
@ -149,10 +64,8 @@ export default function CeleryTaskDetail({
<Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text> <Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text>
<div> <div>
<Typography.Text className="subtitle"> <Typography.Text className="subtitle">
{`${formatTimestamp(taskData.timeRange[0])} ${ {`${formatTimestamp(startTime)} ${
taskData.timeRange[1] endTime ? `- ${formatTimestamp(endTime)}` : ''
? `- ${formatTimestamp(taskData.timeRange[1])}`
: ''
}`} }`}
</Typography.Text> </Typography.Text>
<Divider type="vertical" /> <Divider type="vertical" />
@ -185,8 +98,10 @@ export default function CeleryTaskDetail({
...rowData, ...rowData,
[taskData.entity]: taskData.value, [taskData.entity]: taskData.value,
}); });
navigateToTrace(filters); navigateToTrace(filters, startTime, endTime);
}} }}
start={startTime}
end={endTime}
/> />
</Drawer> </Drawer>
); );

View File

@ -19,6 +19,10 @@ import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail'; import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import { useGetGraphCustomSeries } from '../useGetGraphCustomSeries'; import { useGetGraphCustomSeries } from '../useGetGraphCustomSeries';
import { import {
celeryAllStateWidgetData, celeryAllStateWidgetData,
@ -72,26 +76,60 @@ function CeleryTaskBar({
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All); const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const celeryAllStateData = useMemo( const celeryAllStateData = useMemo(
() => celeryAllStateWidgetData(minTime, maxTime), () => celeryAllStateWidgetData(minTime, maxTime),
[minTime, maxTime], [minTime, maxTime],
); );
const celeryAllStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryAllStateData),
[selectedFilters, celeryAllStateData],
);
const celeryFailedStateData = useMemo( const celeryFailedStateData = useMemo(
() => celeryFailedStateWidgetData(minTime, maxTime), () => celeryFailedStateWidgetData(minTime, maxTime),
[minTime, maxTime], [minTime, maxTime],
); );
const celeryFailedStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryFailedStateData),
[selectedFilters, celeryFailedStateData],
);
const celeryRetryStateData = useMemo( const celeryRetryStateData = useMemo(
() => celeryRetryStateWidgetData(minTime, maxTime), () => celeryRetryStateWidgetData(minTime, maxTime),
[minTime, maxTime], [minTime, maxTime],
); );
const celeryRetryStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryRetryStateData),
[selectedFilters, celeryRetryStateData],
);
const celerySuccessStateData = useMemo( const celerySuccessStateData = useMemo(
() => celerySuccessStateWidgetData(minTime, maxTime), () => celerySuccessStateWidgetData(minTime, maxTime),
[minTime, maxTime], [minTime, maxTime],
); );
const celerySuccessStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celerySuccessStateData),
[selectedFilters, celerySuccessStateData],
);
const onGraphClick = ( const onGraphClick = (
widgetData: Widgets, widgetData: Widgets,
xValue: number, xValue: number,
@ -141,7 +179,7 @@ function CeleryTaskBar({
<div className="celery-task-graph-grid-content"> <div className="celery-task-graph-grid-content">
{barState === CeleryTaskState.All && ( {barState === CeleryTaskState.All && (
<GridCard <GridCard
widget={celeryAllStateData} widget={celeryAllStateFilteredData}
headerMenuList={[...ViewMenuAction]} headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled} isQueryEnabled={queryEnabled}
@ -153,7 +191,7 @@ function CeleryTaskBar({
)} )}
{barState === CeleryTaskState.Failed && ( {barState === CeleryTaskState.Failed && (
<GridCard <GridCard
widget={celeryFailedStateData} widget={celeryFailedStateFilteredData}
headerMenuList={[...ViewMenuAction]} headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled} isQueryEnabled={queryEnabled}
@ -165,7 +203,7 @@ function CeleryTaskBar({
)} )}
{barState === CeleryTaskState.Retry && ( {barState === CeleryTaskState.Retry && (
<GridCard <GridCard
widget={celeryRetryStateData} widget={celeryRetryStateFilteredData}
headerMenuList={[...ViewMenuAction]} headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled} isQueryEnabled={queryEnabled}
@ -177,7 +215,7 @@ function CeleryTaskBar({
)} )}
{barState === CeleryTaskState.Successful && ( {barState === CeleryTaskState.Successful && (
<GridCard <GridCard
widget={celerySuccessStateData} widget={celerySuccessStateFilteredData}
headerMenuList={[...ViewMenuAction]} headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled} isQueryEnabled={queryEnabled}

View File

@ -117,7 +117,7 @@
.metric-page-grid { .metric-page-grid {
display: grid; display: grid;
grid-template-columns: 50% 50%; grid-template-columns: 30% 19%;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
@ -144,6 +144,11 @@
gap: 16px; gap: 16px;
align-items: center; align-items: center;
border: 1px dashed var(--bg-slate-50);
border-radius: 4px;
padding: 6px 24px 6px 12px;
width: max-content;
.configure-option-Info-text { .configure-option-Info-text {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
font-size: 12px; font-size: 12px;
@ -241,11 +246,13 @@
.lightMode { .lightMode {
.celery-task-graph-grid-container { .celery-task-graph-grid-container {
.celery-task-graph-grid { .celery-task-graph-worker-count {
.celery-task-graph-worker-count { border: 1px solid var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-300); background: unset;
background: unset; }
}
.row-panel .row-panel-section .section-title {
color: var(--bg-ink-400);
} }
} }
@ -274,4 +281,8 @@
background-color: var(--bg-ink-400); background-color: var(--bg-ink-400);
} }
} }
.configure-option-Info {
border: 1px dashed var(--bg-robin-400);
}
} }

View File

@ -32,6 +32,9 @@ function CeleryTaskGraph({
openTracesButton, openTracesButton,
onOpenTraceBtnClick, onOpenTraceBtnClick,
applyCeleryTaskFilter, applyCeleryTaskFilter,
customErrorMessage,
start,
end,
}: { }: {
widgetData: Widgets; widgetData: Widgets;
onClick?: (task: CaptureDataProps) => void; onClick?: (task: CaptureDataProps) => void;
@ -42,6 +45,9 @@ function CeleryTaskGraph({
openTracesButton?: boolean; openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void; onOpenTraceBtnClick?: (record: RowData) => void;
applyCeleryTaskFilter?: boolean; applyCeleryTaskFilter?: boolean;
customErrorMessage?: string;
start?: number;
end?: number;
}): JSX.Element { }): JSX.Element {
const history = useHistory(); const history = useHistory();
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -116,6 +122,9 @@ function CeleryTaskGraph({
openTracesButton={openTracesButton} openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick} onOpenTraceBtnClick={onOpenTraceBtnClick}
version={ENTITY_VERSION_V4} version={ENTITY_VERSION_V4}
customErrorMessage={customErrorMessage}
start={start}
end={end}
/> />
</Card> </Card>
); );
@ -129,6 +138,9 @@ CeleryTaskGraph.defaultProps = {
openTracesButton: false, openTracesButton: false,
onOpenTraceBtnClick: undefined, onOpenTraceBtnClick: undefined,
applyCeleryTaskFilter: false, applyCeleryTaskFilter: false,
customErrorMessage: undefined,
start: undefined,
end: undefined,
}; };
export default CeleryTaskGraph; export default CeleryTaskGraph;

View File

@ -123,11 +123,12 @@ export default function CeleryTaskGraphGrid({
key={celeryActiveTasksData.id} key={celeryActiveTasksData.id}
widgetData={celeryActiveTasksData} widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled} queryEnabled={queryEnabled}
customErrorMessage="Enable Flower metrics to view this graph"
/> />
<Card className="celery-task-graph-worker-count"> <Card className="celery-task-graph-worker-count">
<div className="worker-count-header"> <div className="worker-count-header">
<Typography.Text className="worker-count-header-text"> <Typography.Text className="worker-count-header-text">
Worker Count Worker Online
</Typography.Text> </Typography.Text>
</div> </div>
<div className="worker-count-text-container"> <div className="worker-count-text-container">
@ -173,7 +174,7 @@ export default function CeleryTaskGraphGrid({
{!collapsedSections.traceBasedGraphs && ( {!collapsedSections.traceBasedGraphs && (
<> <>
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} /> <CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskLatencyGraph onClick={onClick} queryEnabled={queryEnabled} /> <CeleryTaskLatencyGraph queryEnabled={queryEnabled} />
<div className="celery-task-graph-grid-bottom"> <div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => ( {bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph <CeleryTaskGraph

View File

@ -42,21 +42,7 @@ export const celeryAllStateWidgetData = (
disabled: false, disabled: false,
expression: 'A', expression: 'A',
filters: { filters: {
items: [ items: [],
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
op: 'AND', op: 'AND',
}, },
functions: [], functions: [],
@ -113,7 +99,7 @@ export const celeryRetryStateWidgetData = (
filters: { filters: {
items: [ items: [
{ {
id: '6d97eed3', id: uuidv4(),
key: { key: {
dataType: DataTypes.String, dataType: DataTypes.String,
id: 'celery.state--string--tag--false', id: 'celery.state--string--tag--false',
@ -179,7 +165,7 @@ export const celeryFailedStateWidgetData = (
filters: { filters: {
items: [ items: [
{ {
id: '5983eae2', id: uuidv4(),
key: { key: {
dataType: DataTypes.String, dataType: DataTypes.String,
id: 'celery.state--string--tag--false', id: 'celery.state--string--tag--false',
@ -245,7 +231,7 @@ export const celerySuccessStateWidgetData = (
filters: { filters: {
items: [ items: [
{ {
id: '000c5a93', id: uuidv4(),
key: { key: {
dataType: DataTypes.String, dataType: DataTypes.String,
id: 'celery.state--string--tag--false', id: 'celery.state--string--tag--false',
@ -602,7 +588,7 @@ export const celeryTaskLatencyWidgetData = (
reduceTo: 'avg', reduceTo: 'avg',
spaceAggregation: 'sum', spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime), stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p99', timeAggregation: type || 'p99',
}, },
], ],
yAxisUnit: 'ns', yAxisUnit: 'ns',
@ -686,7 +672,7 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
filters: { filters: {
items: [ items: [
{ {
id: '9e09c9ed', id: uuidv4(),
key: { key: {
dataType: DataTypes.String, dataType: DataTypes.String,
id: 'celery.state--string--tag--false', id: 'celery.state--string--tag--false',
@ -755,7 +741,7 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
filters: { filters: {
items: [ items: [
{ {
id: '2330f906', id: uuidv4(),
key: { key: {
dataType: DataTypes.String, dataType: DataTypes.String,
id: 'celery.state--string--tag--false', id: 'celery.state--string--tag--false',
@ -822,7 +808,7 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
filters: { filters: {
items: [ items: [
{ {
id: 'ec3df7b7', id: uuidv4(),
key: { key: {
dataType: DataTypes.String, dataType: DataTypes.String,
id: 'celery.state--string--tag--false', id: 'celery.state--string--tag--false',
@ -945,33 +931,19 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
queryData: [ queryData: [
{ {
aggregateAttribute: { aggregateAttribute: {
dataType: DataTypes.EMPTY, dataType: DataTypes.String,
id: '------false', id: 'span_id--string----true',
isColumn: false, isColumn: true,
isJSON: false, isJSON: false,
key: '', key: 'span_id',
type: '', type: '',
}, },
aggregateOperator: 'count', aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES, dataSource: DataSource.TRACES,
disabled: false, disabled: false,
expression: 'A', expression: 'A',
filters: { filters: {
items: [ items: [],
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
op: 'AND', op: 'AND',
}, },
functions: [], functions: [],
@ -981,10 +953,10 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
limit: null, limit: null,
orderBy: [], orderBy: [],
queryName: 'A', queryName: 'A',
reduceTo: 'avg', reduceTo: 'last',
spaceAggregation: 'sum', spaceAggregation: 'sum',
stepInterval: 60, stepInterval: 60,
timeAggregation: 'rate', timeAggregation: 'count_distinct',
}, },
], ],
}), }),
@ -998,14 +970,14 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
queryData: [ queryData: [
{ {
aggregateAttribute: { aggregateAttribute: {
dataType: DataTypes.EMPTY, dataType: DataTypes.String,
id: '------false', id: 'span_id--string----true',
isColumn: false, isColumn: true,
isJSON: false, isJSON: false,
key: '', key: 'span_id',
type: '', type: '',
}, },
aggregateOperator: 'count', aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES, dataSource: DataSource.TRACES,
disabled: false, disabled: false,
expression: 'A', expression: 'A',
@ -1034,10 +1006,10 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
limit: null, limit: null,
orderBy: [], orderBy: [],
queryName: 'A', queryName: 'A',
reduceTo: 'avg', reduceTo: 'last',
spaceAggregation: 'sum', spaceAggregation: 'sum',
stepInterval: 60, stepInterval: 60,
timeAggregation: 'rate', timeAggregation: 'count_distinct',
}, },
], ],
}), }),
@ -1051,14 +1023,14 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
queryData: [ queryData: [
{ {
aggregateAttribute: { aggregateAttribute: {
dataType: DataTypes.EMPTY, dataType: DataTypes.String,
id: '------false', id: 'span_id--string----true',
isColumn: false, isColumn: true,
isJSON: false, isJSON: false,
key: '', key: 'span_id',
type: '', type: '',
}, },
aggregateOperator: 'count', aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES, dataSource: DataSource.TRACES,
disabled: false, disabled: false,
expression: 'A', expression: 'A',
@ -1087,10 +1059,10 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
limit: null, limit: null,
orderBy: [], orderBy: [],
queryName: 'A', queryName: 'A',
reduceTo: 'avg', reduceTo: 'last',
spaceAggregation: 'sum', spaceAggregation: 'sum',
stepInterval: 60, stepInterval: 60,
timeAggregation: 'rate', timeAggregation: 'count_distinct',
}, },
], ],
}), }),
@ -1104,13 +1076,13 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
queryData: [ queryData: [
{ {
aggregateAttribute: { aggregateAttribute: {
dataType: DataTypes.EMPTY, dataType: DataTypes.String,
id: '------false', id: 'span_id--string----true',
isColumn: false, isColumn: true,
key: '', key: 'span_id',
type: '', type: '',
}, },
aggregateOperator: 'count', aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES, dataSource: DataSource.TRACES,
disabled: false, disabled: false,
expression: 'A', expression: 'A',
@ -1139,10 +1111,10 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
limit: null, limit: null,
orderBy: [], orderBy: [],
queryName: 'A', queryName: 'A',
reduceTo: 'avg', reduceTo: 'last',
spaceAggregation: 'sum', spaceAggregation: 'sum',
stepInterval: 60, stepInterval: 60,
timeAggregation: 'rate', timeAggregation: 'count_distinct',
}, },
], ],
}), }),

View File

@ -6,8 +6,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config'; import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard'; import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles'; import { Card } from 'container/GridCardLayout/styles';
import { Button } from 'container/MetricsApplication/Tabs/styles';
import { onGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils'; import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -16,15 +19,13 @@ import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import { import {
applyCeleryFilterOnWidgetData, applyCeleryFilterOnWidgetData,
createFiltersFromData,
getFiltersFromQueryParams, getFiltersFromQueryParams,
} from '../CeleryUtils'; } from '../CeleryUtils';
import { import { useNavigateToTraces } from '../useNavigateToTraces';
celeryTaskLatencyWidgetData, import { celeryTaskLatencyWidgetData } from './CeleryTaskGraphUtils';
celeryTimeSeriesTablesWidgetData,
} from './CeleryTaskGraphUtils';
interface TabData { interface TabData {
label: string; label: string;
@ -38,10 +39,8 @@ export enum CeleryTaskGraphState {
} }
function CeleryTaskLatencyGraph({ function CeleryTaskLatencyGraph({
onClick,
queryEnabled, queryEnabled,
}: { }: {
onClick: (task: CaptureDataProps) => void;
queryEnabled: boolean; queryEnabled: boolean;
}): JSX.Element { }): JSX.Element {
const history = useHistory(); const history = useHistory();
@ -106,31 +105,51 @@ function CeleryTaskLatencyGraph({
[celeryTaskLatencyData, selectedFilters], [celeryTaskLatencyData, selectedFilters],
); );
const onGraphClick = ( const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
xValue: number, const [entityData, setEntityData] = useState<{
_yValue: number, entity: string;
_mouseX: number, value: string;
_mouseY: number, }>();
data?: {
[key: string]: string; const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
}, []);
const onGraphClick = useCallback(
(type: string): OnClickPluginOpts['onClick'] => (
xValue,
yValue,
mouseX,
mouseY,
data,
): Promise<void> => {
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = firstDataPoint;
setEntityData({
entity,
value,
});
return onGraphClickHandler(handleSetTimeStamp)(
xValue,
yValue,
mouseX,
mouseY,
type,
);
}, },
): void => { [handleSetTimeStamp],
const { start, end } = getStartAndEndTimesInMilliseconds(xValue); );
// Extract entity and value from data const navigateToTraces = useNavigateToTraces();
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = (firstDataPoint || ([] as unknown)) as [
string,
string,
];
onClick?.({ const goToTraces = useCallback(() => {
entity, const { start, end } = getStartAndEndTimesInMilliseconds(selectedTimeStamp);
value, const filters = createFiltersFromData({
timeRange: [start, end], [entityData?.entity as string]: entityData?.value,
widgetData: celeryTimeSeriesTablesWidgetData(entity, value, 'Task Latency'),
}); });
}; navigateToTraces(filters, start, end, true);
}, [entityData, navigateToTraces, selectedTimeStamp]);
return ( return (
<Card <Card
@ -161,32 +180,62 @@ function CeleryTaskLatencyGraph({
</Row> </Row>
<div className="celery-task-graph-grid-content"> <div className="celery-task-graph-grid-content">
{graphState === CeleryTaskGraphState.P99 && ( {graphState === CeleryTaskGraphState.P99 && (
<GridCard <>
widget={updatedWidgetData} <Button
headerMenuList={[...ViewMenuAction]} type="default"
onDragSelect={onDragSelect} size="small"
onClickHandler={onGraphClick} id="Celery_p99_latency_button"
isQueryEnabled={queryEnabled} onClick={goToTraces}
/> >
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p99_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)} )}
{graphState === CeleryTaskGraphState.P95 && ( {graphState === CeleryTaskGraphState.P95 && (
<GridCard <>
widget={updatedWidgetData} <Button
headerMenuList={[...ViewMenuAction]} type="default"
onDragSelect={onDragSelect} size="small"
onClickHandler={onGraphClick} id="Celery_p95_latency_button"
isQueryEnabled={queryEnabled} onClick={goToTraces}
/> >
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p95_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)} )}
{graphState === CeleryTaskGraphState.P90 && ( {graphState === CeleryTaskGraphState.P90 && (
<GridCard <>
widget={updatedWidgetData} <Button
headerMenuList={[...ViewMenuAction]} type="default"
onDragSelect={onDragSelect} size="small"
onClickHandler={onGraphClick} id="Celery_p90_latency_button"
isQueryEnabled={queryEnabled} onClick={goToTraces}
/> >
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p90_latency')}
isQueryEnabled={queryEnabled}
/>
</>
)} )}
</div> </div>
</Card> </Card>

View File

@ -2,8 +2,14 @@
import './CeleryTaskGraph.style.scss'; import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import { Dispatch, SetStateAction } from 'react'; import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Dispatch, SetStateAction, useMemo } from 'react';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import { import {
celeryAllStateCountWidgetData, celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData, celeryFailedStateCountWidgetData,
@ -42,16 +48,29 @@ function CeleryTaskStateGraphConfig({
setBarState(key as CeleryTaskState); setBarState(key as CeleryTaskState);
}; };
const { values, isLoading, isError } = useGetValueFromWidget( const urlQuery = useUrlQuery();
[
celeryAllStateCountWidgetData, const selectedFilters = useMemo(
celeryFailedStateCountWidgetData, () =>
celeryRetryStateCountWidgetData, getFiltersFromQueryParams(
celerySuccessStateCountWidgetData, QueryParams.taskName,
], urlQuery,
['celery-task-states'], 'celery.task_name',
),
[urlQuery],
); );
const widgetData = [
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
].map((data) => applyCeleryFilterOnWidgetData(selectedFilters || [], data));
const { values, isLoading, isError } = useGetValueFromWidget(widgetData, [
'celery-task-states',
]);
return ( return (
<Row className="celery-task-states"> <Row className="celery-task-states">
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
@ -66,7 +85,13 @@ function CeleryTaskStateGraphConfig({
<div className="celery-task-states__label-wrapper"> <div className="celery-task-states__label-wrapper">
<div className="celery-task-states__label">{tab.label}</div> <div className="celery-task-states__label">{tab.label}</div>
<div className="celery-task-states__value"> <div className="celery-task-states__value">
{isLoading ? '-' : isError ? '-' : values[index]} {isLoading
? '-'
: isError
? '-'
: Number.isNaN(values[index])
? '-'
: Math.round(Number(values[index]))}
</div> </div>
</div> </div>
{tab.key === barState && <div className="celery-task-states__indicator" />} {tab.key === barState && <div className="celery-task-states__indicator" />}

View File

@ -90,3 +90,40 @@ export const paths = (
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip); return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
}; };
export const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return (
Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
// eslint-disable-next-line sonarjs/no-identical-functions
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}))
);
};

View File

@ -12,6 +12,7 @@ export function useNavigateToTraces(): (
filters: TagFilterItem[], filters: TagFilterItem[],
startTime?: number, startTime?: number,
endTime?: number, endTime?: number,
sameTab?: boolean,
) => void { ) => void {
const { currentQuery } = useQueryBuilder(); const { currentQuery } = useQueryBuilder();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>( const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
@ -38,7 +39,12 @@ export function useNavigateToTraces(): (
); );
return useCallback( return useCallback(
(filters: TagFilterItem[], startTime?: number, endTime?: number): void => { (
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
sameTab?: boolean,
): void => {
const urlParams = new URLSearchParams(); const urlParams = new URLSearchParams();
if (startTime && endTime) { if (startTime && endTime) {
urlParams.set(QueryParams.startTime, startTime.toString()); urlParams.set(QueryParams.startTime, startTime.toString());
@ -58,7 +64,7 @@ export function useNavigateToTraces(): (
QueryParams.compositeQuery QueryParams.compositeQuery
}=${JSONCompositeQuery}`; }=${JSONCompositeQuery}`;
window.open(newTraceExplorerPath, '_blank'); window.open(newTraceExplorerPath, sameTab ? '_self' : '_blank');
}, },
[minTime, maxTime, prepareQuery], [minTime, maxTime, prepareQuery],
); );

View File

@ -48,6 +48,7 @@ function WidgetGraphComponent({
openTracesButton, openTracesButton,
onOpenTraceBtnClick, onOpenTraceBtnClick,
customSeries, customSeries,
customErrorMessage,
}: WidgetGraphComponentProps): JSX.Element { }: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@ -317,6 +318,13 @@ function WidgetGraphComponent({
setSearchTerm={setSearchTerm} setSearchTerm={setSearchTerm}
/> />
</div> </div>
{queryResponse.error && customErrorMessage && (
<div className="error-message-container">
<Typography.Text type="warning">{customErrorMessage}</Typography.Text>
</div>
)}
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && ( {queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
<Skeleton /> <Skeleton />
)} )}

View File

@ -41,9 +41,15 @@ function GridCardGraph({
openTracesButton, openTracesButton,
onOpenTraceBtnClick, onOpenTraceBtnClick,
customSeries, customSeries,
customErrorMessage,
start,
end,
}: GridCardGraphProps): JSX.Element { }: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
false,
);
const { const {
toScrollWidgetId, toScrollWidgetId,
setToScrollWidgetId, setToScrollWidgetId,
@ -178,6 +184,8 @@ function GridCardGraph({
variables: getDashboardVariables(variables), variables: getDashboardVariables(variables),
selectedTime: widget.timePreferance || 'GLOBAL_TIME', selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval, globalSelectedInterval,
start,
end,
}, },
version || DEFAULT_ENTITY_VERSION, version || DEFAULT_ENTITY_VERSION,
{ {
@ -207,6 +215,11 @@ function GridCardGraph({
refetchOnMount: false, refetchOnMount: false,
onError: (error) => { onError: (error) => {
setErrorMessage(error.message); setErrorMessage(error.message);
if (customErrorMessage) {
setIsInternalServerError(
String(error.message).includes('API responded with 500'),
);
}
setDashboardQueryRangeCalled(true); setDashboardQueryRangeCalled(true);
}, },
onSettled: (data) => { onSettled: (data) => {
@ -256,6 +269,7 @@ function GridCardGraph({
openTracesButton={openTracesButton} openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick} onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries} customSeries={customSeries}
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
/> />
)} )}
</div> </div>

View File

@ -37,6 +37,7 @@ export interface WidgetGraphComponentProps {
openTracesButton?: boolean; openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void; onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[]; customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
} }
export interface GridCardGraphProps { export interface GridCardGraphProps {
@ -54,6 +55,9 @@ export interface GridCardGraphProps {
openTracesButton?: boolean; openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void; onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[]; customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
start?: number;
end?: number;
} }
export interface GetGraphVisibilityStateOnLegendClickProps { export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -106,6 +106,16 @@
} }
} }
.error-message-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
padding-top: 0;
padding-bottom: 32px;
}
.row-settings { .row-settings {
.ant-popover-inner { .ant-popover-inner {
width: 191px; width: 191px;

View File

@ -23,7 +23,7 @@ export default function CeleryOverview(): JSX.Element {
<p className="celery-overview-content-header-title"> <p className="celery-overview-content-header-title">
Messaging Queue Overview Messaging Queue Overview
</p> </p>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal /> <DateTimeSelectionV2 showAutoRefresh hideShareModal={false} />
</div> </div>
<CeleryOverviewConfigOptions /> <CeleryOverviewConfigOptions />
<CeleryOverviewTable onRowClick={onRowClick} /> <CeleryOverviewTable onRowClick={onRowClick} />

View File

@ -1,16 +1,12 @@
import './CeleryTask.styles.scss'; import './CeleryTask.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import CeleryTaskConfigOptions from 'components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions'; import CeleryTaskConfigOptions from 'components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions';
import CeleryTaskDetail, { import CeleryTaskDetail, {
CaptureDataProps, CaptureDataProps,
} from 'components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail'; } from 'components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail';
import CeleryTaskGraphGrid from 'components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid'; import CeleryTaskGraphGrid from 'components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
export default function CeleryTask(): JSX.Element { export default function CeleryTask(): JSX.Element {
const [task, setTask] = useState<CaptureDataProps | null>(null); const [task, setTask] = useState<CaptureDataProps | null>(null);
@ -19,36 +15,13 @@ export default function CeleryTask(): JSX.Element {
setTask(captureData); setTask(captureData);
}; };
const [isURLCopied, setIsURLCopied] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
return ( return (
<div className="celery-task-container"> <div className="celery-task-container">
<div className="celery-content"> <div className="celery-content">
<div className="celery-content-header"> <div className="celery-content-header">
<p className="celery-content-header-title">Celery</p> <p className="celery-content-header-title">Celery</p>
<div className="celery-content-header-right"> <div className="celery-content-header-right">
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal /> <DateTimeSelectionV2 showAutoRefresh hideShareModal={false} />
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={(): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
}}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div> </div>
</div> </div>
<CeleryTaskGraphGrid <CeleryTaskGraphGrid