refactor + new account sync metrics + isolating health status inside folder admin-panel > health-status (#10314)

closes https://github.com/twentyhq/core-team-issues/issues/444
https://github.com/twentyhq/core-team-issues/issues/443
https://github.com/twentyhq/core-team-issues/issues/442
This commit is contained in:
nitin
2025-02-21 14:18:47 +05:30
committed by GitHub
parent 41bbb4b47f
commit c46f7848b7
57 changed files with 1441 additions and 833 deletions

View File

@ -1,44 +0,0 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
export const SettingsAdminHealthMessageSyncCountersTable = ({
details,
}: {
details: string | null | undefined;
}) => {
const parsedDetails = details ? JSON.parse(details) : null;
if (!parsedDetails) {
return null;
}
return (
<Table>
<TableRow>
<TableHeader>Status</TableHeader>
<TableHeader align="right">Count</TableHeader>
</TableRow>
<TableRow>
<TableCell>Message Not Synced</TableCell>
<TableCell align="right">{parsedDetails.counters.NOT_SYNCED}</TableCell>
</TableRow>
<TableRow>
<TableCell>Message Sync Ongoing</TableCell>
<TableCell align="right">{parsedDetails.counters.ONGOING}</TableCell>
</TableRow>
<TableRow>
<TableCell>Total Jobs</TableCell>
<TableCell align="right">{parsedDetails.totalJobs}</TableCell>
</TableRow>
<TableRow>
<TableCell>Failed Jobs</TableCell>
<TableCell align="right">{parsedDetails.failedJobs}</TableCell>
</TableRow>
<TableRow>
<TableCell>Failure Rate</TableCell>
<TableCell align="right">{parsedDetails.failureRate}%</TableCell>
</TableRow>
</Table>
);
};

View File

@ -1,66 +0,0 @@
import { SettingsAdminHealthMessageSyncCountersTable } from '@/settings/admin-panel/components/SettingsAdminHealthMessageSyncCountersTable';
import { SettingsHealthStatusListCard } from '@/settings/admin-panel/components/SettingsHealthStatusListCard';
import { AdminHealthService } from '@/settings/admin-panel/types/AdminHealthService';
import styled from '@emotion/styled';
import { H2Title, Section } from 'twenty-ui';
import {
AdminPanelHealthServiceStatus,
useGetSystemHealthStatusQuery,
} from '~/generated/graphql';
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAdminHealthStatus = () => {
const { data, loading } = useGetSystemHealthStatusQuery({
fetchPolicy: 'network-only',
});
const services = [
{
id: 'DATABASE',
name: 'Database Status',
...data?.getSystemHealthStatus.database,
},
{ id: 'REDIS', name: 'Redis Status', ...data?.getSystemHealthStatus.redis },
{
id: 'WORKER',
name: 'Worker Status',
status: data?.getSystemHealthStatus.worker.status,
queues: data?.getSystemHealthStatus.worker.queues,
},
].filter((service): service is AdminHealthService => !!service.status);
const isMessageSyncCounterDown =
!data?.getSystemHealthStatus.messageSync.status ||
data?.getSystemHealthStatus.messageSync.status ===
AdminPanelHealthServiceStatus.OUTAGE;
return (
<>
<Section>
<H2Title title="Health Status" description="How your system is doing" />
<SettingsHealthStatusListCard services={services} loading={loading} />
</Section>
<Section>
<H2Title
title="Message Sync Status"
description="How your message sync is doing"
/>
{isMessageSyncCounterDown ? (
<StyledErrorMessage>
{data?.getSystemHealthStatus.messageSync.details ||
'Message sync status is unavailable'}
</StyledErrorMessage>
) : (
<SettingsAdminHealthMessageSyncCountersTable
details={data?.getSystemHealthStatus.messageSync.details}
/>
)}
</Section>
</>
);
};

View File

@ -1,27 +0,0 @@
import { AdminHealthService } from '@/settings/admin-panel/types/AdminHealthService';
import styled from '@emotion/styled';
import { Status } from 'twenty-ui';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
const StyledRowRightContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsAdminHealthStatusRightContainer = ({
service,
}: {
service: AdminHealthService;
}) => {
return (
<StyledRowRightContainer>
{service.status === AdminPanelHealthServiceStatus.OPERATIONAL && (
<Status color="green" text="Operational" weight="medium" />
)}
{service.status === AdminPanelHealthServiceStatus.OUTAGE && (
<Status color="red" text="Outage" weight="medium" />
)}
</StyledRowRightContainer>
);
};

View File

@ -1,8 +1,8 @@
import { SettingsAdminEnvVariables } from '@/settings/admin-panel/components/SettingsAdminEnvVariables';
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/components/SettingsAdminHealthStatus';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
export const SettingsAdminTabContent = () => {

View File

@ -1,43 +0,0 @@
import { AdminHealthService } from '@/settings/admin-panel/types/AdminHealthService';
import styled from '@emotion/styled';
import { SettingsPath } from '@/types/SettingsPath';
import { Link } from 'react-router-dom';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsListCard } from '../../components/SettingsListCard';
import { SettingsAdminHealthStatusRightContainer } from './SettingsAdminHealthStatusRightContainer';
const StyledLink = styled(Link)`
text-decoration: none;
`;
export const SettingsHealthStatusListCard = ({
services,
loading,
}: {
services: Array<AdminHealthService>;
loading?: boolean;
}) => {
return (
<>
{services.map((service) => (
<>
<StyledLink
to={getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
indicatorName: service.id,
})}
>
<SettingsListCard
items={[service]}
getItemLabel={(service) => service.name}
isLoading={loading}
RowRightComponent={({ item: service }) => (
<SettingsAdminHealthStatusRightContainer service={service} />
)}
/>
</StyledLink>
</>
))}
</>
);
};

View File

@ -1,36 +0,0 @@
import { gql } from '@apollo/client';
export const GET_SYSTEM_HEALTH_STATUS = gql`
query GetSystemHealthStatus {
getSystemHealthStatus {
database {
status
details
}
redis {
status
details
}
worker {
status
queues {
name
workers
status
metrics {
failed
completed
waiting
active
delayed
prioritized
}
}
}
messageSync {
status
details
}
}
}
`;

View File

@ -0,0 +1,57 @@
import { SettingsAdminHealthAccountSyncCountersTable } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable';
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const ConnectedAccountHealthStatus = () => {
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
const details = indicatorHealth.details;
if (!details) {
return null;
}
const parsedDetails = JSON.parse(details);
const isMessageSyncDown =
parsedDetails.messageSync?.status === AdminPanelHealthServiceStatus.OUTAGE;
const isCalendarSyncDown =
parsedDetails.calendarSync?.status === AdminPanelHealthServiceStatus.OUTAGE;
const errorMessages = [];
if (isMessageSyncDown) {
errorMessages.push('Message Sync');
}
if (isCalendarSyncDown) {
errorMessages.push('Calendar Sync');
}
return (
<>
{errorMessages.length > 0 && (
<StyledErrorMessage>
{`${errorMessages.join(' and ')} ${errorMessages.length > 1 ? 'are' : 'is'} not available because the service is down`}
</StyledErrorMessage>
)}
{!isMessageSyncDown && parsedDetails.messageSync?.details && (
<SettingsAdminHealthAccountSyncCountersTable
details={parsedDetails.messageSync.details}
title="Message Sync Status"
/>
)}
{!isCalendarSyncDown && parsedDetails.calendarSync?.details && (
<SettingsAdminHealthAccountSyncCountersTable
details={parsedDetails.calendarSync.details}
title="Calendar Sync Status"
/>
)}
</>
);
};

View File

@ -0,0 +1,45 @@
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { Section } from 'twenty-ui';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
const StyledDetailsContainer = styled.pre`
background-color: ${({ theme }) => theme.background.quaternary};
padding: ${({ theme }) => theme.spacing(6)};
border-radius: ${({ theme }) => theme.border.radius.sm};
white-space: pre-wrap;
font-size: ${({ theme }) => theme.font.size.sm};
margin: 0;
`;
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const DatabaseAndRedisHealthStatus = () => {
const { indicatorHealth, loading } = useContext(
SettingsAdminIndicatorHealthContext,
);
const formattedDetails = indicatorHealth.details
? JSON.stringify(JSON.parse(indicatorHealth.details), null, 2)
: null;
const isDatabaseOrRedisDown =
!indicatorHealth.status ||
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
return (
<Section>
{isDatabaseOrRedisDown && !loading ? (
<StyledErrorMessage>
{`${indicatorHealth.label} information is not available because the service is down`}
</StyledErrorMessage>
) : (
<StyledDetailsContainer>{formattedDetails}</StyledDetailsContainer>
)}
</Section>
);
};

View File

@ -0,0 +1,55 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { H2Title } from 'twenty-ui';
const StyledContainer = styled.div``;
export const SettingsAdminHealthAccountSyncCountersTable = ({
details,
title,
}: {
details: Record<string, any> | null;
title: string;
}) => {
if (!details) {
return null;
}
return (
<StyledContainer>
<H2Title
title={title}
description={`How your ${title.toLowerCase()} is doing`}
/>
<Table>
<TableRow>
<TableHeader>Status</TableHeader>
<TableHeader align="right">Count</TableHeader>
</TableRow>
<TableRow>
<TableCell>Not Synced</TableCell>
<TableCell align="right">{details.counters.NOT_SYNCED}</TableCell>
</TableRow>
<TableRow>
<TableCell>Sync Ongoing</TableCell>
<TableCell align="right">{details.counters.ONGOING}</TableCell>
</TableRow>
<TableRow>
<TableCell>Total Jobs</TableCell>
<TableCell align="right">{details.totalJobs}</TableCell>
</TableRow>
<TableRow>
<TableCell>Failed Jobs</TableCell>
<TableCell align="right">{details.failedJobs}</TableCell>
</TableRow>
<TableRow>
<TableCell>Failure Rate</TableCell>
<TableCell align="right">{details.failureRate}%</TableCell>
</TableRow>
</Table>
</StyledContainer>
);
};

View File

@ -0,0 +1,19 @@
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({
fetchPolicy: 'network-only',
});
const services = data?.getSystemHealthStatus.services ?? [];
return (
<>
<Section>
<H2Title title="Health Status" description="How your system is doing" />
<SettingsHealthStatusListCard services={services} loading={loading} />
</Section>
</>
);
};

View File

@ -0,0 +1,19 @@
import { Status } from 'twenty-ui';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
export const SettingsAdminHealthStatusRightContainer = ({
status,
}: {
status: AdminPanelHealthServiceStatus;
}) => {
return (
<>
{status === AdminPanelHealthServiceStatus.OPERATIONAL && (
<Status color="green" text="Operational" weight="medium" />
)}
{status === AdminPanelHealthServiceStatus.OUTAGE && (
<Status color="red" text="Outage" weight="medium" />
)}
</>
);
};

View File

@ -0,0 +1,21 @@
import { ConnectedAccountHealthStatus } from '@/settings/admin-panel/health-status/components/ConnectedAccountHealthStatus';
import { DatabaseAndRedisHealthStatus } from '@/settings/admin-panel/health-status/components/DatabaseAndRedisHealthStatus';
import { WorkerHealthStatus } from '@/settings/admin-panel/health-status/components/WorkerHealthStatus';
import { useParams } from 'react-router-dom';
import { HealthIndicatorId } from '~/generated/graphql';
export const SettingsAdminIndicatorHealthStatusContent = () => {
const { indicatorId } = useParams();
switch (indicatorId) {
case HealthIndicatorId.database:
case HealthIndicatorId.redis:
return <DatabaseAndRedisHealthStatus />;
case HealthIndicatorId.worker:
return <WorkerHealthStatus />;
case HealthIndicatorId.connectedAccount:
return <ConnectedAccountHealthStatus />;
default:
return null;
}
};

View File

@ -43,7 +43,7 @@ export const SettingsAdminQueueExpandableContainer = ({
selectedQueue: string | null;
}) => {
const selectedQueueData = queues.find(
(queue) => queue.name === selectedQueue,
(queue) => queue.queueName === selectedQueue,
);
return (
@ -55,10 +55,12 @@ export const SettingsAdminQueueExpandableContainer = ({
<>
<StyledContainer>
<SettingsListCard
items={[{ ...selectedQueueData, id: selectedQueueData.name }]}
items={[
{ ...selectedQueueData, id: selectedQueueData.queueName },
]}
getItemLabel={(
item: AdminPanelWorkerQueueHealth & { id: string },
) => item.name}
) => item.queueName}
isLoading={false}
RowRightComponent={({
item,

View File

@ -39,11 +39,11 @@ export const SettingsAdminQueueHealthButtons = ({
<StyledQueueButtonsRow>
{queues.map((queue) => (
<StyledQueueHealthButton
key={queue.name}
onClick={() => toggleQueueVisibility(queue.name)}
title={queue.name}
key={queue.queueName}
onClick={() => toggleQueueVisibility(queue.queueName)}
title={queue.queueName}
variant="secondary"
isSelected={selectedQueue === queue.name}
isSelected={selectedQueue === queue.queueName}
status={queue.status}
/>
))}

View File

@ -0,0 +1,29 @@
import { SettingsListCard } from '@/settings/components/SettingsListCard';
import { SettingsPath } from '@/types/SettingsPath';
import { SystemHealthService } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsAdminHealthStatusRightContainer } from './SettingsAdminHealthStatusRightContainer';
export const SettingsHealthStatusListCard = ({
services,
loading,
}: {
services: Array<SystemHealthService>;
loading?: boolean;
}) => {
return (
<SettingsListCard
items={services}
getItemLabel={(service) => service.label}
isLoading={loading}
RowRightComponent={({ item: service }) => (
<SettingsAdminHealthStatusRightContainer status={service.status} />
)}
to={(service) =>
getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
indicatorId: service.id,
})
}
/>
);
};

View File

@ -0,0 +1,62 @@
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 styled from '@emotion/styled';
import { useContext, useState } from 'react';
import { H2Title, Section } from 'twenty-ui';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
const StyledTitleContainer = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`;
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const WorkerHealthStatus = () => {
const { indicatorHealth, loading } = 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 ? (
<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}
/>
</>
)}
</Section>
);
};

View File

@ -0,0 +1,23 @@
import { createContext } from 'react';
import {
AdminPanelHealthServiceData,
AdminPanelHealthServiceStatus,
} from '~/generated/graphql';
type SettingsAdminIndicatorHealthContextType = {
indicatorHealth: AdminPanelHealthServiceData;
loading: boolean;
};
export const SettingsAdminIndicatorHealthContext =
createContext<SettingsAdminIndicatorHealthContextType>({
indicatorHealth: {
id: '',
label: '',
description: '',
status: AdminPanelHealthServiceStatus.OPERATIONAL,
details: '',
queues: [],
},
loading: false,
});

View File

@ -1,14 +1,16 @@
import { gql } from '@apollo/client';
export const GET_INDICATOR_HEALTH_STATUS = gql`
query GetIndicatorHealthStatus(
$indicatorName: AdminPanelIndicatorHealthStatusInputEnum!
) {
getIndicatorHealthStatus(indicatorName: $indicatorName) {
query GetIndicatorHealthStatus($indicatorId: HealthIndicatorId!) {
getIndicatorHealthStatus(indicatorId: $indicatorId) {
id
label
description
status
details
queues {
name
id
queueName
status
workers
metrics {

View File

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const GET_SYSTEM_HEALTH_STATUS = gql`
query GetSystemHealthStatus {
getSystemHealthStatus {
services {
id
label
status
}
}
}
`;

View File

@ -1,12 +0,0 @@
import { useGetSystemHealthStatusQuery } from '~/generated/graphql';
export const useGetUptoDateHealthStatus = () => {
const { data, loading } = useGetSystemHealthStatusQuery({
fetchPolicy: 'network-only',
});
return {
healthStatus: data?.getSystemHealthStatus,
healthStatusLoading: loading,
};
};

View File

@ -1,12 +0,0 @@
import {
AdminPanelHealthServiceData,
AdminPanelWorkerQueueHealth,
} from '~/generated/graphql';
type AdminWorkerService = AdminPanelHealthServiceData & {
id: string;
name: string;
queues: AdminPanelWorkerQueueHealth[] | null | undefined;
};
export type AdminHealthService = AdminWorkerService;