Skip to content

Commit 6de2b1a

Browse files
committed
EDM-2422: Split applications and systemd units statuses
1 parent ce054d1 commit 6de2b1a

12 files changed

Lines changed: 194 additions & 232 deletions

File tree

libs/i18n/locales/en/translation.json

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,18 @@
120120
"Status": "Status",
121121
"Ready": "Ready",
122122
"Restarts": "Restarts",
123-
"Type": "Type",
124-
"Delete system service": "Delete system service",
125-
"Waiting for service to be reported...": "Waiting for service to be reported...",
126-
"App": "App",
127-
"Systemd": "Systemd",
128-
"{{ appName }} is being removed, this may take some time.": "{{ appName }} is being removed, this may take some time.",
129123
"No applications found": "No applications found",
124+
"Type": "Type",
130125
"Reason": "Reason",
131126
"Message": "Message",
132127
"No conditions found": "No conditions found",
128+
"Enable state": "Enable state",
129+
"Load state": "Load state",
130+
"Active state": "Active state",
131+
"Sub state": "Sub state",
132+
"System services table": "System services table",
133+
"No system services found": "No system services found",
134+
"System services can be configured via the device specification": "System services can be configured via the device specification",
133135
"Add devices": "Add devices",
134136
"You can add devices following these steps:": "You can add devices following these steps:",
135137
"Request an enrollment certificate for your device": "Request an enrollment certificate for your device",
@@ -143,7 +145,6 @@
143145
"Edit alias": "Edit alias",
144146
"Device alias could not be updated": "Device alias could not be updated",
145147
"Applications": "Applications",
146-
"Track systemd services": "Track systemd services",
147148
"Delete forever": "Delete forever",
148149
"You are about to resume <1>{deviceNameOrAlias}</1>": "You are about to resume <1>{deviceNameOrAlias}</1>",
149150
"Details": "Details",
@@ -179,6 +180,7 @@
179180
"Device is owned by more than one fleet:": "Device is owned by more than one fleet:",
180181
"System image mismatch": "System image mismatch",
181182
"Desired system image: {{ desiredOsImage }}": "Desired system image: {{ desiredOsImage }}",
183+
"System services": "System services",
182184
"Connection was closed": "Connection was closed",
183185
"Reconnect": "Reconnect",
184186
"Show decommissioned devices": "Show decommissioned devices",
@@ -319,6 +321,7 @@
319321
"Maximum unavailable devices: {{ maxUnavailable }}": "Maximum unavailable devices: {{ maxUnavailable }}",
320322
"Add service": "Add service",
321323
"Tracked systemd services": "Tracked systemd services",
324+
"Track systemd services": "Track systemd services",
322325
"Systemd service name": "Systemd service name",
323326
"Track services": "Track services",
324327
"Approve": "Approve",

libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.css

Lines changed: 0 additions & 7 deletions
This file was deleted.

libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx

Lines changed: 17 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,58 @@
11
import * as React from 'react';
2-
import { Bullseye, Button, Spinner } from '@patternfly/react-core';
2+
import { Bullseye } from '@patternfly/react-core';
33
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
4-
import MinusCircleIcon from '@patternfly/react-icons/dist/js/icons/minus-circle-icon';
54

65
import { DeviceApplicationStatus } from '@flightctl/types';
76
import { useTranslation } from '../../../hooks/useTranslation';
87
import ApplicationStatus from '../../Status/ApplicationStatus';
9-
import WithTooltip from '../../common/WithTooltip';
10-
11-
import './ApplicationsTable.css';
128

139
type ApplicationsTableProps = {
14-
// Contains the statuses of all detected applications and systemdUnits
10+
// Contains the statuses of all detected applications
1511
appsStatus: DeviceApplicationStatus[];
1612
// List of apps as defined the device / fleet spec
1713
specApps: string[];
18-
// List of systemd units as defined in the device / fleet spec
19-
specSystemdUnits: string[];
20-
// Map: (systemdUnitName, timeItWasAdded)
21-
addedSystemdUnitDates: Record<string, number>;
22-
onSystemdDelete?: (deletedUnit: string) => void;
23-
isUpdating: boolean;
24-
canEdit: boolean;
2514
};
2615

27-
const DELETE_SYSTED_TIMEOUT = 30000; // 30 seconds
28-
29-
const ApplicationsTable = ({
30-
appsStatus,
31-
specApps,
32-
specSystemdUnits,
33-
addedSystemdUnitDates,
34-
onSystemdDelete,
35-
isUpdating,
36-
canEdit,
37-
}: ApplicationsTableProps) => {
16+
const ApplicationsTable = ({ appsStatus, specApps }: ApplicationsTableProps) => {
3817
const { t } = useTranslation();
3918

40-
// Required to be able to detect removed systemd units for their correct type.
41-
// It takes a bit for them to be removed from the applications list.
42-
const [deletedSystemdUnits, setDeletedSystemdUnits] = React.useState<string[]>([]);
43-
44-
React.useEffect(() => {
45-
// Remove a service from the deleted list if it was added back later
46-
const filtered = deletedSystemdUnits.filter((deletedUnit) => {
47-
if (addedSystemdUnitDates[deletedUnit]) {
48-
return false;
49-
}
50-
return true;
51-
});
52-
if (filtered.length < deletedSystemdUnits.length) {
53-
setDeletedSystemdUnits(filtered);
54-
}
55-
// eslint-disable-next-line react-hooks/exhaustive-deps
56-
}, [addedSystemdUnitDates]);
57-
58-
const appsAndSystemdUnits: string[] = [];
19+
// Includes applications already reported in status as well as those that are only in the spec yet
20+
const allAppNames: string[] = [];
5921
specApps.forEach((app) => {
60-
appsAndSystemdUnits.push(app);
61-
});
62-
specSystemdUnits.forEach((systemdUnit) => {
63-
appsAndSystemdUnits.push(systemdUnit);
22+
allAppNames.push(app);
6423
});
6524
appsStatus.forEach((appStatus) => {
66-
if (!appsAndSystemdUnits.includes(appStatus.name)) {
67-
appsAndSystemdUnits.push(appStatus.name);
25+
if (!allAppNames.includes(appStatus.name)) {
26+
allAppNames.push(appStatus.name);
6827
}
6928
});
7029

71-
return appsAndSystemdUnits.length ? (
30+
return allAppNames.length ? (
7231
<Table aria-label={t('Device applications table')}>
7332
<Thead>
7433
<Tr>
7534
<Th>{t('Name')}</Th>
7635
<Th modifier="wrap">{t('Status')}</Th>
7736
<Th modifier="wrap">{t('Ready')}</Th>
7837
<Th modifier="wrap">{t('Restarts')}</Th>
79-
<Th modifier="wrap">{t('Type')}</Th>
8038
</Tr>
8139
</Thead>
8240
<Tbody>
83-
{appsAndSystemdUnits.map((appName) => {
84-
const appDetails = appsStatus.find((app) => app.name === appName);
85-
const isDeletedSystemdUnit = deletedSystemdUnits.includes(appName);
86-
const isAddedSystemdUnit = !!addedSystemdUnitDates[appName];
87-
const isApp =
88-
specApps.includes(appName) ||
89-
!(specSystemdUnits.includes(appName) || isDeletedSystemdUnit || isAddedSystemdUnit);
90-
91-
const deleteSystemdUnit = canEdit && !isDeletedSystemdUnit && onSystemdDelete && (
92-
<Button
93-
aria-label={t('Delete system service')}
94-
isDisabled={isUpdating}
95-
variant="plain"
96-
icon={<MinusCircleIcon />}
97-
onClick={() => {
98-
setDeletedSystemdUnits(deletedSystemdUnits.concat(appName));
99-
onSystemdDelete(appName);
100-
}}
101-
/>
102-
);
103-
104-
if (!appDetails) {
105-
// It's an app or a systemd unit which has not been reported yet
106-
const appAddedTime = addedSystemdUnitDates[appName] || 0;
107-
// For apps there is no spinner since we don't know when the app was added to the spec
108-
const showSpinner = !isApp && Date.now() - appAddedTime < DELETE_SYSTED_TIMEOUT;
109-
return (
110-
<Tr key={appName} className="applications-table__row">
111-
<Td dataLabel={t('Name')}>{appName}</Td>
112-
113-
<Td dataLabel={t('Status')} colSpan={showSpinner ? 3 : 1}>
114-
{showSpinner ? (
115-
<>
116-
<Spinner size="sm" /> {t('Waiting for service to be reported...')}
117-
</>
118-
) : (
119-
'-'
120-
)}
121-
</Td>
122-
{!showSpinner && <Td dataLabel={t('Ready')}>-</Td>}
123-
{!showSpinner && <Td dataLabel={t('Restarts')}>-</Td>}
124-
<Td dataLabel={t('Type')}>
125-
{isApp ? (
126-
<>{t('App')}</>
127-
) : (
128-
<>
129-
{t('Systemd')} {deleteSystemdUnit}
130-
</>
131-
)}
132-
</Td>
133-
</Tr>
134-
);
135-
}
136-
137-
let typeColumnContent: React.ReactNode;
138-
139-
if (isApp) {
140-
typeColumnContent = t('App');
141-
} else if (onSystemdDelete) {
142-
let extraContent: React.ReactNode;
143-
if (isDeletedSystemdUnit) {
144-
extraContent = (
145-
<WithTooltip
146-
showTooltip
147-
content={t('{{ appName }} is being removed, this may take some time.', { appName })}
148-
>
149-
<Spinner size="sm" />
150-
</WithTooltip>
151-
);
152-
} else {
153-
extraContent = deleteSystemdUnit;
154-
}
155-
156-
typeColumnContent = (
157-
<>
158-
{t('Systemd')} {extraContent}
159-
</>
160-
);
161-
} else {
162-
typeColumnContent = t('Systemd');
163-
}
41+
{allAppNames.map((appName) => {
42+
const appDetails = appsStatus.find((app) => app.name === appName) || {
43+
status: null,
44+
ready: '-',
45+
restarts: '-',
46+
};
16447

16548
return (
166-
<Tr key={appName} className="fctl-applications-table__row">
49+
<Tr key={appName}>
16750
<Td dataLabel={t('Name')}>{appName}</Td>
16851
<Td dataLabel={t('Status')}>
169-
<ApplicationStatus status={appDetails.status} />
52+
{appDetails.status ? <ApplicationStatus status={appDetails.status} /> : '-'}
17053
</Td>
17154
<Td dataLabel={t('Ready')}>{appDetails.ready}</Td>
17255
<Td dataLabel={t('Restarts')}>{appDetails.restarts}</Td>
173-
<Td dataLabel={t('Type')}>{typeColumnContent}</Td>
17456
</Tr>
17557
);
17658
})}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as React from 'react';
2+
import { Bullseye, EmptyState, EmptyStateBody, EmptyStateVariant, Label } from '@patternfly/react-core';
3+
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
4+
import { CogIcon } from '@patternfly/react-icons/dist/js/icons/cog-icon';
5+
import { ClockIcon } from '@patternfly/react-icons/dist/js/icons/clock-icon';
6+
7+
import { SystemdUnitStatus } from '@flightctl/types';
8+
import { useTranslation } from '../../../hooks/useTranslation';
9+
10+
const serviceRegex = /\.service$/;
11+
const timerRegex = /\.timer$/;
12+
13+
const SystemdUnitStatusIcon = ({ unitName }: { unitName: string }) => {
14+
if (serviceRegex.test(unitName)) {
15+
return <CogIcon className="pf-v5-u-mr-md" />;
16+
}
17+
if (timerRegex.test(unitName)) {
18+
return <ClockIcon className="pf-v5-u-mr-md" />;
19+
}
20+
return null;
21+
};
22+
23+
type SystemdUnitsTableProps = {
24+
systemdUnitsStatus: SystemdUnitStatus[];
25+
};
26+
27+
const SystemdUnitRow = ({ unitStatus }: { unitStatus: SystemdUnitStatus }) => {
28+
const { t } = useTranslation();
29+
30+
return (
31+
<Tr key={unitStatus.unit}>
32+
<Td dataLabel={t('Name')}>
33+
<SystemdUnitStatusIcon unitName={unitStatus.unit} /> {unitStatus.unit}
34+
</Td>
35+
<Td dataLabel={t('Enable state')}>
36+
<Label color="blue" variant="outline">
37+
{unitStatus.enableState}
38+
</Label>
39+
</Td>
40+
<Td dataLabel={t('Load state')}>
41+
<Label color="blue" variant="outline">
42+
{unitStatus.loadState}
43+
</Label>
44+
</Td>
45+
<Td dataLabel={t('Active state')}>
46+
<Label color="blue" variant="outline">
47+
{unitStatus.activeState}
48+
</Label>
49+
</Td>
50+
<Td dataLabel={t('Sub state')}>
51+
<Label color="blue" variant="outline">
52+
{unitStatus.subState}
53+
</Label>
54+
</Td>
55+
</Tr>
56+
);
57+
};
58+
59+
// Contrary to applications, we don't show the matchPatterns that were defined in the device spec.
60+
// Since these may contain glob patterns, etc, we can't reliably translate the patterns to a list of units.
61+
const SystemdUnitsTable = ({ systemdUnitsStatus }: SystemdUnitsTableProps) => {
62+
const { t } = useTranslation();
63+
64+
return systemdUnitsStatus.length ? (
65+
<Table aria-label={t('System services table')}>
66+
<Thead>
67+
<Tr>
68+
<Th>{t('Name')}</Th>
69+
<Th modifier="wrap">{t('Enable state')}</Th>
70+
<Th modifier="wrap">{t('Load state')}</Th>
71+
<Th modifier="wrap">{t('Active state')}</Th>
72+
<Th modifier="wrap">{t('Sub state')}</Th>
73+
</Tr>
74+
</Thead>
75+
<Tbody>
76+
{systemdUnitsStatus.map((unitStatus) => {
77+
return <SystemdUnitRow key={unitStatus.unit} unitStatus={unitStatus} />;
78+
})}
79+
</Tbody>
80+
</Table>
81+
) : (
82+
<Bullseye>
83+
<EmptyState variant={EmptyStateVariant.sm}>
84+
<EmptyStateBody>
85+
<p>{t('No system services found')}</p>
86+
<p className="pf-v5-u-font-size-sm">{t('System services can be configured via the device specification')}</p>
87+
</EmptyStateBody>
88+
</EmptyState>
89+
</Bullseye>
90+
);
91+
};
92+
93+
export default SystemdUnitsTable;

0 commit comments

Comments
 (0)