@flightctl/ui-components 0.8.0 → 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.
Files changed (80) hide show
  1. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.d.ts.map +1 -1
  2. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js +6 -0
  3. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js.map +1 -1
  4. package/dist/src/components/Device/DeviceDetails/DeviceFleet.d.ts.map +1 -1
  5. package/dist/src/components/Device/DeviceDetails/DeviceFleet.js +36 -23
  6. package/dist/src/components/Device/DeviceDetails/DeviceFleet.js.map +1 -1
  7. package/dist/src/components/Events/useEvents.d.ts.map +1 -1
  8. package/dist/src/components/Events/useEvents.js +28 -1
  9. package/dist/src/components/Events/useEvents.js.map +1 -1
  10. package/dist/src/components/Fleet/FleetStatus.d.ts.map +1 -1
  11. package/dist/src/components/Fleet/FleetStatus.js +0 -3
  12. package/dist/src/components/Fleet/FleetStatus.js.map +1 -1
  13. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.d.ts +4 -0
  14. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.d.ts.map +1 -0
  15. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js +125 -0
  16. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js.map +1 -0
  17. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.d.ts +4 -0
  18. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.d.ts.map +1 -0
  19. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.js +24 -0
  20. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.js.map +1 -0
  21. package/dist/src/components/OverviewPage/Overview.d.ts.map +1 -1
  22. package/dist/src/components/OverviewPage/Overview.js +13 -6
  23. package/dist/src/components/OverviewPage/Overview.js.map +1 -1
  24. package/dist/src/components/Status/IntegrityStatus.d.ts +10 -0
  25. package/dist/src/components/Status/IntegrityStatus.d.ts.map +1 -0
  26. package/dist/src/components/Status/IntegrityStatus.js +71 -0
  27. package/dist/src/components/Status/IntegrityStatus.js.map +1 -0
  28. package/dist/src/components/Status/StatusDisplay.d.ts +1 -1
  29. package/dist/src/components/Status/StatusDisplay.d.ts.map +1 -1
  30. package/dist/src/components/Status/SystemUpdateStatus.d.ts.map +1 -1
  31. package/dist/src/components/Status/SystemUpdateStatus.js +1 -8
  32. package/dist/src/components/Status/SystemUpdateStatus.js.map +1 -1
  33. package/dist/src/components/form/validations.d.ts.map +1 -1
  34. package/dist/src/components/form/validations.js +3 -3
  35. package/dist/src/components/form/validations.js.map +1 -1
  36. package/dist/src/hooks/useAlerts.d.ts +26 -0
  37. package/dist/src/hooks/useAlerts.d.ts.map +1 -0
  38. package/dist/src/hooks/useAlerts.js +114 -0
  39. package/dist/src/hooks/useAlerts.js.map +1 -0
  40. package/dist/src/hooks/useAppContext.d.ts +1 -0
  41. package/dist/src/hooks/useAppContext.d.ts.map +1 -1
  42. package/dist/src/hooks/useAppContext.js.map +1 -1
  43. package/dist/src/types/extraTypes.d.ts +1 -1
  44. package/dist/src/types/extraTypes.d.ts.map +1 -1
  45. package/dist/src/types/extraTypes.js.map +1 -1
  46. package/dist/src/types/rbac.d.ts +2 -1
  47. package/dist/src/types/rbac.d.ts.map +1 -1
  48. package/dist/src/types/rbac.js +1 -0
  49. package/dist/src/types/rbac.js.map +1 -1
  50. package/dist/src/utils/apiCalls.d.ts +1 -0
  51. package/dist/src/utils/apiCalls.d.ts.map +1 -1
  52. package/dist/src/utils/apiCalls.js +18 -1
  53. package/dist/src/utils/apiCalls.js.map +1 -1
  54. package/dist/src/utils/status/fleet.d.ts +0 -1
  55. package/dist/src/utils/status/fleet.d.ts.map +1 -1
  56. package/dist/src/utils/status/fleet.js +2 -11
  57. package/dist/src/utils/status/fleet.js.map +1 -1
  58. package/dist/src/utils/status/integrity.d.ts +7 -0
  59. package/dist/src/utils/status/integrity.d.ts.map +1 -0
  60. package/dist/src/utils/status/integrity.js +42 -0
  61. package/dist/src/utils/status/integrity.js.map +1 -0
  62. package/package.json +1 -1
  63. package/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.tsx +12 -0
  64. package/src/components/Device/DeviceDetails/DeviceFleet.tsx +64 -39
  65. package/src/components/Events/useEvents.ts +28 -1
  66. package/src/components/Fleet/FleetStatus.tsx +0 -3
  67. package/src/components/OverviewPage/Cards/Alerts/AlertsCard.tsx +182 -0
  68. package/src/components/OverviewPage/Cards/Alerts/AlertsEmptyState.tsx +42 -0
  69. package/src/components/OverviewPage/Overview.tsx +24 -10
  70. package/src/components/Status/IntegrityStatus.tsx +111 -0
  71. package/src/components/Status/StatusDisplay.tsx +1 -1
  72. package/src/components/Status/SystemUpdateStatus.tsx +2 -18
  73. package/src/components/form/validations.ts +4 -3
  74. package/src/hooks/useAlerts.ts +147 -0
  75. package/src/hooks/useAppContext.tsx +1 -0
  76. package/src/types/extraTypes.ts +1 -5
  77. package/src/types/rbac.ts +1 -0
  78. package/src/utils/apiCalls.ts +15 -0
  79. package/src/utils/status/fleet.ts +0 -13
  80. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flightctl/ui-components",
3
- "version": "0.8.0",
3
+ "version": "0.9.0-rc1",
4
4
  "description": "Components for Flightctl UI",
5
5
  "repository": "https://github.com/flightctl/flightctl-ui.git",
6
6
  "homepage": "https://github.com/flightctl/flightctl-ui.git",
@@ -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 = ({ multipleOwnersCondition }: { multipleOwnersCondition?: Condition }) => {
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
- {hasMultipleOwners ? t('Multiple owners') : t('None')}
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
- return <Link to={{ route: ROUTE.FLEET_DETAILS, postfix: fleetName }}>{fleetName}</Link>;
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
- const multipleOwnersCondition = getCondition(device.status?.conditions, ConditionType.DeviceMultipleOwners);
73
- return <FleetLessDevice multipleOwnersCondition={multipleOwnersCondition} />;
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
 
@@ -17,9 +17,6 @@ const FleetStatus = ({ fleet }: { fleet: Fleet }) => {
17
17
  case ConditionType.FleetValid:
18
18
  level = 'success';
19
19
  break;
20
- case ConditionType.FleetOverlappingSelectors:
21
- level = 'warning';
22
- break;
23
20
  case 'SyncPending':
24
21
  level = 'info';
25
22
  break;
@@ -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
- import StatusCard from './Cards/Status/StatusCard';
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
- {canListDevices && (
17
- <GridItem>
18
- <StatusCard />
19
- </GridItem>
20
- )}
21
- {canListErs && (
22
- <GridItem md={6} lg={4}>
23
- <TasksCard />
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;
@@ -68,7 +68,7 @@ type StatusDisplayProps = {
68
68
  level: StatusLevel;
69
69
  customIcon?: React.ComponentClass<SVGIconProps>;
70
70
  };
71
- message?: string;
71
+ message?: React.ReactNode;
72
72
  };
73
73
 
74
74
  const StatusDisplay = ({ item, message }: StatusDisplayProps) => {