@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
@@ -1,30 +1,14 @@
1
1
  import * as React from 'react';
2
2
 
3
- import { ConditionStatus, ConditionType, DeviceStatus } from '@flightctl/types';
3
+ import { DeviceStatus } from '@flightctl/types';
4
4
  import { useTranslation } from '../../hooks/useTranslation';
5
- import StatusDisplay, { StatusDisplayContent } from './StatusDisplay';
6
5
  import { getSystemUpdateStatusItems } from '../../utils/status/system';
6
+ import StatusDisplay from './StatusDisplay';
7
7
 
8
8
  const SystemUpdateStatus = ({ deviceStatus }: { deviceStatus?: DeviceStatus }) => {
9
9
  const { t } = useTranslation();
10
10
  const statusItems = getSystemUpdateStatusItems(t);
11
11
 
12
- // TODO Until Update status if fully implemented in the backend, we check for the "SpecValid" error condition
13
- const invalidSpec = deviceStatus?.conditions?.find(
14
- (c) => c.type === ConditionType.DeviceSpecValid && c.status === ConditionStatus.ConditionStatusFalse,
15
- );
16
-
17
- if (invalidSpec) {
18
- return (
19
- <StatusDisplayContent
20
- level="danger"
21
- label={t('Updates blocked')}
22
- messageTitle={t('Invalid configuration')}
23
- message={invalidSpec.message}
24
- />
25
- );
26
- }
27
-
28
12
  const item = statusItems.find((statusItem) => {
29
13
  return statusItem.id === deviceStatus?.updated.status;
30
14
  });
@@ -29,7 +29,8 @@ import {
29
29
  import { labelToString } from '../../utils/labels';
30
30
  import { UpdateScheduleMode } from '../../utils/time';
31
31
 
32
- const SYSTEMD_PATTERNS_REGEXP = /^[a-z][a-z0-9-_.]*$/;
32
+ const SYSTEMD_PATTERNS_REGEXP =
33
+ /^[0-9a-zA-Z:\-_.\\\[\]!\-\*\?]+(@[0-9a-zA-Z:\-_.\\\[\]!\-\*\?]+)?(\.[a-zA-Z\[\]!\-\*\?]+)?$/;
33
34
  const SYSTEMD_UNITS_MAX_PATTERNS = 256;
34
35
 
35
36
  // Accepts uppercase characters, and "underscore" symbols
@@ -653,8 +654,8 @@ export const systemdUnitListValidationSchema = (t: TFunction) =>
653
654
  }),
654
655
  )
655
656
  .test('invalid patterns', (systemdUnits: SystemdUnitFormValue[] | undefined, testContext) => {
656
- // TODO analyze https://github.com/systemd/systemd/blob/9cebda59e818cdb89dc1e53ab5bb51b91b3dc3ff/src/basic/unit-name.c#L42
657
- // and adjust the regular expression and / or the validation to accommodate for it
657
+ // Supports templated SystemD services with extended regex for all allowed unit file formats and glob searches
658
+ // See SYSTEMD_PATTERNS_REGEXP
658
659
  const invalidSystemdUnits = (systemdUnits || [])
659
660
  .map((unit) => unit.pattern)
660
661
  .filter((pattern) => {
@@ -0,0 +1,147 @@
1
+ import * as React from 'react';
2
+ import { useAppContext } from './useAppContext';
3
+ import { useAccessReview } from './useAccessReview';
4
+ import { RESOURCE, VERB } from '../types/rbac';
5
+
6
+ // AlertManager alert structure
7
+ type AlertManagerAlert = {
8
+ fingerprint: string;
9
+ labels: {
10
+ alertname: string;
11
+ org_id: string;
12
+ resource: string;
13
+ [key: string]: string;
14
+ };
15
+ annotations: Record<string, string>;
16
+ startsAt: string;
17
+ endsAt: string;
18
+ updatedAt: string;
19
+ status: {
20
+ state: string;
21
+ inhibitedBy: string[];
22
+ mutedBy: string[];
23
+ silencedBy: string[];
24
+ };
25
+ receivers: Array<{ name: string }>;
26
+ };
27
+
28
+ const DEFAULT_REFRESH_INTERVAL = 30000; // 30 seconds
29
+
30
+ export const useAlerts = (
31
+ refreshInterval: number = DEFAULT_REFRESH_INTERVAL,
32
+ ): [AlertManagerAlert[], boolean, unknown, VoidFunction] => {
33
+ const { getAlerts } = useAppContext();
34
+ const [alerts, setAlerts] = React.useState<AlertManagerAlert[]>([]);
35
+ const [isLoading, setIsLoading] = React.useState(true);
36
+ const [error, setError] = React.useState<unknown>();
37
+ const [forceRefresh, setForceRefresh] = React.useState(0);
38
+
39
+ const refetch = React.useCallback(() => {
40
+ setForceRefresh((prev) => prev + 1);
41
+ }, []);
42
+
43
+ React.useEffect(() => {
44
+ let abortController: AbortController;
45
+ let intervalId: NodeJS.Timeout;
46
+
47
+ const fetchAlerts = async () => {
48
+ if (!getAlerts) {
49
+ setIsLoading(false);
50
+ return;
51
+ }
52
+
53
+ try {
54
+ abortController = new AbortController();
55
+ const alertsData = await getAlerts<AlertManagerAlert[]>(abortController.signal);
56
+ setAlerts(alertsData || []);
57
+ setError(undefined);
58
+ } catch (err) {
59
+ if (!abortController.signal.aborted) {
60
+ setError(err);
61
+ setAlerts([]);
62
+ }
63
+ } finally {
64
+ if (!abortController.signal.aborted) {
65
+ setIsLoading(false);
66
+ }
67
+ }
68
+ };
69
+
70
+ // Initial fetch
71
+ fetchAlerts();
72
+
73
+ // Set up periodic refresh only if getAlerts is available
74
+ if (getAlerts && refreshInterval > 0) {
75
+ intervalId = setInterval(() => {
76
+ fetchAlerts();
77
+ }, refreshInterval);
78
+ }
79
+
80
+ return () => {
81
+ if (intervalId) {
82
+ clearInterval(intervalId);
83
+ }
84
+ abortController?.abort();
85
+ };
86
+ }, [getAlerts, refreshInterval, forceRefresh]);
87
+
88
+ return [alerts, isLoading, error, refetch];
89
+ };
90
+
91
+ // Type guard to check if error is HttpError with status property
92
+ const isHttpError = (error: unknown): error is { status: number } => {
93
+ const errorObj = error as Record<string, unknown>;
94
+ return typeof error === 'object' && error !== null && 'status' in error && typeof errorObj.status === 'number';
95
+ };
96
+
97
+ export const useAlertsEnabled = (): boolean => {
98
+ const { getAlerts } = useAppContext();
99
+ const [alertsEnabled, setAlertsEnabled] = React.useState(false);
100
+
101
+ const [canListAlerts, alertsLoading] = useAccessReview(RESOURCE.ALERTS, VERB.LIST);
102
+ const checkServiceEnabled = Boolean(getAlerts) && !alertsLoading && canListAlerts;
103
+
104
+ React.useEffect(() => {
105
+ let abortController: AbortController;
106
+
107
+ const checkAlertServiceEnabled = async () => {
108
+ if (!getAlerts) {
109
+ // If getAlerts is not available, alerts are disabled
110
+ setAlertsEnabled(false);
111
+ return;
112
+ }
113
+
114
+ try {
115
+ abortController = new AbortController();
116
+ await getAlerts<AlertManagerAlert[]>(abortController.signal);
117
+ setAlertsEnabled(true);
118
+ } catch (err) {
119
+ if (!abortController.signal.aborted) {
120
+ // Check if AlertManager is disabled (501 Not Implemented) or unavailable (500 Internal Server Error)
121
+ if (isHttpError(err) && (err.status === 501 || err.status === 500)) {
122
+ setAlertsEnabled(false);
123
+ } else {
124
+ // For other errors, assume alerts are enabled but there's a temporary issue
125
+ setAlertsEnabled(true);
126
+ }
127
+ }
128
+ }
129
+ };
130
+
131
+ if (checkServiceEnabled) {
132
+ // Check only if we know that the user has permisions to read the alerts
133
+ checkAlertServiceEnabled();
134
+ } else if (!getAlerts) {
135
+ // If getAlerts is not available, immediately set to disabled
136
+ setAlertsEnabled(false);
137
+ }
138
+
139
+ return () => {
140
+ abortController?.abort();
141
+ };
142
+ }, [getAlerts, checkServiceEnabled]);
143
+
144
+ return alertsEnabled;
145
+ };
146
+
147
+ export type { AlertManagerAlert };
@@ -78,6 +78,7 @@ export type AppContextProps = {
78
78
  checkPermissions: (resource: RESOURCE, verb: VERB) => Promise<boolean>;
79
79
  };
80
80
  // Extra fetch functions
81
+ getAlerts?: <R>(abortSignal?: AbortSignal) => Promise<R>;
81
82
  getMetrics?: <R>(query: string, abortSignal?: AbortSignal) => Promise<R>;
82
83
  getCliArtifacts?: (abortSignal?: AbortSignal) => Promise<CliArtifactsResponse>;
83
84
  };
@@ -44,11 +44,7 @@ export interface MetricsQuery {
44
44
  period: string;
45
45
  }
46
46
 
47
- export type FleetConditionType =
48
- | ConditionType.FleetOverlappingSelectors
49
- | ConditionType.FleetValid
50
- | 'Invalid'
51
- | 'SyncPending';
47
+ export type FleetConditionType = ConditionType.FleetValid | 'Invalid' | 'SyncPending';
52
48
 
53
49
  export type FlightControlQuery = ApiQuery | MetricsQuery;
54
50
 
package/src/types/rbac.ts CHANGED
@@ -17,4 +17,5 @@ export enum RESOURCE {
17
17
  RESOURCE_SYNC = 'resourcesyncs',
18
18
  ENROLLMENT_REQUEST = 'enrollmentrequests',
19
19
  ENROLLMENT_REQUEST_APPROVAL = 'enrollmentrequests/approval',
20
+ ALERTS = 'alerts',
20
21
  }
@@ -13,3 +13,18 @@ export const getErrorMsgFromApiResponse = async (response: Response): Promise<st
13
13
  }
14
14
  return `Error ${response.status}: ${response.statusText}${errorText ? `: ${errorText}` : ''}`;
15
15
  };
16
+
17
+ export const getErrorMsgFromAlertsApiResponse = async (response: Response): Promise<string> => {
18
+ let errorText = '';
19
+ try {
20
+ const msg = await response.text();
21
+ try {
22
+ errorText = JSON.parse(msg) as string;
23
+ } catch (e) {
24
+ errorText = msg;
25
+ }
26
+ } catch (e) {
27
+ //ignore
28
+ }
29
+ return `Error ${response.status}: ${response.statusText}${errorText ? `: ${errorText}` : ''}`;
30
+ };
@@ -7,7 +7,6 @@ import { getConditionMessage } from '../error';
7
7
  const FLEET_ROLLOUT_FAILED_REASON = 'Suspended';
8
8
 
9
9
  export const fleetStatusLabels = (t: TFunction) => ({
10
- [ConditionType.FleetOverlappingSelectors]: t('Selectors overlap'),
11
10
  [ConditionType.FleetValid]: t('Valid'),
12
11
  Invalid: t('Invalid'),
13
12
  SyncPending: t('Sync pending'),
@@ -20,18 +19,6 @@ export const getFleetSyncStatus = (
20
19
  status: FleetConditionType;
21
20
  message: string | undefined;
22
21
  } => {
23
- const selectorOverlap = fleet.status?.conditions?.find(
24
- (c) => c.type === ConditionType.FleetOverlappingSelectors && c.status === ConditionStatus.ConditionStatusTrue,
25
- );
26
- if (selectorOverlap) {
27
- return {
28
- message:
29
- selectorOverlap.message ||
30
- t("Fleet's selector overlaps with at least one other fleet, causing ambiguous device ownership."),
31
- status: ConditionType.FleetOverlappingSelectors,
32
- };
33
- }
34
-
35
22
  const validCondition = (fleet.status?.conditions || []).find((c) => c.type === ConditionType.FleetValid);
36
23
  if (validCondition) {
37
24
  const isOK = validCondition.status === ConditionStatus.ConditionStatusTrue;
@@ -0,0 +1,44 @@
1
+ import { TFunction } from 'react-i18next';
2
+
3
+ import { DeviceIntegrityCheckStatusType, DeviceIntegrityStatusSummaryType } from '@flightctl/types';
4
+ import { StatusItem } from './common';
5
+
6
+ export const getIntegrityStatusItems = (t: TFunction): StatusItem<DeviceIntegrityStatusSummaryType>[] => [
7
+ {
8
+ id: DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusFailed,
9
+ label: t('Failed'),
10
+ level: 'warning',
11
+ },
12
+ {
13
+ id: DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusUnsupported,
14
+ label: t('Unsupported'),
15
+ level: 'unknown',
16
+ },
17
+ {
18
+ id: DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusUnknown,
19
+ label: t('Unknown'),
20
+ level: 'unknown',
21
+ },
22
+ {
23
+ id: DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusVerified,
24
+ label: t('Verified'),
25
+ level: 'success',
26
+ },
27
+ ];
28
+
29
+ export const integrityCheckToSummaryType = (
30
+ status: DeviceIntegrityCheckStatusType,
31
+ ): DeviceIntegrityStatusSummaryType => {
32
+ switch (status) {
33
+ case DeviceIntegrityCheckStatusType.DeviceIntegrityCheckStatusVerified:
34
+ return DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusVerified;
35
+ case DeviceIntegrityCheckStatusType.DeviceIntegrityCheckStatusFailed:
36
+ return DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusFailed;
37
+ case DeviceIntegrityCheckStatusType.DeviceIntegrityCheckStatusUnknown:
38
+ return DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusUnknown;
39
+ case DeviceIntegrityCheckStatusType.DeviceIntegrityCheckStatusUnsupported:
40
+ return DeviceIntegrityStatusSummaryType.DeviceIntegrityStatusUnsupported;
41
+ }
42
+ };
43
+
44
+ export const integrityStatusOrder = getIntegrityStatusItems((s: string) => s).map((item) => item.id);