Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,18 @@
"Status": "Status",
"Ready": "Ready",
"Restarts": "Restarts",
"Type": "Type",
"Delete system service": "Delete system service",
"Waiting for service to be reported...": "Waiting for service to be reported...",
"App": "App",
"Systemd": "Systemd",
"{{ appName }} is being removed, this may take some time.": "{{ appName }} is being removed, this may take some time.",
"No applications found": "No applications found",
"Type": "Type",
"Reason": "Reason",
"Message": "Message",
"No conditions found": "No conditions found",
"Enable state": "Enable state",
"Load state": "Load state",
"Active state": "Active state",
"Sub state": "Sub state",
"System services table": "System services table",
"No system services found": "No system services found",
"System services can be configured via the device specification": "System services can be configured via the device specification",
"Add devices": "Add devices",
"You can add devices following these steps:": "You can add devices following these steps:",
"Request an enrollment certificate for your device": "Request an enrollment certificate for your device",
Expand All @@ -143,7 +145,6 @@
"Edit alias": "Edit alias",
"Device alias could not be updated": "Device alias could not be updated",
"Applications": "Applications",
"Track systemd services": "Track systemd services",
"Delete forever": "Delete forever",
"You are about to resume <1>{deviceNameOrAlias}</1>": "You are about to resume <1>{deviceNameOrAlias}</1>",
"Details": "Details",
Expand Down Expand Up @@ -179,6 +180,7 @@
"Device is owned by more than one fleet:": "Device is owned by more than one fleet:",
"System image mismatch": "System image mismatch",
"Desired system image: {{ desiredOsImage }}": "Desired system image: {{ desiredOsImage }}",
"System services": "System services",
"Connection was closed": "Connection was closed",
"Reconnect": "Reconnect",
"Show decommissioned devices": "Show decommissioned devices",
Expand Down Expand Up @@ -319,6 +321,7 @@
"Maximum unavailable devices: {{ maxUnavailable }}": "Maximum unavailable devices: {{ maxUnavailable }}",
"Add service": "Add service",
"Tracked systemd services": "Tracked systemd services",
"Track systemd services": "Track systemd services",
"Systemd service name": "Systemd service name",
"Track services": "Track services",
"Approve": "Approve",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,176 +1,58 @@
import * as React from 'react';
import { Bullseye, Button, Spinner } from '@patternfly/react-core';
import { Bullseye } from '@patternfly/react-core';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import MinusCircleIcon from '@patternfly/react-icons/dist/js/icons/minus-circle-icon';

import { DeviceApplicationStatus } from '@flightctl/types';
import { useTranslation } from '../../../hooks/useTranslation';
import ApplicationStatus from '../../Status/ApplicationStatus';
import WithTooltip from '../../common/WithTooltip';

import './ApplicationsTable.css';

type ApplicationsTableProps = {
// Contains the statuses of all detected applications and systemdUnits
// Contains the statuses of all detected applications
appsStatus: DeviceApplicationStatus[];
// List of apps as defined the device / fleet spec
specApps: string[];
// List of systemd units as defined in the device / fleet spec
specSystemdUnits: string[];
// Map: (systemdUnitName, timeItWasAdded)
addedSystemdUnitDates: Record<string, number>;
onSystemdDelete?: (deletedUnit: string) => void;
isUpdating: boolean;
canEdit: boolean;
};

const DELETE_SYSTED_TIMEOUT = 30000; // 30 seconds

const ApplicationsTable = ({
appsStatus,
specApps,
specSystemdUnits,
addedSystemdUnitDates,
onSystemdDelete,
isUpdating,
canEdit,
}: ApplicationsTableProps) => {
const ApplicationsTable = ({ appsStatus, specApps }: ApplicationsTableProps) => {
const { t } = useTranslation();

// Required to be able to detect removed systemd units for their correct type.
// It takes a bit for them to be removed from the applications list.
const [deletedSystemdUnits, setDeletedSystemdUnits] = React.useState<string[]>([]);

React.useEffect(() => {
// Remove a service from the deleted list if it was added back later
const filtered = deletedSystemdUnits.filter((deletedUnit) => {
if (addedSystemdUnitDates[deletedUnit]) {
return false;
}
return true;
});
if (filtered.length < deletedSystemdUnits.length) {
setDeletedSystemdUnits(filtered);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [addedSystemdUnitDates]);

const appsAndSystemdUnits: string[] = [];
// Includes applications already reported in status as well as those that are only in the spec yet
const allAppNames: string[] = [];
specApps.forEach((app) => {
appsAndSystemdUnits.push(app);
});
specSystemdUnits.forEach((systemdUnit) => {
appsAndSystemdUnits.push(systemdUnit);
allAppNames.push(app);
});
appsStatus.forEach((appStatus) => {
if (!appsAndSystemdUnits.includes(appStatus.name)) {
appsAndSystemdUnits.push(appStatus.name);
if (!allAppNames.includes(appStatus.name)) {
allAppNames.push(appStatus.name);
}
});

return appsAndSystemdUnits.length ? (
return allAppNames.length ? (
<Table aria-label={t('Device applications table')}>
<Thead>
<Tr>
<Th>{t('Name')}</Th>
<Th modifier="wrap">{t('Status')}</Th>
<Th modifier="wrap">{t('Ready')}</Th>
<Th modifier="wrap">{t('Restarts')}</Th>
<Th modifier="wrap">{t('Type')}</Th>
</Tr>
</Thead>
<Tbody>
{appsAndSystemdUnits.map((appName) => {
const appDetails = appsStatus.find((app) => app.name === appName);
const isDeletedSystemdUnit = deletedSystemdUnits.includes(appName);
const isAddedSystemdUnit = !!addedSystemdUnitDates[appName];
const isApp =
specApps.includes(appName) ||
!(specSystemdUnits.includes(appName) || isDeletedSystemdUnit || isAddedSystemdUnit);

const deleteSystemdUnit = canEdit && !isDeletedSystemdUnit && onSystemdDelete && (
<Button
aria-label={t('Delete system service')}
isDisabled={isUpdating}
variant="plain"
icon={<MinusCircleIcon />}
onClick={() => {
setDeletedSystemdUnits(deletedSystemdUnits.concat(appName));
onSystemdDelete(appName);
}}
/>
);

if (!appDetails) {
// It's an app or a systemd unit which has not been reported yet
const appAddedTime = addedSystemdUnitDates[appName] || 0;
// For apps there is no spinner since we don't know when the app was added to the spec
const showSpinner = !isApp && Date.now() - appAddedTime < DELETE_SYSTED_TIMEOUT;
return (
<Tr key={appName} className="applications-table__row">
<Td dataLabel={t('Name')}>{appName}</Td>

<Td dataLabel={t('Status')} colSpan={showSpinner ? 3 : 1}>
{showSpinner ? (
<>
<Spinner size="sm" /> {t('Waiting for service to be reported...')}
</>
) : (
'-'
)}
</Td>
{!showSpinner && <Td dataLabel={t('Ready')}>-</Td>}
{!showSpinner && <Td dataLabel={t('Restarts')}>-</Td>}
<Td dataLabel={t('Type')}>
{isApp ? (
<>{t('App')}</>
) : (
<>
{t('Systemd')} {deleteSystemdUnit}
</>
)}
</Td>
</Tr>
);
}

let typeColumnContent: React.ReactNode;

if (isApp) {
typeColumnContent = t('App');
} else if (onSystemdDelete) {
let extraContent: React.ReactNode;
if (isDeletedSystemdUnit) {
extraContent = (
<WithTooltip
showTooltip
content={t('{{ appName }} is being removed, this may take some time.', { appName })}
>
<Spinner size="sm" />
</WithTooltip>
);
} else {
extraContent = deleteSystemdUnit;
}

typeColumnContent = (
<>
{t('Systemd')} {extraContent}
</>
);
} else {
typeColumnContent = t('Systemd');
}
{allAppNames.map((appName) => {
const appDetails = appsStatus.find((app) => app.name === appName) || {
status: null,
ready: '-',
restarts: '-',
};

return (
<Tr key={appName} className="fctl-applications-table__row">
<Tr key={appName}>
<Td dataLabel={t('Name')}>{appName}</Td>
<Td dataLabel={t('Status')}>
<ApplicationStatus status={appDetails.status} />
{appDetails.status ? <ApplicationStatus status={appDetails.status} /> : '-'}
</Td>
<Td dataLabel={t('Ready')}>{appDetails.ready}</Td>
<Td dataLabel={t('Restarts')}>{appDetails.restarts}</Td>
<Td dataLabel={t('Type')}>{typeColumnContent}</Td>
</Tr>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as React from 'react';
import { Bullseye, EmptyState, EmptyStateBody, EmptyStateVariant, Label } from '@patternfly/react-core';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { CogIcon } from '@patternfly/react-icons/dist/js/icons/cog-icon';
import { ClockIcon } from '@patternfly/react-icons/dist/js/icons/clock-icon';

import { SystemdUnitStatus } from '@flightctl/types';
import { useTranslation } from '../../../hooks/useTranslation';

const serviceRegex = /\.service$/;
const timerRegex = /\.timer$/;

const SystemdUnitStatusIcon = ({ unitName }: { unitName: string }) => {
if (serviceRegex.test(unitName)) {
return <CogIcon className="pf-v5-u-mr-md" />;
}
if (timerRegex.test(unitName)) {
return <ClockIcon className="pf-v5-u-mr-md" />;
}
return null;
};

type SystemdUnitsTableProps = {
systemdUnitsStatus: SystemdUnitStatus[];
};

const SystemdUnitRow = ({ unitStatus }: { unitStatus: SystemdUnitStatus }) => {
const { t } = useTranslation();

return (
<Tr key={unitStatus.unit}>
<Td dataLabel={t('Name')}>
<SystemdUnitStatusIcon unitName={unitStatus.unit} /> {unitStatus.unit}
</Td>
<Td dataLabel={t('Enable state')}>
<Label color="blue" variant="outline">
{unitStatus.enableState}
</Label>
</Td>
<Td dataLabel={t('Load state')}>
<Label color="blue" variant="outline">
{unitStatus.loadState}
</Label>
</Td>
<Td dataLabel={t('Active state')}>
<Label color="blue" variant="outline">
{unitStatus.activeState}
</Label>
</Td>
<Td dataLabel={t('Sub state')}>
<Label color="blue" variant="outline">
{unitStatus.subState}
</Label>
</Td>
</Tr>
);
};

// Contrary to applications, we don't show the matchPatterns that were defined in the device spec.
// Since these may contain glob patterns, etc, we can't reliably translate the patterns to a list of units.
const SystemdUnitsTable = ({ systemdUnitsStatus }: SystemdUnitsTableProps) => {
const { t } = useTranslation();

return systemdUnitsStatus.length ? (
<Table aria-label={t('System services table')}>
<Thead>
<Tr>
<Th>{t('Name')}</Th>
<Th modifier="wrap">{t('Enable state')}</Th>
<Th modifier="wrap">{t('Load state')}</Th>
<Th modifier="wrap">{t('Active state')}</Th>
<Th modifier="wrap">{t('Sub state')}</Th>
</Tr>
</Thead>
<Tbody>
{systemdUnitsStatus.map((unitStatus) => {
return <SystemdUnitRow key={unitStatus.unit} unitStatus={unitStatus} />;
})}
</Tbody>
</Table>
) : (
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateBody>
<p>{t('No system services found')}</p>
<p className="pf-v5-u-font-size-sm">{t('System services can be configured via the device specification')}</p>
</EmptyStateBody>
</EmptyState>
</Bullseye>
);
};

export default SystemdUnitsTable;
Loading