Health status worker metrics improvements (#10442)
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
|
||||
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { Button, H1Title, H1TitleFontColor, Section } from 'twenty-ui';
|
||||
@ -37,11 +38,10 @@ const StyledShowMoreButton = styled(Button)<{ isSelected?: boolean }>`
|
||||
`;
|
||||
|
||||
export const SettingsAdminEnvVariables = () => {
|
||||
const { data: environmentVariables } = useGetEnvironmentVariablesGroupedQuery(
|
||||
{
|
||||
const { data: environmentVariables, loading: environmentVariablesLoading } =
|
||||
useGetEnvironmentVariablesGroupedQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
|
||||
|
||||
@ -64,6 +64,10 @@ export const SettingsAdminEnvVariables = () => {
|
||||
(group) => group.name === selectedGroup,
|
||||
);
|
||||
|
||||
if (environmentVariablesLoading) {
|
||||
return <SettingsAdminTabSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
|
||||
export const SettingsAdminTabSkeletonLoader = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} width={120} />
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
@ -19,9 +19,7 @@ const StyledErrorMessage = styled.div`
|
||||
`;
|
||||
|
||||
export const DatabaseAndRedisHealthStatus = () => {
|
||||
const { indicatorHealth, loading } = useContext(
|
||||
SettingsAdminIndicatorHealthContext,
|
||||
);
|
||||
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
|
||||
|
||||
const formattedDetails = indicatorHealth.details
|
||||
? JSON.stringify(JSON.parse(indicatorHealth.details), null, 2)
|
||||
@ -33,7 +31,7 @@ export const DatabaseAndRedisHealthStatus = () => {
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{isDatabaseOrRedisDown && !loading ? (
|
||||
{isDatabaseOrRedisDown ? (
|
||||
<StyledErrorMessage>
|
||||
{`${indicatorHealth.label} information is not available because the service is down`}
|
||||
</StyledErrorMessage>
|
||||
|
||||
@ -1,18 +1,27 @@
|
||||
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
|
||||
import { SettingsHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsHealthStatusListCard';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { useGetSystemHealthStatusQuery } from '~/generated/graphql';
|
||||
|
||||
export const SettingsAdminHealthStatus = () => {
|
||||
const { data, loading } = useGetSystemHealthStatusQuery({
|
||||
const { data, loading: loadingHealthStatus } = useGetSystemHealthStatusQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const services = data?.getSystemHealthStatus.services ?? [];
|
||||
|
||||
if (loadingHealthStatus) {
|
||||
return <SettingsAdminTabSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title="Health Status" description="How your system is doing" />
|
||||
<SettingsHealthStatusListCard services={services} loading={loading} />
|
||||
<SettingsHealthStatusListCard
|
||||
services={services}
|
||||
loading={loadingHealthStatus}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatedExpandableContainer, Status } from 'twenty-ui';
|
||||
import {
|
||||
AdminPanelHealthServiceStatus,
|
||||
AdminPanelWorkerQueueHealth,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const StyledExpandedContent = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
margin-top: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledQueueMetricsTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const SettingsAdminQueueExpandableContainer = ({
|
||||
queues,
|
||||
selectedQueue,
|
||||
}: {
|
||||
queues: AdminPanelWorkerQueueHealth[];
|
||||
selectedQueue: string | null;
|
||||
}) => {
|
||||
const selectedQueueData = queues.find(
|
||||
(queue) => queue.queueName === selectedQueue,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={!!selectedQueue}
|
||||
mode="fit-content"
|
||||
>
|
||||
{selectedQueueData && (
|
||||
<>
|
||||
<StyledContainer>
|
||||
<SettingsListCard
|
||||
items={[
|
||||
{ ...selectedQueueData, id: selectedQueueData.queueName },
|
||||
]}
|
||||
getItemLabel={(
|
||||
item: AdminPanelWorkerQueueHealth & { id: string },
|
||||
) => item.queueName}
|
||||
isLoading={false}
|
||||
RowRightComponent={({
|
||||
item,
|
||||
}: {
|
||||
item: AdminPanelWorkerQueueHealth;
|
||||
}) => (
|
||||
<Status
|
||||
color={
|
||||
item.status === AdminPanelHealthServiceStatus.OPERATIONAL
|
||||
? 'green'
|
||||
: 'red'
|
||||
}
|
||||
text={item.status.toLowerCase()}
|
||||
weight="medium"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<StyledQueueMetricsTitle> Metrics:</StyledQueueMetricsTitle>
|
||||
<StyledExpandedContent>
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableCell align="left">Workers</TableCell>
|
||||
<TableCell align="right">{selectedQueueData.workers}</TableCell>
|
||||
</StyledTableRow>
|
||||
{Object.entries(selectedQueueData.metrics)
|
||||
.filter(([key]) => key !== '__typename')
|
||||
.map(([key, value]) => (
|
||||
<StyledTableRow key={key}>
|
||||
<TableCell align="left">
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</TableCell>
|
||||
<TableCell align="right">{value}</TableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</Table>
|
||||
</StyledExpandedContent>
|
||||
</>
|
||||
)}
|
||||
</AnimatedExpandableContainer>
|
||||
);
|
||||
};
|
||||
@ -1,52 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Button } from 'twenty-ui';
|
||||
import {
|
||||
AdminPanelHealthServiceStatus,
|
||||
AdminPanelWorkerQueueHealth,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const StyledQueueButtonsRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-top: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledQueueHealthButton = styled(Button)<{
|
||||
isSelected?: boolean;
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
}>`
|
||||
${({ isSelected, theme, status }) =>
|
||||
isSelected &&
|
||||
`
|
||||
background-color: ${
|
||||
status === AdminPanelHealthServiceStatus.OPERATIONAL
|
||||
? theme.tag.background.green
|
||||
: theme.tag.background.red
|
||||
};
|
||||
`}
|
||||
`;
|
||||
export const SettingsAdminQueueHealthButtons = ({
|
||||
queues,
|
||||
selectedQueue,
|
||||
toggleQueueVisibility,
|
||||
}: {
|
||||
queues: AdminPanelWorkerQueueHealth[];
|
||||
selectedQueue: string | null;
|
||||
toggleQueueVisibility: (queueName: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<StyledQueueButtonsRow>
|
||||
{queues.map((queue) => (
|
||||
<StyledQueueHealthButton
|
||||
key={queue.queueName}
|
||||
onClick={() => toggleQueueVisibility(queue.queueName)}
|
||||
title={queue.queueName}
|
||||
variant="secondary"
|
||||
isSelected={selectedQueue === queue.queueName}
|
||||
status={queue.status}
|
||||
/>
|
||||
))}
|
||||
</StyledQueueButtonsRow>
|
||||
);
|
||||
};
|
||||
@ -1,16 +1,8 @@
|
||||
import { SettingsAdminQueueExpandableContainer } from '@/settings/admin-panel/health-status/components/SettingsAdminQueueExpandableContainer';
|
||||
import { SettingsAdminQueueHealthButtons } from '@/settings/admin-panel/health-status/components/SettingsAdminQueueHealthButtons';
|
||||
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
|
||||
import { WorkerQueueMetricsSection } from '@/settings/admin-panel/health-status/components/WorkerQueueMetricsSection';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext, useState } from 'react';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { useContext } from 'react';
|
||||
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
import { SettingsAdminIndicatorHealthContext } from '../contexts/SettingsAdminIndicatorHealthContext';
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
@ -18,45 +10,23 @@ const StyledErrorMessage = styled.div`
|
||||
`;
|
||||
|
||||
export const WorkerHealthStatus = () => {
|
||||
const { indicatorHealth, loading } = useContext(
|
||||
SettingsAdminIndicatorHealthContext,
|
||||
);
|
||||
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
|
||||
|
||||
const isWorkerDown =
|
||||
!indicatorHealth.status ||
|
||||
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
|
||||
|
||||
const [selectedQueue, setSelectedQueue] = useState<string | null>(null);
|
||||
|
||||
const toggleQueueVisibility = (queueName: string) => {
|
||||
setSelectedQueue(selectedQueue === queueName ? null : queueName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledTitleContainer>
|
||||
<H2Title
|
||||
title="Queue Status"
|
||||
description="Background job processing status and metrics"
|
||||
/>
|
||||
</StyledTitleContainer>
|
||||
{isWorkerDown && !loading ? (
|
||||
<>
|
||||
{isWorkerDown ? (
|
||||
<StyledErrorMessage>
|
||||
Queue information is not available because the worker is down
|
||||
</StyledErrorMessage>
|
||||
) : (
|
||||
<>
|
||||
<SettingsAdminQueueHealthButtons
|
||||
queues={indicatorHealth.queues ?? []}
|
||||
selectedQueue={selectedQueue}
|
||||
toggleQueueVisibility={toggleQueueVisibility}
|
||||
/>
|
||||
<SettingsAdminQueueExpandableContainer
|
||||
queues={indicatorHealth.queues ?? []}
|
||||
selectedQueue={selectedQueue}
|
||||
/>
|
||||
</>
|
||||
(indicatorHealth.queues ?? []).map((queue) => (
|
||||
<WorkerQueueMetricsSection key={queue.queueName} queue={queue} />
|
||||
))
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,315 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import {
|
||||
QueueMetricsTimeRange,
|
||||
useGetQueueMetricsQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledQueueMetricsTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledGraphContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
height: 230px;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
padding-top: ${({ theme }) => theme.spacing(4)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledQueueMetricsContainer = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledGraphControls = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: flex-end;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledNoDataMessage = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledTooltipContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
`;
|
||||
|
||||
const StyledTooltipItem = styled.div<{ color: string }>`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
padding: ${({ theme }) => theme.spacing(0.5)} 0;
|
||||
`;
|
||||
|
||||
const StyledTooltipColorSquare = styled.div<{ color: string }>`
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: ${({ color }) => color};
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledTooltipValue = styled.span`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
type WorkerMetricsGraphProps = {
|
||||
queueName: string;
|
||||
timeRange: QueueMetricsTimeRange;
|
||||
onTimeRangeChange: (range: QueueMetricsTimeRange) => void;
|
||||
};
|
||||
|
||||
export const WorkerMetricsGraph = ({
|
||||
queueName,
|
||||
timeRange,
|
||||
onTimeRangeChange,
|
||||
}: WorkerMetricsGraphProps) => {
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const { loading, data } = useGetQueueMetricsQuery({
|
||||
variables: {
|
||||
queueName,
|
||||
timeRange,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
onError: (error) => {
|
||||
enqueueSnackBar(`Error fetching worker metrics: ${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const metricsData = data?.getQueueMetrics?.data || [];
|
||||
const hasData =
|
||||
metricsData.length > 0 &&
|
||||
metricsData.some((series) => series.data.length > 0);
|
||||
|
||||
const metricsDetails = {
|
||||
workers: data?.getQueueMetrics?.workers,
|
||||
...data?.getQueueMetrics?.details,
|
||||
};
|
||||
|
||||
const getMaxYValue = () => {
|
||||
if (!hasData) return 2;
|
||||
|
||||
let maxValue = 0;
|
||||
metricsData.forEach((series) => {
|
||||
series.data.forEach((point) => {
|
||||
if (typeof point.y === 'number' && point.y > maxValue) {
|
||||
maxValue = point.y;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return maxValue === 0 ? 2 : maxValue * 1.1;
|
||||
};
|
||||
|
||||
const getAxisLabel = () => {
|
||||
switch (timeRange) {
|
||||
case QueueMetricsTimeRange.OneHour:
|
||||
return 'Last 1 Hour (oldest → newest)';
|
||||
case QueueMetricsTimeRange.FourHours:
|
||||
return 'Last 4 Hours (oldest → newest)';
|
||||
case QueueMetricsTimeRange.TwelveHours:
|
||||
return 'Last 12 Hours (oldest → newest)';
|
||||
case QueueMetricsTimeRange.OneDay:
|
||||
return 'Last 24 Hours (oldest → newest)';
|
||||
case QueueMetricsTimeRange.SevenDays:
|
||||
return 'Last 7 Days (oldest → newest)';
|
||||
default:
|
||||
return 'Recent Events (oldest → newest)';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledGraphControls>
|
||||
<Select
|
||||
dropdownId={`timerange-${queueName}`}
|
||||
value={timeRange}
|
||||
options={[
|
||||
{ value: QueueMetricsTimeRange.SevenDays, label: 'This week' },
|
||||
{ value: QueueMetricsTimeRange.OneDay, label: 'Today' },
|
||||
{
|
||||
value: QueueMetricsTimeRange.TwelveHours,
|
||||
label: 'Last 12 hours',
|
||||
},
|
||||
{ value: QueueMetricsTimeRange.FourHours, label: 'Last 4 hours' },
|
||||
{ value: QueueMetricsTimeRange.OneHour, label: 'Last 1 hour' },
|
||||
]}
|
||||
onChange={onTimeRangeChange}
|
||||
needIconCheck
|
||||
/>
|
||||
</StyledGraphControls>
|
||||
|
||||
<StyledGraphContainer>
|
||||
{loading ? (
|
||||
<StyledNoDataMessage>Loading metrics data...</StyledNoDataMessage>
|
||||
) : hasData ? (
|
||||
<ResponsiveLine
|
||||
data={metricsData}
|
||||
curve="monotoneX"
|
||||
enableArea={true}
|
||||
colors={[theme.color.green, theme.color.red]}
|
||||
theme={{
|
||||
text: {
|
||||
fill: theme.font.color.light,
|
||||
fontSize: theme.font.size.sm,
|
||||
fontFamily: theme.font.family,
|
||||
},
|
||||
axis: {
|
||||
domain: {
|
||||
line: {
|
||||
stroke: theme.border.color.strong,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
line: {
|
||||
stroke: theme.border.color.strong,
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
line: {
|
||||
stroke: theme.border.color.medium,
|
||||
},
|
||||
},
|
||||
crosshair: {
|
||||
line: {
|
||||
stroke: theme.font.color.primary,
|
||||
strokeDasharray: '2 2',
|
||||
},
|
||||
},
|
||||
}}
|
||||
margin={{ top: 40, right: 30, bottom: 40, left: 50 }}
|
||||
xScale={{
|
||||
type: 'linear',
|
||||
min: 0,
|
||||
max: 'auto',
|
||||
}}
|
||||
yScale={{
|
||||
type: 'linear',
|
||||
min: 0,
|
||||
max: getMaxYValue(),
|
||||
stacked: false,
|
||||
}}
|
||||
axisBottom={{
|
||||
legend: getAxisLabel(),
|
||||
legendOffset: 30,
|
||||
legendPosition: 'middle',
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickValues: 5,
|
||||
format: () => '',
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 6,
|
||||
tickPadding: 5,
|
||||
tickValues: 4,
|
||||
legend: 'Count',
|
||||
legendOffset: -40,
|
||||
legendPosition: 'middle',
|
||||
}}
|
||||
enableGridX={false}
|
||||
gridYValues={4}
|
||||
pointSize={0}
|
||||
enableSlices="x"
|
||||
sliceTooltip={({ slice }) => (
|
||||
<StyledTooltipContainer>
|
||||
{slice.points.map((point) => (
|
||||
<StyledTooltipItem key={point.id} color={point.serieColor}>
|
||||
<StyledTooltipColorSquare color={point.serieColor} />
|
||||
<span>
|
||||
{point.serieId}:{' '}
|
||||
<StyledTooltipValue>
|
||||
{String(point.data.y)}
|
||||
</StyledTooltipValue>
|
||||
</span>
|
||||
</StyledTooltipItem>
|
||||
))}
|
||||
</StyledTooltipContainer>
|
||||
)}
|
||||
useMesh={true}
|
||||
legends={[
|
||||
{
|
||||
anchor: 'top-right',
|
||||
direction: 'row',
|
||||
justify: false,
|
||||
translateX: 0,
|
||||
translateY: -40,
|
||||
itemsSpacing: 10,
|
||||
itemDirection: 'left-to-right',
|
||||
itemWidth: 100,
|
||||
itemHeight: 20,
|
||||
symbolSize: 12,
|
||||
symbolShape: 'square',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<StyledNoDataMessage>No metrics data available</StyledNoDataMessage>
|
||||
)}
|
||||
</StyledGraphContainer>
|
||||
{metricsDetails && (
|
||||
<>
|
||||
<StyledQueueMetricsTitle>Metrics:</StyledQueueMetricsTitle>
|
||||
<StyledQueueMetricsContainer>
|
||||
<Table>
|
||||
{Object.entries(metricsDetails)
|
||||
.filter(([key]) => key !== '__typename')
|
||||
.map(([key, value]) => (
|
||||
<StyledTableRow key={key}>
|
||||
<TableCell align="left">
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{typeof value === 'number'
|
||||
? value
|
||||
: Array.isArray(value)
|
||||
? value.length
|
||||
: String(value)}
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</Table>
|
||||
</StyledQueueMetricsContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import {
|
||||
AdminPanelWorkerQueueHealth,
|
||||
QueueMetricsTimeRange,
|
||||
} from '~/generated/graphql';
|
||||
import { WorkerMetricsGraph } from './WorkerMetricsGraph';
|
||||
|
||||
type WorkerQueueMetricsSectionProps = {
|
||||
queue: AdminPanelWorkerQueueHealth;
|
||||
};
|
||||
|
||||
export const WorkerQueueMetricsSection = ({
|
||||
queue,
|
||||
}: WorkerQueueMetricsSectionProps) => {
|
||||
const [timeRange, setTimeRange] = useState(QueueMetricsTimeRange.OneHour);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title={queue.queueName} description="Queue performance" />
|
||||
<WorkerMetricsGraph
|
||||
queueName={queue.queueName}
|
||||
timeRange={timeRange}
|
||||
onTimeRangeChange={setTimeRange}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -6,7 +6,6 @@ import {
|
||||
|
||||
type SettingsAdminIndicatorHealthContextType = {
|
||||
indicatorHealth: AdminPanelHealthServiceData;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const SettingsAdminIndicatorHealthContext =
|
||||
@ -19,5 +18,4 @@ export const SettingsAdminIndicatorHealthContext =
|
||||
details: '',
|
||||
queues: [],
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
|
||||
@ -12,15 +12,6 @@ export const GET_INDICATOR_HEALTH_STATUS = gql`
|
||||
id
|
||||
queueName
|
||||
status
|
||||
workers
|
||||
metrics {
|
||||
failed
|
||||
completed
|
||||
waiting
|
||||
active
|
||||
delayed
|
||||
prioritized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_QUEUE_METRICS = gql`
|
||||
query GetQueueMetrics(
|
||||
$queueName: String!
|
||||
$timeRange: QueueMetricsTimeRange
|
||||
) {
|
||||
getQueueMetrics(queueName: $queueName, timeRange: $timeRange) {
|
||||
queueName
|
||||
timeRange
|
||||
workers
|
||||
details {
|
||||
failed
|
||||
completed
|
||||
waiting
|
||||
active
|
||||
delayed
|
||||
failureRate
|
||||
}
|
||||
data {
|
||||
id
|
||||
data {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user