diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ac5865902..52a4a5993 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -43,10 +43,8 @@ export enum AdminPanelHealthServiceStatus { export type AdminPanelWorkerQueueHealth = { __typename?: 'AdminPanelWorkerQueueHealth'; id: Scalars['String']; - metrics: WorkerQueueMetrics; queueName: Scalars['String']; status: AdminPanelHealthServiceStatus; - workers: Scalars['Float']; }; export type Analytics = { @@ -1310,6 +1308,7 @@ export type Query = { getPostgresCredentials?: Maybe; getProductPrices: BillingProductPricesOutput; getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput; + getQueueMetrics: QueueMetricsData; getRoles: Array; getSSOIdentityProviders: Array; getServerlessFunctionSourceCode?: Maybe; @@ -1369,6 +1368,12 @@ export type QueryGetProductPricesArgs = { }; +export type QueryGetQueueMetricsArgs = { + queueName: Scalars['String']; + timeRange?: InputMaybe; +}; + + export type QueryGetServerlessFunctionSourceCodeArgs = { input: GetServerlessFunctionSourceCodeInput; }; @@ -1413,6 +1418,35 @@ export type QueryValidatePasswordResetTokenArgs = { passwordResetToken: Scalars['String']; }; +export type QueueMetricsData = { + __typename?: 'QueueMetricsData'; + data: Array; + details?: Maybe; + queueName: Scalars['String']; + timeRange: QueueMetricsTimeRange; + workers: Scalars['Float']; +}; + +export type QueueMetricsDataPoint = { + __typename?: 'QueueMetricsDataPoint'; + x: Scalars['Float']; + y: Scalars['Float']; +}; + +export type QueueMetricsSeries = { + __typename?: 'QueueMetricsSeries'; + data: Array; + id: Scalars['String']; +}; + +export enum QueueMetricsTimeRange { + FourHours = 'FourHours', + OneDay = 'OneDay', + OneHour = 'OneHour', + SevenDays = 'SevenDays', + TwelveHours = 'TwelveHours' +} + export type Relation = { __typename?: 'Relation'; sourceFieldMetadata: Field; @@ -1970,9 +2004,11 @@ export type WorkerQueueMetrics = { __typename?: 'WorkerQueueMetrics'; active: Scalars['Float']; completed: Scalars['Float']; + completedData?: Maybe>; delayed: Scalars['Float']; failed: Scalars['Float']; - prioritized: Scalars['Float']; + failedData?: Maybe>; + failureRate: Scalars['Float']; waiting: Scalars['Float']; }; @@ -2394,7 +2430,15 @@ export type GetIndicatorHealthStatusQueryVariables = Exact<{ }>; -export type GetIndicatorHealthStatusQuery = { __typename?: 'Query', getIndicatorHealthStatus: { __typename?: 'AdminPanelHealthServiceData', id: string, label: string, description: string, status: AdminPanelHealthServiceStatus, details?: string | null, queues?: Array<{ __typename?: 'AdminPanelWorkerQueueHealth', id: string, queueName: string, status: AdminPanelHealthServiceStatus, workers: number, metrics: { __typename?: 'WorkerQueueMetrics', failed: number, completed: number, waiting: number, active: number, delayed: number, prioritized: number } }> | null } }; +export type GetIndicatorHealthStatusQuery = { __typename?: 'Query', getIndicatorHealthStatus: { __typename?: 'AdminPanelHealthServiceData', id: string, label: string, description: string, status: AdminPanelHealthServiceStatus, details?: string | null, queues?: Array<{ __typename?: 'AdminPanelWorkerQueueHealth', id: string, queueName: string, status: AdminPanelHealthServiceStatus }> | null } }; + +export type GetQueueMetricsQueryVariables = Exact<{ + queueName: Scalars['String']; + timeRange?: InputMaybe; +}>; + + +export type GetQueueMetricsQuery = { __typename?: 'Query', getQueueMetrics: { __typename?: 'QueueMetricsData', queueName: string, timeRange: QueueMetricsTimeRange, workers: number, details?: { __typename?: 'WorkerQueueMetrics', failed: number, completed: number, waiting: number, active: number, delayed: number, failureRate: number } | null, data: Array<{ __typename?: 'QueueMetricsSeries', id: string, data: Array<{ __typename?: 'QueueMetricsDataPoint', x: number, y: number }> }> } }; export type GetSystemHealthStatusQueryVariables = Exact<{ [key: string]: never; }>; @@ -4238,15 +4282,6 @@ export const GetIndicatorHealthStatusDocument = gql` id queueName status - workers - metrics { - failed - completed - waiting - active - delayed - prioritized - } } } } @@ -4279,6 +4314,59 @@ export function useGetIndicatorHealthStatusLazyQuery(baseOptions?: Apollo.LazyQu export type GetIndicatorHealthStatusQueryHookResult = ReturnType; export type GetIndicatorHealthStatusLazyQueryHookResult = ReturnType; export type GetIndicatorHealthStatusQueryResult = Apollo.QueryResult; +export const GetQueueMetricsDocument = 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 + } + } + } +} + `; + +/** + * __useGetQueueMetricsQuery__ + * + * To run a query within a React component, call `useGetQueueMetricsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetQueueMetricsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetQueueMetricsQuery({ + * variables: { + * queueName: // value for 'queueName' + * timeRange: // value for 'timeRange' + * }, + * }); + */ +export function useGetQueueMetricsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetQueueMetricsDocument, options); + } +export function useGetQueueMetricsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetQueueMetricsDocument, options); + } +export type GetQueueMetricsQueryHookResult = ReturnType; +export type GetQueueMetricsLazyQueryHookResult = ReturnType; +export type GetQueueMetricsQueryResult = Apollo.QueryResult; export const GetSystemHealthStatusDocument = gql` query GetSystemHealthStatus { getSystemHealthStatus { diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx index 65eee9f7b..2e7d44851 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx @@ -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(null); @@ -64,6 +64,10 @@ export const SettingsAdminEnvVariables = () => { (group) => group.name === selectedGroup, ); + if (environmentVariablesLoading) { + return ; + } + return ( <>
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabSkeletonLoader.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabSkeletonLoader.tsx new file mode 100644 index 000000000..0ee92eb67 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabSkeletonLoader.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/DatabaseAndRedisHealthStatus.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/DatabaseAndRedisHealthStatus.tsx index 802dce751..1e3ca43d4 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/DatabaseAndRedisHealthStatus.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/DatabaseAndRedisHealthStatus.tsx @@ -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 (
- {isDatabaseOrRedisDown && !loading ? ( + {isDatabaseOrRedisDown ? ( {`${indicatorHealth.label} information is not available because the service is down`} diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx index 14db3d3bd..e7a7c6580 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx @@ -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 ; + } + return ( <>
- +
); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueExpandableContainer.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueExpandableContainer.tsx deleted file mode 100644 index de4721ebc..000000000 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueExpandableContainer.tsx +++ /dev/null @@ -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 ( - - {selectedQueueData && ( - <> - - item.queueName} - isLoading={false} - RowRightComponent={({ - item, - }: { - item: AdminPanelWorkerQueueHealth; - }) => ( - - )} - /> - - Metrics: - - - - Workers - {selectedQueueData.workers} - - {Object.entries(selectedQueueData.metrics) - .filter(([key]) => key !== '__typename') - .map(([key, value]) => ( - - - {key.charAt(0).toUpperCase() + key.slice(1)} - - {value} - - ))} -
-
- - )} -
- ); -}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueHealthButtons.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueHealthButtons.tsx deleted file mode 100644 index 59d2f5b3f..000000000 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminQueueHealthButtons.tsx +++ /dev/null @@ -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 ( - - {queues.map((queue) => ( - toggleQueueVisibility(queue.queueName)} - title={queue.queueName} - variant="secondary" - isSelected={selectedQueue === queue.queueName} - status={queue.status} - /> - ))} - - ); -}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx index dc8969381..7e1224af5 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx @@ -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(null); - - const toggleQueueVisibility = (queueName: string) => { - setSelectedQueue(selectedQueue === queueName ? null : queueName); - }; - return ( -
- - - - {isWorkerDown && !loading ? ( + <> + {isWorkerDown ? ( Queue information is not available because the worker is down ) : ( - <> - - - + (indicatorHealth.queues ?? []).map((queue) => ( + + )) )} -
+ ); }; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx new file mode 100644 index 000000000..d505a39d0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx @@ -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 ( + <> + +