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:
@ -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 />}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export const SettingsHealthStatusListCard = ({
|
||||
<SettingsAdminHealthStatusRightContainer status={service.status} />
|
||||
)}
|
||||
to={(service) =>
|
||||
getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
|
||||
getSettingsPath(SettingsPath.ServerAdminIndicatorHealthStatus, {
|
||||
indicatorId: service.id,
|
||||
})
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export const SettingsAdminIndicatorHealthContext =
|
||||
id: '',
|
||||
label: '',
|
||||
description: '',
|
||||
errorMessage: '',
|
||||
status: AdminPanelHealthServiceStatus.OPERATIONAL,
|
||||
details: '',
|
||||
queues: [],
|
||||
|
||||
@ -7,6 +7,7 @@ export const GET_INDICATOR_HEALTH_STATUS = gql`
|
||||
label
|
||||
description
|
||||
status
|
||||
errorMessage
|
||||
details
|
||||
queues {
|
||||
id
|
||||
|
||||
@ -179,7 +179,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
items: [
|
||||
{
|
||||
label: t`Server Admin`,
|
||||
path: SettingsPath.AdminPanel,
|
||||
path: SettingsPath.ServerAdmin,
|
||||
Icon: IconServer,
|
||||
isHidden: !isAdminEnabled,
|
||||
},
|
||||
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user