@flightctl/ui-components 0.8.1 → 0.9.1
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
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 =
|
|
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
|
-
//
|
|
657
|
-
//
|
|
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
|
};
|
package/src/types/extraTypes.ts
CHANGED
|
@@ -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
package/src/utils/apiCalls.ts
CHANGED
|
@@ -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);
|