@flightctl/ui-components 0.8.1 → 0.9.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.d.ts.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js +6 -0
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceFleet.d.ts.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceFleet.js +36 -23
- package/dist/src/components/Device/DeviceDetails/DeviceFleet.js.map +1 -1
- package/dist/src/components/Events/useEvents.d.ts.map +1 -1
- package/dist/src/components/Events/useEvents.js +28 -1
- package/dist/src/components/Events/useEvents.js.map +1 -1
- package/dist/src/components/Fleet/FleetStatus.d.ts.map +1 -1
- package/dist/src/components/Fleet/FleetStatus.js +0 -3
- package/dist/src/components/Fleet/FleetStatus.js.map +1 -1
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.d.ts +4 -0
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.d.ts.map +1 -0
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js +125 -0
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js.map +1 -0
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.d.ts +4 -0
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.d.ts.map +1 -0
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.js +24 -0
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.js.map +1 -0
- package/dist/src/components/OverviewPage/Overview.d.ts.map +1 -1
- package/dist/src/components/OverviewPage/Overview.js +13 -6
- package/dist/src/components/OverviewPage/Overview.js.map +1 -1
- package/dist/src/components/Status/IntegrityStatus.d.ts +10 -0
- package/dist/src/components/Status/IntegrityStatus.d.ts.map +1 -0
- package/dist/src/components/Status/IntegrityStatus.js +71 -0
- package/dist/src/components/Status/IntegrityStatus.js.map +1 -0
- package/dist/src/components/Status/StatusDisplay.d.ts +1 -1
- package/dist/src/components/Status/StatusDisplay.d.ts.map +1 -1
- package/dist/src/components/Status/SystemUpdateStatus.d.ts.map +1 -1
- package/dist/src/components/Status/SystemUpdateStatus.js +1 -8
- package/dist/src/components/Status/SystemUpdateStatus.js.map +1 -1
- package/dist/src/components/form/validations.d.ts.map +1 -1
- package/dist/src/components/form/validations.js +3 -3
- package/dist/src/components/form/validations.js.map +1 -1
- package/dist/src/hooks/useAlerts.d.ts +26 -0
- package/dist/src/hooks/useAlerts.d.ts.map +1 -0
- package/dist/src/hooks/useAlerts.js +114 -0
- package/dist/src/hooks/useAlerts.js.map +1 -0
- package/dist/src/hooks/useAppContext.d.ts +1 -0
- package/dist/src/hooks/useAppContext.d.ts.map +1 -1
- package/dist/src/hooks/useAppContext.js.map +1 -1
- package/dist/src/types/extraTypes.d.ts +1 -1
- package/dist/src/types/extraTypes.d.ts.map +1 -1
- package/dist/src/types/extraTypes.js.map +1 -1
- package/dist/src/types/rbac.d.ts +2 -1
- package/dist/src/types/rbac.d.ts.map +1 -1
- package/dist/src/types/rbac.js +1 -0
- package/dist/src/types/rbac.js.map +1 -1
- package/dist/src/utils/apiCalls.d.ts +1 -0
- package/dist/src/utils/apiCalls.d.ts.map +1 -1
- package/dist/src/utils/apiCalls.js +18 -1
- package/dist/src/utils/apiCalls.js.map +1 -1
- package/dist/src/utils/status/fleet.d.ts +0 -1
- package/dist/src/utils/status/fleet.d.ts.map +1 -1
- package/dist/src/utils/status/fleet.js +2 -11
- package/dist/src/utils/status/fleet.js.map +1 -1
- package/dist/src/utils/status/integrity.d.ts +7 -0
- package/dist/src/utils/status/integrity.d.ts.map +1 -0
- package/dist/src/utils/status/integrity.js +42 -0
- package/dist/src/utils/status/integrity.js.map +1 -0
- package/package.json +1 -1
- package/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.tsx +12 -0
- package/src/components/Device/DeviceDetails/DeviceFleet.tsx +64 -39
- package/src/components/Events/useEvents.ts +28 -1
- package/src/components/Fleet/FleetStatus.tsx +0 -3
- package/src/components/OverviewPage/Cards/Alerts/AlertsCard.tsx +182 -0
- package/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.tsx +42 -0
- package/src/components/OverviewPage/Overview.tsx +24 -10
- package/src/components/Status/IntegrityStatus.tsx +111 -0
- package/src/components/Status/StatusDisplay.tsx +1 -1
- package/src/components/Status/SystemUpdateStatus.tsx +2 -18
- package/src/components/form/validations.ts +4 -3
- package/src/hooks/useAlerts.ts +147 -0
- package/src/hooks/useAppContext.tsx +1 -0
- package/src/types/extraTypes.ts +1 -5
- package/src/types/rbac.ts +1 -0
- package/src/utils/apiCalls.ts +15 -0
- package/src/utils/status/fleet.ts +0 -13
- package/src/utils/status/integrity.ts +44 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integrity.js","sourceRoot":"","sources":["../../../../src/utils/status/integrity.ts"],"names":[],"mappings":";;;AAEA,4CAAoG;AAG7F,MAAM,uBAAuB,GAAG,CAAC,CAAY,EAAkD,EAAE,CAAC;IACvG;QACE,EAAE,EAAE,wCAAgC,CAAC,2BAA2B;QAChE,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC;QAClB,KAAK,EAAE,SAAS;KACjB;IACD;QACE,EAAE,EAAE,wCAAgC,CAAC,gCAAgC;QACrE,KAAK,EAAE,CAAC,CAAC,aAAa,CAAC;QACvB,KAAK,EAAE,SAAS;KACjB;IACD;QACE,EAAE,EAAE,wCAAgC,CAAC,4BAA4B;QACjE,KAAK,EAAE,CAAC,CAAC,SAAS,CAAC;QACnB,KAAK,EAAE,SAAS;KACjB;IACD;QACE,EAAE,EAAE,wCAAgC,CAAC,6BAA6B;QAClE,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC;QACpB,KAAK,EAAE,SAAS;KACjB;CACF,CAAC;AArBW,QAAA,uBAAuB,2BAqBlC;AAEK,MAAM,2BAA2B,GAAG,CACzC,MAAsC,EACJ,EAAE;IACpC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,sCAA8B,CAAC,kCAAkC;YACpE,OAAO,wCAAgC,CAAC,6BAA6B,CAAC;QACxE,KAAK,sCAA8B,CAAC,gCAAgC;YAClE,OAAO,wCAAgC,CAAC,2BAA2B,CAAC;QACtE,KAAK,sCAA8B,CAAC,iCAAiC;YACnE,OAAO,wCAAgC,CAAC,4BAA4B,CAAC;QACvE,KAAK,sCAA8B,CAAC,qCAAqC;YACvE,OAAO,wCAAgC,CAAC,gCAAgC,CAAC;IAC7E,CAAC;AACH,CAAC,CAAC;AAbW,QAAA,2BAA2B,+BAatC;AAEW,QAAA,oBAAoB,GAAG,IAAA,+BAAuB,EAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ import LabelWithHelperText from '../../../common/WithHelperText';
|
|
|
15
15
|
import ApplicationSummaryStatus from '../../../Status/ApplicationSummaryStatus';
|
|
16
16
|
import DeviceStatus from '../../../Status/DeviceStatus';
|
|
17
17
|
import SystemUpdateStatus from '../../../Status/SystemUpdateStatus';
|
|
18
|
+
import IntegrityStatus from '../../../Status/IntegrityStatus';
|
|
18
19
|
|
|
19
20
|
const StatusContent = ({ device }: { device: Required<Device> }) => {
|
|
20
21
|
const { t } = useTranslation();
|
|
@@ -59,6 +60,17 @@ const StatusContent = ({ device }: { device: Required<Device> }) => {
|
|
|
59
60
|
<SystemUpdateStatus deviceStatus={device.status} />
|
|
60
61
|
</DescriptionListDescription>
|
|
61
62
|
</DescriptionListGroup>
|
|
63
|
+
<DescriptionListGroup>
|
|
64
|
+
<DescriptionListTerm>
|
|
65
|
+
<LabelWithHelperText
|
|
66
|
+
label={t('Integrity status')}
|
|
67
|
+
content={t('Indicates whether the device has been verified as secure and authentic.')}
|
|
68
|
+
/>
|
|
69
|
+
</DescriptionListTerm>
|
|
70
|
+
<DescriptionListDescription>
|
|
71
|
+
<IntegrityStatus integrityStatus={device.status.integrity} />
|
|
72
|
+
</DescriptionListDescription>
|
|
73
|
+
</DescriptionListGroup>
|
|
62
74
|
<DescriptionListGroup>
|
|
63
75
|
<DescriptionListTerm>{t('Last seen')}</DescriptionListTerm>
|
|
64
76
|
<DescriptionListDescription>{timeSinceText(t, device.status.lastSeen)}</DescriptionListDescription>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { Button, List, ListItem, Popover } from '@patternfly/react-core';
|
|
2
|
+
import { Button, Icon, List, ListItem, Popover } from '@patternfly/react-core';
|
|
3
3
|
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons/dist/js/icons/outlined-question-circle-icon';
|
|
4
|
+
import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-triangle-icon';
|
|
4
5
|
|
|
5
6
|
import { Condition, ConditionType, Device } from '@flightctl/types';
|
|
6
7
|
import { getDeviceFleet } from '../../../utils/devices';
|
|
@@ -10,44 +11,12 @@ import { Link, ROUTE } from '../../../hooks/useNavigate';
|
|
|
10
11
|
|
|
11
12
|
import './DeviceFleet.css';
|
|
12
13
|
|
|
13
|
-
const FleetLessDevice = (
|
|
14
|
+
const FleetLessDevice = () => {
|
|
14
15
|
const { t } = useTranslation();
|
|
15
|
-
|
|
16
|
-
let message = '';
|
|
17
|
-
let owners: string[] = [];
|
|
18
|
-
if (multipleOwnersCondition) {
|
|
19
|
-
message = t('Device is owned by more than one fleet');
|
|
20
|
-
owners = (multipleOwnersCondition.message || '').split(',');
|
|
21
|
-
} else {
|
|
22
|
-
message = t("Device labels don't match any fleet's selector labels");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const hasMultipleOwners = owners.length > 1;
|
|
26
|
-
|
|
27
16
|
return (
|
|
28
17
|
<div className="fctl-device-fleet">
|
|
29
|
-
{
|
|
30
|
-
<Popover
|
|
31
|
-
bodyContent={
|
|
32
|
-
<span>
|
|
33
|
-
{message}
|
|
34
|
-
{hasMultipleOwners && (
|
|
35
|
-
<span>
|
|
36
|
-
{': '}
|
|
37
|
-
<List>
|
|
38
|
-
{owners.map((ownerFleet) => {
|
|
39
|
-
return (
|
|
40
|
-
<ListItem key={ownerFleet}>
|
|
41
|
-
<Link to={{ route: ROUTE.FLEET_DETAILS, postfix: ownerFleet }}>{ownerFleet}</Link>
|
|
42
|
-
</ListItem>
|
|
43
|
-
);
|
|
44
|
-
})}
|
|
45
|
-
</List>
|
|
46
|
-
</span>
|
|
47
|
-
)}
|
|
48
|
-
</span>
|
|
49
|
-
}
|
|
50
|
-
>
|
|
18
|
+
{t('None')}
|
|
19
|
+
<Popover bodyContent={<span>{t("Device labels don't match any fleet's selector labels")}</span>}>
|
|
51
20
|
<Button
|
|
52
21
|
isInline
|
|
53
22
|
variant="plain"
|
|
@@ -59,18 +28,74 @@ const FleetLessDevice = ({ multipleOwnersCondition }: { multipleOwnersCondition?
|
|
|
59
28
|
);
|
|
60
29
|
};
|
|
61
30
|
|
|
31
|
+
const MultipleDeviceOwners = ({ multipleOwnersCondition }: { multipleOwnersCondition: Condition }) => {
|
|
32
|
+
const { t } = useTranslation();
|
|
33
|
+
|
|
34
|
+
const owners: string[] = (multipleOwnersCondition.message || '').split(',');
|
|
35
|
+
|
|
36
|
+
const hasMultipleOwners = owners.length > 1;
|
|
37
|
+
if (!hasMultipleOwners) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Popover
|
|
43
|
+
bodyContent={
|
|
44
|
+
<span>
|
|
45
|
+
{t('Device is owned by more than one fleet:')}
|
|
46
|
+
<span>
|
|
47
|
+
<List>
|
|
48
|
+
{owners.map((ownerFleet) => {
|
|
49
|
+
return (
|
|
50
|
+
<ListItem key={ownerFleet}>
|
|
51
|
+
<Link to={{ route: ROUTE.FLEET_DETAILS, postfix: ownerFleet }}>{ownerFleet}</Link>
|
|
52
|
+
</ListItem>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</List>
|
|
56
|
+
</span>
|
|
57
|
+
</span>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
<Button
|
|
61
|
+
isInline
|
|
62
|
+
variant="plain"
|
|
63
|
+
icon={
|
|
64
|
+
<Icon status="warning">
|
|
65
|
+
<ExclamationTriangleIcon />
|
|
66
|
+
</Icon>
|
|
67
|
+
}
|
|
68
|
+
aria-label={t('Ownership information')}
|
|
69
|
+
/>
|
|
70
|
+
</Popover>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
62
74
|
const DeviceFleet = ({ device }: { device?: Device }) => {
|
|
75
|
+
const { t } = useTranslation();
|
|
63
76
|
if (!device) {
|
|
64
77
|
return '-';
|
|
65
78
|
}
|
|
66
79
|
|
|
80
|
+
const multipleOwnersCondition = getCondition(device.status?.conditions, ConditionType.DeviceMultipleOwners);
|
|
81
|
+
let fleetNameEl: React.ReactNode = null;
|
|
67
82
|
const fleetName = getDeviceFleet(device.metadata);
|
|
68
83
|
if (fleetName) {
|
|
69
|
-
|
|
84
|
+
fleetNameEl = <Link to={{ route: ROUTE.FLEET_DETAILS, postfix: fleetName }}>{fleetName}</Link>;
|
|
85
|
+
} else if (multipleOwnersCondition) {
|
|
86
|
+
// Device has no owner set, but with the multiple owners condition. The warning icon should be displayed
|
|
87
|
+
fleetNameEl = t('None');
|
|
88
|
+
} else {
|
|
89
|
+
// Valid fleetless device
|
|
90
|
+
fleetNameEl = <FleetLessDevice />;
|
|
70
91
|
}
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
{fleetNameEl}
|
|
96
|
+
{multipleOwnersCondition && <MultipleDeviceOwners multipleOwnersCondition={multipleOwnersCondition} />}
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
74
99
|
};
|
|
75
100
|
|
|
76
101
|
export default DeviceFleet;
|
|
@@ -43,19 +43,21 @@ export type EventSearchCriteria = Partial<ObjectReference> & {
|
|
|
43
43
|
const getEventReasonTitles = (t: TFunction, kindType: string): Record<Event.reason, string> => {
|
|
44
44
|
const params = { resourceType: kindType };
|
|
45
45
|
return {
|
|
46
|
+
// Generic resource events
|
|
46
47
|
[Event.reason.RESOURCE_CREATED]: t('{{ resourceType }} was created successfully', params),
|
|
47
48
|
[Event.reason.RESOURCE_CREATION_FAILED]: t('{{ resourceType }} could not be created', params),
|
|
48
49
|
[Event.reason.RESOURCE_DELETED]: t('{{ resourceType }} was deleted successfully', params),
|
|
49
50
|
[Event.reason.RESOURCE_DELETION_FAILED]: t('{{ resourceType }} could not be deleted', params),
|
|
50
51
|
[Event.reason.RESOURCE_UPDATED]: t('{{ resourceType }} was updated successfully', params),
|
|
51
52
|
[Event.reason.RESOURCE_UPDATE_FAILED]: t('{{ resourceType }} could not be updated', params),
|
|
53
|
+
// Device events
|
|
52
54
|
[Event.reason.DEVICE_DECOMMISSIONED]: t('Device decommissioned successfully'),
|
|
53
55
|
[Event.reason.DEVICE_DECOMMISSION_FAILED]: t('Device could not be decommissioned'),
|
|
54
56
|
[Event.reason.DEVICE_CPUNORMAL]: t('CPU utilization has returned to normal'),
|
|
55
57
|
[Event.reason.DEVICE_CPUWARNING]: t('CPU utilization has reached a warning level'),
|
|
56
58
|
[Event.reason.DEVICE_CPUCRITICAL]: t('CPU utilization has reached a critical level'),
|
|
57
59
|
[Event.reason.DEVICE_MEMORY_NORMAL]: t('Memory utilization has returned to normal'),
|
|
58
|
-
[Event.reason.DEVICE_MEMORY_WARNING]: t('Memory utilization has reached a warning level
|
|
60
|
+
[Event.reason.DEVICE_MEMORY_WARNING]: t('Memory utilization has reached a warning level'),
|
|
59
61
|
[Event.reason.DEVICE_MEMORY_CRITICAL]: t('Memory utilization has reached a critical level'),
|
|
60
62
|
[Event.reason.DEVICE_DISK_NORMAL]: t('Disk utilization has returned to normal'),
|
|
61
63
|
[Event.reason.DEVICE_DISK_WARNING]: t('Disk utilization has reached a warning level'),
|
|
@@ -65,9 +67,34 @@ const getEventReasonTitles = (t: TFunction, kindType: string): Record<Event.reas
|
|
|
65
67
|
[Event.reason.DEVICE_APPLICATION_ERROR]: t('Some application workloads are in error state'),
|
|
66
68
|
[Event.reason.DEVICE_CONNECTED]: t('Device reconnected'),
|
|
67
69
|
[Event.reason.DEVICE_DISCONNECTED]: t('Device is disconnected'),
|
|
70
|
+
[Event.reason.DEVICE_IS_REBOOTING]: t('Device is rebooting'),
|
|
68
71
|
[Event.reason.DEVICE_CONTENT_UP_TO_DATE]: t('Device returned to being up-to-date'),
|
|
69
72
|
[Event.reason.DEVICE_CONTENT_UPDATING]: t('Device is updating'),
|
|
70
73
|
[Event.reason.DEVICE_CONTENT_OUT_OF_DATE]: t('Device is out-of-date'),
|
|
74
|
+
[Event.reason.DEVICE_MULTIPLE_OWNERS_DETECTED]: t('Detected device ownership conflict'),
|
|
75
|
+
[Event.reason.DEVICE_MULTIPLE_OWNERS_RESOLVED]: t('Device ownership conflict has been resolved'),
|
|
76
|
+
[Event.reason.DEVICE_SPEC_VALID]: t('Device specification has returned to a valid state'),
|
|
77
|
+
[Event.reason.DEVICE_SPEC_INVALID]: t('Device specification is invalid'),
|
|
78
|
+
// Enrollment request events
|
|
79
|
+
[Event.reason.ENROLLMENT_REQUEST_APPROVED]: t('Enrollment request was approved'),
|
|
80
|
+
[Event.reason.ENROLLMENT_REQUEST_APPROVAL_FAILED]: t('Enrollment request approval failed'),
|
|
81
|
+
// Internal task events
|
|
82
|
+
[Event.reason.INTERNAL_TASK_FAILED]: t('Internal task failed'),
|
|
83
|
+
// Repository events
|
|
84
|
+
[Event.reason.REPOSITORY_ACCESSIBLE]: t('Repository is accessible'),
|
|
85
|
+
[Event.reason.REPOSITORY_INACCESSIBLE]: t('Repository is inaccessible'),
|
|
86
|
+
// Fleet events
|
|
87
|
+
[Event.reason.FLEET_ROLLOUT_STARTED]: t('Fleet rollout started'),
|
|
88
|
+
[Event.reason.FLEET_ROLLOUT_CREATED]: t('Fleet rollout created'),
|
|
89
|
+
[Event.reason.FLEET_ROLLOUT_BATCH_COMPLETED]: t('Fleet rollout batch completed'),
|
|
90
|
+
// Resource sync events
|
|
91
|
+
[Event.reason.RESOURCE_SYNC_SYNCED]: t('Resourcesync synchronization completed', params),
|
|
92
|
+
[Event.reason.RESOURCE_SYNC_SYNC_FAILED]: t('Resourcesync synchronization failed', params),
|
|
93
|
+
[Event.reason.RESOURCE_SYNC_PARSED]: t('Resourcesync parsed successfully', params),
|
|
94
|
+
[Event.reason.RESOURCE_SYNC_PARSING_FAILED]: t('Resourcesync parsing failed', params),
|
|
95
|
+
[Event.reason.RESOURCE_SYNC_ACCESSIBLE]: t('Resourcesync is accessible', params),
|
|
96
|
+
[Event.reason.RESOURCE_SYNC_INACCESSIBLE]: t('Resourcesync is not accessible', params),
|
|
97
|
+
[Event.reason.RESOURCE_SYNC_COMMIT_DETECTED]: t('Resourcesync new commit detected', params),
|
|
71
98
|
};
|
|
72
99
|
};
|
|
73
100
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
Card,
|
|
5
|
+
CardBody,
|
|
6
|
+
CardTitle,
|
|
7
|
+
Icon,
|
|
8
|
+
List,
|
|
9
|
+
ListItem,
|
|
10
|
+
Stack,
|
|
11
|
+
StackItem,
|
|
12
|
+
TextContent,
|
|
13
|
+
} from '@patternfly/react-core';
|
|
14
|
+
import { TFunction } from 'react-i18next';
|
|
15
|
+
import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-circle-icon';
|
|
16
|
+
|
|
17
|
+
import { Event, ResourceKind } from '@flightctl/types';
|
|
18
|
+
import { useAlerts } from '../../../../hooks/useAlerts';
|
|
19
|
+
import { useTranslation } from '../../../../hooks/useTranslation';
|
|
20
|
+
import { getDateDisplay } from '../../../../utils/dates';
|
|
21
|
+
import { getErrorMessage } from '../../../../utils/error';
|
|
22
|
+
import ResourceLink from '../../../common/ResourceLink';
|
|
23
|
+
|
|
24
|
+
import AlertsEmptyState from './AlertsEmptyState';
|
|
25
|
+
|
|
26
|
+
// Define only the Event.reason values that correspond to alerts
|
|
27
|
+
type AlertEventReason =
|
|
28
|
+
| Event.reason.DEVICE_APPLICATION_DEGRADED
|
|
29
|
+
| Event.reason.DEVICE_APPLICATION_ERROR
|
|
30
|
+
| Event.reason.DEVICE_APPLICATION_HEALTHY
|
|
31
|
+
| Event.reason.DEVICE_CPUCRITICAL
|
|
32
|
+
| Event.reason.DEVICE_CPUNORMAL
|
|
33
|
+
| Event.reason.DEVICE_CPUWARNING
|
|
34
|
+
| Event.reason.DEVICE_MEMORY_CRITICAL
|
|
35
|
+
| Event.reason.DEVICE_MEMORY_NORMAL
|
|
36
|
+
| Event.reason.DEVICE_MEMORY_WARNING
|
|
37
|
+
| Event.reason.DEVICE_DISK_CRITICAL
|
|
38
|
+
| Event.reason.DEVICE_DISK_NORMAL
|
|
39
|
+
| Event.reason.DEVICE_DISK_WARNING
|
|
40
|
+
| Event.reason.DEVICE_CONNECTED
|
|
41
|
+
| Event.reason.DEVICE_DISCONNECTED
|
|
42
|
+
| Event.reason.RESOURCE_DELETED
|
|
43
|
+
| Event.reason.DEVICE_DECOMMISSIONED;
|
|
44
|
+
|
|
45
|
+
// Alert types that are processed by the alert exporter
|
|
46
|
+
const getAlertTitles = (t: (key: string) => string): Record<AlertEventReason, string> => ({
|
|
47
|
+
// Application status alerts
|
|
48
|
+
[Event.reason.DEVICE_APPLICATION_DEGRADED]: t('Some application workloads are degraded'),
|
|
49
|
+
[Event.reason.DEVICE_APPLICATION_ERROR]: t('Some application workloads are in error state'),
|
|
50
|
+
[Event.reason.DEVICE_APPLICATION_HEALTHY]: t('All application workloads are healthy'),
|
|
51
|
+
// CPU alerts
|
|
52
|
+
[Event.reason.DEVICE_CPUCRITICAL]: t('CPU utilization has reached a critical level'),
|
|
53
|
+
[Event.reason.DEVICE_CPUNORMAL]: t('CPU utilization has returned to normal'),
|
|
54
|
+
[Event.reason.DEVICE_CPUWARNING]: t('CPU utilization has reached a warning level'),
|
|
55
|
+
// Memory alerts
|
|
56
|
+
[Event.reason.DEVICE_MEMORY_CRITICAL]: t('Memory utilization has reached a critical level'),
|
|
57
|
+
[Event.reason.DEVICE_MEMORY_NORMAL]: t('Memory utilization has returned to normal'),
|
|
58
|
+
[Event.reason.DEVICE_MEMORY_WARNING]: t('Memory utilization has reached a warning level'),
|
|
59
|
+
// Disk alerts
|
|
60
|
+
[Event.reason.DEVICE_DISK_CRITICAL]: t('Disk utilization has reached a critical level'),
|
|
61
|
+
[Event.reason.DEVICE_DISK_NORMAL]: t('Disk utilization has returned to normal'),
|
|
62
|
+
[Event.reason.DEVICE_DISK_WARNING]: t('Disk utilization has reached a warning level'),
|
|
63
|
+
// Other device-specific alerts
|
|
64
|
+
[Event.reason.DEVICE_CONNECTED]: t('Device reconnected'),
|
|
65
|
+
[Event.reason.DEVICE_DISCONNECTED]: t('Device is disconnected'),
|
|
66
|
+
[Event.reason.DEVICE_DECOMMISSIONED]: t('Device decommissioned successfully'),
|
|
67
|
+
// Resource lifecycle alerts
|
|
68
|
+
[Event.reason.RESOURCE_DELETED]: t('Resource was deleted successfully'),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const alertResourceKind: Record<AlertEventReason, ResourceKind | undefined> = {
|
|
72
|
+
// Application status alerts
|
|
73
|
+
[Event.reason.DEVICE_APPLICATION_DEGRADED]: ResourceKind.DEVICE,
|
|
74
|
+
[Event.reason.DEVICE_APPLICATION_ERROR]: ResourceKind.DEVICE,
|
|
75
|
+
[Event.reason.DEVICE_APPLICATION_HEALTHY]: ResourceKind.DEVICE,
|
|
76
|
+
// CPU alerts
|
|
77
|
+
[Event.reason.DEVICE_CPUCRITICAL]: ResourceKind.DEVICE,
|
|
78
|
+
[Event.reason.DEVICE_CPUNORMAL]: ResourceKind.DEVICE,
|
|
79
|
+
[Event.reason.DEVICE_CPUWARNING]: ResourceKind.DEVICE,
|
|
80
|
+
// Memory alerts
|
|
81
|
+
[Event.reason.DEVICE_MEMORY_CRITICAL]: ResourceKind.DEVICE,
|
|
82
|
+
[Event.reason.DEVICE_MEMORY_NORMAL]: ResourceKind.DEVICE,
|
|
83
|
+
[Event.reason.DEVICE_MEMORY_WARNING]: ResourceKind.DEVICE,
|
|
84
|
+
// Disk alerts
|
|
85
|
+
[Event.reason.DEVICE_DISK_CRITICAL]: ResourceKind.DEVICE,
|
|
86
|
+
[Event.reason.DEVICE_DISK_NORMAL]: ResourceKind.DEVICE,
|
|
87
|
+
[Event.reason.DEVICE_DISK_WARNING]: ResourceKind.DEVICE,
|
|
88
|
+
// Other device-specific alerts
|
|
89
|
+
[Event.reason.DEVICE_CONNECTED]: ResourceKind.DEVICE,
|
|
90
|
+
[Event.reason.DEVICE_DISCONNECTED]: ResourceKind.DEVICE,
|
|
91
|
+
[Event.reason.DEVICE_DECOMMISSIONED]: ResourceKind.DEVICE,
|
|
92
|
+
// Resource lifecycle alerts
|
|
93
|
+
[Event.reason.RESOURCE_DELETED]: undefined,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const resourceKindLabel = (t: TFunction, resourceKind: ResourceKind | undefined) => {
|
|
97
|
+
switch (resourceKind) {
|
|
98
|
+
case undefined:
|
|
99
|
+
return t('Resource');
|
|
100
|
+
case ResourceKind.DEVICE:
|
|
101
|
+
return t('Device');
|
|
102
|
+
case ResourceKind.ENROLLMENT_REQUEST:
|
|
103
|
+
return t('Enrollment request');
|
|
104
|
+
case ResourceKind.CERTIFICATE_SIGNING_REQUEST:
|
|
105
|
+
return t('Certificate signing request');
|
|
106
|
+
case ResourceKind.FLEET:
|
|
107
|
+
return t('Fleet');
|
|
108
|
+
case ResourceKind.REPOSITORY:
|
|
109
|
+
return t('Repository');
|
|
110
|
+
case ResourceKind.RESOURCE_SYNC:
|
|
111
|
+
return t('Resource sync');
|
|
112
|
+
case ResourceKind.TEMPLATE_VERSION:
|
|
113
|
+
return t('Template version');
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const AlertsCard = () => {
|
|
118
|
+
const { t } = useTranslation();
|
|
119
|
+
const [alerts, isLoading, error] = useAlerts();
|
|
120
|
+
const alertTypes = React.useMemo(() => getAlertTitles(t), [t]);
|
|
121
|
+
|
|
122
|
+
let alertsBody: React.ReactNode;
|
|
123
|
+
if (isLoading) {
|
|
124
|
+
alertsBody = <CardBody>{t('Loading alerts...')}</CardBody>;
|
|
125
|
+
} else if (error) {
|
|
126
|
+
alertsBody = (
|
|
127
|
+
<CardBody>
|
|
128
|
+
<Alert variant="danger" title={t('Error loading alerts')}>
|
|
129
|
+
{getErrorMessage(error)}
|
|
130
|
+
</Alert>
|
|
131
|
+
</CardBody>
|
|
132
|
+
);
|
|
133
|
+
} else if (alerts.length === 0) {
|
|
134
|
+
alertsBody = <AlertsEmptyState />;
|
|
135
|
+
} else {
|
|
136
|
+
alertsBody = (
|
|
137
|
+
<List isPlain>
|
|
138
|
+
{alerts.map((alert) => {
|
|
139
|
+
const alertName = alert.labels.alertname as AlertEventReason;
|
|
140
|
+
const resourceKind = alertResourceKind[alertName];
|
|
141
|
+
const kindLabel = resourceKindLabel(t, resourceKind);
|
|
142
|
+
return (
|
|
143
|
+
<ListItem key={alert.fingerprint}>
|
|
144
|
+
<Stack>
|
|
145
|
+
<StackItem>
|
|
146
|
+
<Icon status="danger" size="md">
|
|
147
|
+
<ExclamationCircleIcon />
|
|
148
|
+
</Icon>{' '}
|
|
149
|
+
<strong>{alertTypes[alertName] || alertName}</strong>
|
|
150
|
+
</StackItem>
|
|
151
|
+
<StackItem>
|
|
152
|
+
<TextContent>
|
|
153
|
+
{kindLabel}{' '}
|
|
154
|
+
{resourceKind === ResourceKind.DEVICE ? (
|
|
155
|
+
<ResourceLink id={alert.labels.resource} />
|
|
156
|
+
) : (
|
|
157
|
+
alert.labels.resource
|
|
158
|
+
)}
|
|
159
|
+
</TextContent>
|
|
160
|
+
</StackItem>
|
|
161
|
+
<StackItem>
|
|
162
|
+
<TextContent>
|
|
163
|
+
<small>{getDateDisplay(alert.startsAt || '')}</small>
|
|
164
|
+
</TextContent>
|
|
165
|
+
</StackItem>
|
|
166
|
+
</Stack>
|
|
167
|
+
</ListItem>
|
|
168
|
+
);
|
|
169
|
+
})}
|
|
170
|
+
</List>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<Card>
|
|
176
|
+
<CardTitle>{t('Alerts')}</CardTitle>
|
|
177
|
+
<CardBody>{alertsBody}</CardBody>
|
|
178
|
+
</Card>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default AlertsCard;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
EmptyState,
|
|
4
|
+
EmptyStateBody,
|
|
5
|
+
EmptyStateFooter,
|
|
6
|
+
EmptyStateHeader,
|
|
7
|
+
EmptyStateIcon,
|
|
8
|
+
EmptyStateVariant,
|
|
9
|
+
Text,
|
|
10
|
+
} from '@patternfly/react-core';
|
|
11
|
+
import { EmptyStateActions } from '@patternfly/react-core/dist/esm/components/EmptyState/EmptyStateActions';
|
|
12
|
+
import { SearchIcon } from '@patternfly/react-icons/dist/js/icons/search-icon';
|
|
13
|
+
|
|
14
|
+
import { useTranslation } from '../../../../hooks/useTranslation';
|
|
15
|
+
import { Link, ROUTE } from '../../../../hooks/useNavigate';
|
|
16
|
+
|
|
17
|
+
const AlertsEmptyState = () => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
return (
|
|
20
|
+
<EmptyState variant={EmptyStateVariant.sm}>
|
|
21
|
+
<EmptyStateHeader
|
|
22
|
+
titleText={t('There are no active Alerts at this time')}
|
|
23
|
+
headingLevel="h5"
|
|
24
|
+
icon={<EmptyStateIcon icon={SearchIcon} />}
|
|
25
|
+
/>
|
|
26
|
+
<EmptyStateBody>
|
|
27
|
+
<Text>
|
|
28
|
+
{t('This area displays current notifications about your monitored devices and fleets.')}{' '}
|
|
29
|
+
{t('Alerts will appear here if an issue is detected.')}
|
|
30
|
+
</Text>
|
|
31
|
+
</EmptyStateBody>
|
|
32
|
+
|
|
33
|
+
<EmptyStateFooter>
|
|
34
|
+
<EmptyStateActions>
|
|
35
|
+
<Link to={ROUTE.DEVICES}>{t('View devices')}</Link>
|
|
36
|
+
</EmptyStateActions>
|
|
37
|
+
</EmptyStateFooter>
|
|
38
|
+
</EmptyState>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default AlertsEmptyState;
|
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Grid, GridItem } from '@patternfly/react-core';
|
|
3
|
-
|
|
4
|
-
import TasksCard from './Cards/Tasks/TasksCard';
|
|
3
|
+
|
|
5
4
|
import { useAccessReview } from '../../hooks/useAccessReview';
|
|
6
5
|
import { RESOURCE, VERB } from '../../types/rbac';
|
|
7
6
|
import PageWithPermissions from '../common/PageWithPermissions';
|
|
7
|
+
import { useAlertsEnabled } from '../../hooks/useAlerts';
|
|
8
|
+
|
|
9
|
+
import AlertsCard from './Cards/Alerts/AlertsCard';
|
|
10
|
+
import StatusCard from './Cards/Status/StatusCard';
|
|
11
|
+
import TasksCard from './Cards/Tasks/TasksCard';
|
|
8
12
|
|
|
9
13
|
const Overview = () => {
|
|
10
14
|
const [canListDevices, devicesLoading] = useAccessReview(RESOURCE.DEVICE, VERB.LIST);
|
|
11
15
|
const [canListErs, erLoading] = useAccessReview(RESOURCE.ENROLLMENT_REQUEST, VERB.LIST);
|
|
16
|
+
const alertsEnabled = useAlertsEnabled();
|
|
12
17
|
|
|
13
18
|
return (
|
|
14
19
|
<PageWithPermissions allowed={canListDevices || canListErs} loading={devicesLoading || erLoading}>
|
|
15
20
|
<Grid hasGutter>
|
|
16
|
-
{
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
<GridItem md={alertsEnabled ? 9 : 12}>
|
|
22
|
+
<Grid hasGutter>
|
|
23
|
+
{canListDevices && (
|
|
24
|
+
<GridItem>
|
|
25
|
+
<StatusCard />
|
|
26
|
+
</GridItem>
|
|
27
|
+
)}
|
|
28
|
+
{canListErs && (
|
|
29
|
+
<GridItem md={9} lg={6}>
|
|
30
|
+
<TasksCard />
|
|
31
|
+
</GridItem>
|
|
32
|
+
)}
|
|
33
|
+
</Grid>
|
|
34
|
+
</GridItem>
|
|
35
|
+
{alertsEnabled && (
|
|
36
|
+
<GridItem md={3}>
|
|
37
|
+
<AlertsCard />
|
|
24
38
|
</GridItem>
|
|
25
39
|
)}
|
|
26
40
|
</Grid>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TFunction } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
import { Icon, Stack, StackItem } from '@patternfly/react-core';
|
|
5
|
+
import {
|
|
6
|
+
DeviceIntegrityCheckStatusType,
|
|
7
|
+
DeviceIntegrityStatus,
|
|
8
|
+
DeviceIntegrityStatusSummaryType,
|
|
9
|
+
} from '@flightctl/types';
|
|
10
|
+
|
|
11
|
+
import { useTranslation } from '../../hooks/useTranslation';
|
|
12
|
+
import { getIntegrityStatusItems, integrityCheckToSummaryType } from '../../utils/status/integrity';
|
|
13
|
+
import { StatusItem, getDefaultStatusColor } from '../../utils/status/common';
|
|
14
|
+
import { getDefaultStatusIcon } from '../../utils/status/common';
|
|
15
|
+
import { getDateDisplay } from '../../utils/dates';
|
|
16
|
+
import StatusDisplay from './StatusDisplay';
|
|
17
|
+
|
|
18
|
+
const getIntegrityCheckItem = (
|
|
19
|
+
t: TFunction,
|
|
20
|
+
statusItems: StatusItem<DeviceIntegrityStatusSummaryType>[],
|
|
21
|
+
statusField: { status: DeviceIntegrityCheckStatusType; info?: string },
|
|
22
|
+
key: string,
|
|
23
|
+
): React.ReactNode => {
|
|
24
|
+
const statusType = integrityCheckToSummaryType(statusField.status);
|
|
25
|
+
const levelItem = statusItems.find((statusItem) => statusItem.id === statusType) || {
|
|
26
|
+
id: DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusUnknown,
|
|
27
|
+
label: t('Unknown'),
|
|
28
|
+
level: 'unknown' as const,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const IconComponent = levelItem.customIcon || getDefaultStatusIcon(levelItem.level);
|
|
32
|
+
const iconStatus = levelItem.level === 'unknown' ? undefined : levelItem.level;
|
|
33
|
+
|
|
34
|
+
const label =
|
|
35
|
+
key === 'device-identity'
|
|
36
|
+
? t('Device identity - {{ status }}', { status: levelItem.label })
|
|
37
|
+
: t('TPM - {{ status }}', { status: levelItem.label });
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<StackItem key={key}>
|
|
41
|
+
<Icon
|
|
42
|
+
status={iconStatus}
|
|
43
|
+
style={{ '--pf-v5-c-icon__content--Color': getDefaultStatusColor(levelItem.level) } as React.CSSProperties}
|
|
44
|
+
>
|
|
45
|
+
<IconComponent />
|
|
46
|
+
</Icon>{' '}
|
|
47
|
+
{label}
|
|
48
|
+
{statusField.info ? `: ${statusField.info}` : ''}
|
|
49
|
+
</StackItem>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const getIntegrityExtraDetails = (
|
|
54
|
+
t: TFunction,
|
|
55
|
+
integrityStatus: DeviceIntegrityStatus,
|
|
56
|
+
statusItems: StatusItem<DeviceIntegrityStatusSummaryType>[],
|
|
57
|
+
): React.ReactNode | undefined => {
|
|
58
|
+
if (!integrityStatus) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const extraDetails: React.ReactNode[] = [];
|
|
63
|
+
|
|
64
|
+
const deviceIdentity = integrityStatus.deviceIdentity;
|
|
65
|
+
if (deviceIdentity) {
|
|
66
|
+
extraDetails.push(getIntegrityCheckItem(t, statusItems, deviceIdentity, 'device-identity'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tpm = integrityStatus.tpm;
|
|
70
|
+
if (tpm) {
|
|
71
|
+
extraDetails.push(getIntegrityCheckItem(t, statusItems, tpm, 'tpm'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return extraDetails.length > 0 ? <Stack hasGutter>{extraDetails}</Stack> : undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const IntegrityStatus = ({ integrityStatus }: { integrityStatus?: DeviceIntegrityStatus }) => {
|
|
78
|
+
const { t } = useTranslation();
|
|
79
|
+
|
|
80
|
+
// Show unknown status if we don't receive any information
|
|
81
|
+
if (!integrityStatus) {
|
|
82
|
+
return <StatusDisplay />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const statusItems = getIntegrityStatusItems(t);
|
|
86
|
+
const item = statusItems.find((statusItem) => {
|
|
87
|
+
return statusItem.id === (integrityStatus.status || DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusUnknown);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let message: React.ReactNode = integrityStatus.info || '';
|
|
91
|
+
|
|
92
|
+
const extraDetails = item ? getIntegrityExtraDetails(t, integrityStatus, statusItems) : undefined;
|
|
93
|
+
if (extraDetails) {
|
|
94
|
+
message = (
|
|
95
|
+
<Stack hasGutter>
|
|
96
|
+
{integrityStatus.info && <StackItem>{integrityStatus.info}</StackItem>}
|
|
97
|
+
<StackItem>{extraDetails}</StackItem>
|
|
98
|
+
{integrityStatus.lastVerified && (
|
|
99
|
+
<StackItem>
|
|
100
|
+
<small>
|
|
101
|
+
{t('Last verification at: {{ timestamp }}', { timestamp: getDateDisplay(integrityStatus.lastVerified) })}
|
|
102
|
+
</small>
|
|
103
|
+
</StackItem>
|
|
104
|
+
)}
|
|
105
|
+
</Stack>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return <StatusDisplay item={item} message={message} />;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default IntegrityStatus;
|