Admin panel: App health check (#10546)

closes https://github.com/twentyhq/core-team-issues/issues/441
whats new - 
- app health with the proposed format -- update, scratched this format
in favor of a more concise and more intentional health check. Now we
will check app health only based on pendingMigrations
<del>
```
status: 
  system: {
    nodeVersion
  },
  overview: {
    totalWorkspaces,
    criticalWorkspaces,
    workspacesWithPendingMigrations,
    healthDistribution: {
      healthy,
      warning,
      critical,
    }
  },
 problematicWorkspaces: [
    {
      workspaceId,
      severity,
      pendingMigrations,
      issuesSummary: {
        structural,
        data,
        relationship
      }
    }
  ]
  ```
</del>

- errorMessage and details seperation -- before we used to send error in details which made it difficult if we want both error and details on the front -- usecase >>
suppose app health indicator is not healthy but still want to send details
- stateHistory with timestamp -- this is something I introduced, not sure about this. Basically the thought process was to store the LastState of the details, incase of no connection or timeout errors. This is not yet used on the front, just the endpoint. But it could be used on the front too
- name unifying ⁠https://discord.com/channels/1130383047699738754/1346454192776155156
- json tree https://discord.com/channels/1130383047699738754/1346458558048501760
- match figma design https://discord.com/channels/1130383047699738754/1346451659647094815
- fix the collapse/open styles in tables https://discord.com/channels/1130383047699738754/1346452051974160406
- shift eye icon to expanded container https://discord.com/channels/1130383047699738754/1346452282669010987
- use H2Title for title and description of env variables groups https://discord.com/channels/1130383047699738754/1346434955936530452
This commit is contained in:
nitin
2025-03-07 01:49:41 +05:30
committed by GitHub
parent f6314e52fe
commit 3061576302
34 changed files with 808 additions and 244 deletions

View File

@ -450,13 +450,13 @@ export const SettingsRoutes = ({
{isAdminPageEnabled && (
<>
<Route path={SettingsPath.AdminPanel} element={<SettingsAdmin />} />
<Route path={SettingsPath.ServerAdmin} element={<SettingsAdmin />} />
<Route
path={SettingsPath.FeatureFlags}
element={<SettingsAdminContent />}
/>
<Route
path={SettingsPath.AdminPanelIndicatorHealthStatus}
path={SettingsPath.ServerAdminIndicatorHealthStatus}
element={<SettingsAdminIndicatorHealthStatus />}
/>
</>

View File

@ -3,8 +3,8 @@ import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAd
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { TabList } from '@/ui/layout/tab/components/TabList';
import styled from '@emotion/styled';
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui';
import { t } from '@lingui/core/macro';
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui';
const StyledTabListContainer = styled.div`
align-items: center;

View File

@ -2,22 +2,13 @@ import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/component
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';
import { Button, H1Title, H1TitleFontColor, H2Title, Section } from 'twenty-ui';
import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
const StyledGroupContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(6)};
`;
const StyledGroupVariablesContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
border: 1px solid ${({ theme }) => theme.border.color.medium};
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(4)};
padding-right: ${({ theme }) => theme.spacing(4)};
`;
const StyledGroupDescription = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
@ -78,16 +69,9 @@ export const SettingsAdminEnvVariables = () => {
<Section>
{visibleGroups.map((group) => (
<StyledGroupContainer key={group.name}>
<H1Title title={group.name} fontColor={H1TitleFontColor.Primary} />
{group.description !== '' && (
<StyledGroupDescription>
{group.description}
</StyledGroupDescription>
)}
<H2Title title={group.name} description={group.description} />
{group.variables.length > 0 && (
<StyledGroupVariablesContainer>
<SettingsAdminEnvVariablesTable variables={group.variables} />
</StyledGroupVariablesContainer>
<SettingsAdminEnvVariablesTable variables={group.variables} />
)}
</StyledGroupContainer>
))}
@ -120,11 +104,9 @@ export const SettingsAdminEnvVariables = () => {
</StyledGroupDescription>
)}
{selectedGroupData.variables.length > 0 && (
<StyledGroupVariablesContainer>
<SettingsAdminEnvVariablesTable
variables={selectedGroupData.variables}
/>
</StyledGroupVariablesContainer>
<SettingsAdminEnvVariablesTable
variables={selectedGroupData.variables}
/>
)}
</StyledGroupContainer>
)}

View File

@ -2,6 +2,7 @@ import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useState } from 'react';
import {
AnimatedExpandableContainer,
@ -10,6 +11,7 @@ import {
IconEyeOff,
LightIconButton,
} from 'twenty-ui';
type SettingsAdminEnvVariablesRowProps = {
variable: {
name: string;
@ -26,8 +28,23 @@ const StyledTruncatedCell = styled(TableCell)`
cursor: pointer;
`;
const StyledButton = styled(motion.button)`
align-items: center;
border: none;
display: flex;
justify-content: center;
padding-inline: ${({ theme }) => theme.spacing(1)};
background-color: transparent;
height: 24px;
width: 24px;
box-sizing: border-box;
cursor: pointer;
`;
const MotionIconChevronDown = motion(IconChevronRight);
const StyledExpandedDetails = styled.div`
background-color: ${({ theme }) => theme.background.tertiary};
background-color: ${({ theme }) => theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
margin: ${({ theme }) => theme.spacing(2)} 0;
padding: ${({ theme }) => theme.spacing(2)};
@ -40,7 +57,8 @@ const StyledExpandedDetails = styled.div`
`;
const StyledDetailLabel = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
color: ${({ theme }) => theme.font.color.tertiary};
font-weight: ${({ theme }) => theme.font.weight.regular};
padding-right: ${({ theme }) => theme.spacing(4)};
`;
@ -56,12 +74,16 @@ const StyledExpandedLabel = styled.div`
overflow: visible;
`;
const StyledTransitionedIconChevronRight = styled(IconChevronRight)`
cursor: pointer;
transform: ${({ $isExpanded }: { $isExpanded: boolean }) =>
$isExpanded ? 'rotate(90deg)' : 'rotate(0deg)'};
transition: ${({ theme }) =>
`transform ${theme.animation.duration.normal}s ease`};
const StyledValueContainer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
const StyledTableRow = styled(TableRow)<{ isExpanded: boolean }>`
background-color: ${({ isExpanded, theme }) =>
isExpanded ? theme.background.transparent.light : 'transparent'};
margin-bottom: ${({ theme }) => theme.spacing(0.5)};
`;
export const SettingsAdminEnvVariablesRow = ({
@ -85,44 +107,51 @@ export const SettingsAdminEnvVariablesRow = ({
return (
<>
<TableRow
<StyledTableRow
onClick={() => setIsExpanded(!isExpanded)}
gridAutoColumns="4fr 3fr 2fr 1fr 1fr"
gridAutoColumns="5fr 4fr 3fr 1fr"
isExpanded={isExpanded}
>
<StyledTruncatedCell color="primary">
<StyledTruncatedCell color={theme.font.color.primary}>
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
</StyledTruncatedCell>
<StyledTruncatedCell>
<StyledEllipsisLabel>{variable.description}</StyledEllipsisLabel>
</StyledTruncatedCell>
<StyledTruncatedCell>
<StyledTruncatedCell align="right">
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
</StyledTruncatedCell>
<TableCell align="right">
{variable.sensitive && variable.value !== '' && (
<LightIconButton
Icon={showSensitiveValue ? IconEyeOff : IconEye}
size="small"
accent="secondary"
onClick={handleToggleVisibility}
<StyledButton onClick={() => setIsExpanded(!isExpanded)}>
<MotionIconChevronDown
size={theme.icon.size.md}
color={theme.font.color.tertiary}
initial={false}
animate={{ rotate: isExpanded ? 90 : 0 }}
/>
)}
</StyledButton>
</TableCell>
<TableCell align="right">
<StyledTransitionedIconChevronRight
$isExpanded={isExpanded}
size={theme.icon.size.sm}
/>
</TableCell>
</TableRow>
</StyledTableRow>
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
<StyledExpandedDetails>
<StyledDetailLabel>Name:</StyledDetailLabel>
<StyledDetailLabel>Name</StyledDetailLabel>
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
<StyledDetailLabel>Description:</StyledDetailLabel>
<StyledDetailLabel>Description</StyledDetailLabel>
<StyledExpandedLabel>{variable.description}</StyledExpandedLabel>
<StyledDetailLabel>Value:</StyledDetailLabel>
<StyledExpandedLabel>{displayValue}</StyledExpandedLabel>
<StyledDetailLabel>Value</StyledDetailLabel>
<StyledExpandedLabel>
<StyledValueContainer>
{displayValue}
{variable.sensitive && variable.value !== '' && (
<LightIconButton
Icon={showSensitiveValue ? IconEyeOff : IconEye}
size="small"
accent="secondary"
onClick={handleToggleVisibility}
/>
)}
</StyledValueContainer>
</StyledExpandedLabel>
</StyledExpandedDetails>
</AnimatedExpandableContainer>
</>

View File

@ -21,11 +21,10 @@ export const SettingsAdminEnvVariablesTable = ({
variables,
}: SettingsAdminEnvVariablesTableProps) => (
<StyledTable>
<TableRow gridAutoColumns="4fr 3fr 2fr 1fr 1fr">
<TableRow gridAutoColumns="5fr 4fr 3fr 1fr">
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader>Value</TableHeader>
<TableHeader align="right"></TableHeader>
<TableHeader align="right">Value</TableHeader>
<TableHeader align="right"></TableHeader>
</TableRow>
{variables.map((variable) => (

View File

@ -17,11 +17,13 @@ export const ConnectedAccountHealthStatus = () => {
}
const parsedDetails = JSON.parse(details);
const serviceDetails = parsedDetails.details;
const isMessageSyncDown =
parsedDetails.messageSync?.status === AdminPanelHealthServiceStatus.OUTAGE;
serviceDetails.messageSync?.status === AdminPanelHealthServiceStatus.OUTAGE;
const isCalendarSyncDown =
parsedDetails.calendarSync?.status === AdminPanelHealthServiceStatus.OUTAGE;
serviceDetails.calendarSync?.status ===
AdminPanelHealthServiceStatus.OUTAGE;
const errorMessages = [];
if (isMessageSyncDown) {
@ -39,16 +41,16 @@ export const ConnectedAccountHealthStatus = () => {
</StyledErrorMessage>
)}
{!isMessageSyncDown && parsedDetails.messageSync?.details && (
{!isMessageSyncDown && serviceDetails.messageSync?.details && (
<SettingsAdminHealthAccountSyncCountersTable
details={parsedDetails.messageSync.details}
details={serviceDetails.messageSync.details}
title="Message Sync Status"
/>
)}
{!isCalendarSyncDown && parsedDetails.calendarSync?.details && (
{!isCalendarSyncDown && serviceDetails.calendarSync?.details && (
<SettingsAdminHealthAccountSyncCountersTable
details={parsedDetails.calendarSync.details}
details={serviceDetails.calendarSync.details}
title="Calendar Sync Status"
/>
)}

View File

@ -1,43 +0,0 @@
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 } = 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 ? (
<StyledErrorMessage>
{`${indicatorHealth.label} information is not available because the service is down`}
</StyledErrorMessage>
) : (
<StyledDetailsContainer>{formattedDetails}</StyledDetailsContainer>
)}
</Section>
);
};

View File

@ -0,0 +1,49 @@
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
import { JsonTree } from '@/workflow/components/json-visualizer/components/JsonTree';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { Section } from 'twenty-ui';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
const StyledDetailsContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
padding: ${({ theme }) => theme.spacing(4)};
border-radius: ${({ theme }) => theme.border.radius.md};
border: 1px solid ${({ theme }) => theme.border.color.medium};
white-space: pre-wrap;
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
margin-top: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
export const JsonDataIndicatorHealthStatus = () => {
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
const parsedDetails = indicatorHealth.details
? JSON.parse(indicatorHealth.details)
: null;
const isDown =
!indicatorHealth.status ||
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
return (
<Section>
{isDown && (
<StyledErrorMessage>
{indicatorHealth.errorMessage ||
`${indicatorHealth.label} service is unreachable`}
</StyledErrorMessage>
)}
{parsedDetails && (
<StyledDetailsContainer>
<JsonTree value={parsedDetails} />
</StyledDetailsContainer>
)}
</Section>
);
};

View File

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

View File

@ -20,7 +20,7 @@ export const SettingsHealthStatusListCard = ({
<SettingsAdminHealthStatusRightContainer status={service.status} />
)}
to={(service) =>
getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
getSettingsPath(SettingsPath.ServerAdminIndicatorHealthStatus, {
indicatorId: service.id,
})
}

View File

@ -14,6 +14,7 @@ export const SettingsAdminIndicatorHealthContext =
id: '',
label: '',
description: '',
errorMessage: '',
status: AdminPanelHealthServiceStatus.OPERATIONAL,
details: '',
queues: [],

View File

@ -7,6 +7,7 @@ export const GET_INDICATOR_HEALTH_STATUS = gql`
label
description
status
errorMessage
details
queues {
id

View File

@ -179,7 +179,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
items: [
{
label: t`Server Admin`,
path: SettingsPath.AdminPanel,
path: SettingsPath.ServerAdmin,
Icon: IconServer,
isHidden: !isAdminEnabled,
},

View File

@ -34,10 +34,10 @@ export enum SettingsPath {
DevelopersNewWebhook = 'developers/webhooks/new',
DevelopersNewWebhookDetail = 'developers/webhooks/:webhookId',
Releases = 'releases',
AdminPanel = 'admin-panel',
FeatureFlags = 'admin-panel/feature-flags',
AdminPanelHealthStatus = 'admin-panel#health-status',
AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
ServerAdmin = 'server-admin',
FeatureFlags = 'server-admin/feature-flags',
ServerAdminHealthStatus = 'server-admin#health-status',
ServerAdminIndicatorHealthStatus = 'server-admin/health-status/:indicatorId',
Lab = 'lab',
Roles = 'roles',
RoleDetail = 'roles/:roleId',