@flightctl/ui-components 0.9.2 → 0.10.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 (242) hide show
  1. package/dist/src/components/DetailsPage/DetailsPage.d.ts +2 -1
  2. package/dist/src/components/DetailsPage/DetailsPage.d.ts.map +1 -1
  3. package/dist/src/components/DetailsPage/DetailsPage.js +2 -1
  4. package/dist/src/components/DetailsPage/DetailsPage.js.map +1 -1
  5. package/dist/src/components/DetailsPage/DetailsPageActions.d.ts +10 -0
  6. package/dist/src/components/DetailsPage/DetailsPageActions.d.ts.map +1 -1
  7. package/dist/src/components/DetailsPage/DetailsPageActions.js +23 -1
  8. package/dist/src/components/DetailsPage/DetailsPageActions.js.map +1 -1
  9. package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.d.ts.map +1 -1
  10. package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.js +29 -3
  11. package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.js.map +1 -1
  12. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTab.d.ts.map +1 -1
  13. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTab.js +0 -4
  14. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTab.js.map +1 -1
  15. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.d.ts.map +1 -1
  16. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js +8 -2
  17. package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js.map +1 -1
  18. package/dist/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.d.ts.map +1 -1
  19. package/dist/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.js +1 -3
  20. package/dist/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.js.map +1 -1
  21. package/dist/src/components/Device/DevicesPage/DecommissionedDevicesTable.d.ts.map +1 -1
  22. package/dist/src/components/Device/DevicesPage/DecommissionedDevicesTable.js +0 -3
  23. package/dist/src/components/Device/DevicesPage/DecommissionedDevicesTable.js.map +1 -1
  24. package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.d.ts.map +1 -1
  25. package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.js +3 -1
  26. package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.js.map +1 -1
  27. package/dist/src/components/Device/DevicesPage/DevicesPage.d.ts.map +1 -1
  28. package/dist/src/components/Device/DevicesPage/DevicesPage.js +1 -1
  29. package/dist/src/components/Device/DevicesPage/DevicesPage.js.map +1 -1
  30. package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.d.ts +3 -1
  31. package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.d.ts.map +1 -1
  32. package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.js +12 -4
  33. package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.js.map +1 -1
  34. package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.d.ts +2 -1
  35. package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.d.ts.map +1 -1
  36. package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.js +7 -6
  37. package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.js.map +1 -1
  38. package/dist/src/components/Device/SystemdUnitsModal/TrackSystemdUnitsForm.js +1 -1
  39. package/dist/src/components/Device/SystemdUnitsModal/TrackSystemdUnitsForm.js.map +1 -1
  40. package/dist/src/components/Events/useEvents.d.ts.map +1 -1
  41. package/dist/src/components/Events/useEvents.js +12 -0
  42. package/dist/src/components/Events/useEvents.js.map +1 -1
  43. package/dist/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.js +1 -1
  44. package/dist/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.js.map +1 -1
  45. package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.d.ts.map +1 -1
  46. package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.js +4 -3
  47. package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.js.map +1 -1
  48. package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.d.ts.map +1 -1
  49. package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.js +7 -1
  50. package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.js.map +1 -1
  51. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.d.ts +8 -0
  52. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.d.ts.map +1 -0
  53. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.js +36 -0
  54. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.js.map +1 -0
  55. package/dist/src/components/Fleet/FleetsPage.d.ts.map +1 -1
  56. package/dist/src/components/Fleet/FleetsPage.js +2 -0
  57. package/dist/src/components/Fleet/FleetsPage.js.map +1 -1
  58. package/dist/src/components/ListPage/ListPageActions.d.ts +3 -2
  59. package/dist/src/components/ListPage/ListPageActions.d.ts.map +1 -1
  60. package/dist/src/components/ListPage/ListPageActions.js +27 -1
  61. package/dist/src/components/ListPage/ListPageActions.js.map +1 -1
  62. package/dist/src/components/Masthead/CommandLineToolsPage.d.ts.map +1 -1
  63. package/dist/src/components/Masthead/CommandLineToolsPage.js +18 -14
  64. package/dist/src/components/Masthead/CommandLineToolsPage.js.map +1 -1
  65. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.d.ts.map +1 -1
  66. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js +15 -5
  67. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js.map +1 -1
  68. package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.d.ts.map +1 -1
  69. package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.js +7 -1
  70. package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.js.map +1 -1
  71. package/dist/src/components/OverviewPage/Overview.d.ts.map +1 -1
  72. package/dist/src/components/OverviewPage/Overview.js +4 -2
  73. package/dist/src/components/OverviewPage/Overview.js.map +1 -1
  74. package/dist/src/components/Status/StatusDisplay.d.ts +3 -1
  75. package/dist/src/components/Status/StatusDisplay.d.ts.map +1 -1
  76. package/dist/src/components/Status/StatusDisplay.js +8 -8
  77. package/dist/src/components/Status/StatusDisplay.js.map +1 -1
  78. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.d.ts +7 -0
  79. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.d.ts.map +1 -0
  80. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.js +22 -0
  81. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.js.map +1 -0
  82. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.d.ts +16 -0
  83. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.d.ts.map +1 -0
  84. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.js +75 -0
  85. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.js.map +1 -0
  86. package/dist/src/components/SystemRestore/SystemRestoreBanners.css +6 -0
  87. package/dist/src/components/SystemRestore/SystemRestoreBanners.d.ts +28 -0
  88. package/dist/src/components/SystemRestore/SystemRestoreBanners.d.ts.map +1 -0
  89. package/dist/src/components/SystemRestore/SystemRestoreBanners.js +38 -0
  90. package/dist/src/components/SystemRestore/SystemRestoreBanners.js.map +1 -0
  91. package/dist/src/components/charts/utils.js +1 -1
  92. package/dist/src/components/charts/utils.js.map +1 -1
  93. package/dist/src/components/common/OrganizationGuard.d.ts +13 -0
  94. package/dist/src/components/common/OrganizationGuard.d.ts.map +1 -0
  95. package/dist/src/components/common/OrganizationGuard.js +106 -0
  96. package/dist/src/components/common/OrganizationGuard.js.map +1 -0
  97. package/dist/src/components/common/OrganizationSelector.d.ts +8 -0
  98. package/dist/src/components/common/OrganizationSelector.d.ts.map +1 -0
  99. package/dist/src/components/common/OrganizationSelector.js +92 -0
  100. package/dist/src/components/common/OrganizationSelector.js.map +1 -0
  101. package/dist/src/components/common/PageNavigation.d.ts +4 -0
  102. package/dist/src/components/common/PageNavigation.d.ts.map +1 -0
  103. package/dist/src/components/common/PageNavigation.js +46 -0
  104. package/dist/src/components/common/PageNavigation.js.map +1 -0
  105. package/dist/src/components/form/FilterSelect.css +3 -4
  106. package/dist/src/components/form/validations.d.ts +9 -0
  107. package/dist/src/components/form/validations.d.ts.map +1 -1
  108. package/dist/src/components/form/validations.js +12 -1
  109. package/dist/src/components/form/validations.js.map +1 -1
  110. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.d.ts +15 -0
  111. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.d.ts.map +1 -0
  112. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.js +56 -0
  113. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.js.map +1 -0
  114. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.d.ts +7 -0
  115. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.d.ts.map +1 -0
  116. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.js +265 -0
  117. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.js.map +1 -0
  118. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.d.ts +9 -0
  119. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.d.ts.map +1 -0
  120. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.js +27 -0
  121. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.js.map +1 -0
  122. package/dist/src/hooks/useAccessReview.d.ts.map +1 -1
  123. package/dist/src/hooks/useAccessReview.js +17 -4
  124. package/dist/src/hooks/useAccessReview.js.map +1 -1
  125. package/dist/src/hooks/useAlertsEnabled.d.ts +2 -0
  126. package/dist/src/hooks/useAlertsEnabled.d.ts.map +1 -0
  127. package/dist/src/hooks/useAlertsEnabled.js +50 -0
  128. package/dist/src/hooks/useAlertsEnabled.js.map +1 -0
  129. package/dist/src/hooks/useAppContext.d.ts +3 -6
  130. package/dist/src/hooks/useAppContext.d.ts.map +1 -1
  131. package/dist/src/hooks/useAppContext.js +1 -0
  132. package/dist/src/hooks/useAppContext.js.map +1 -1
  133. package/dist/src/hooks/useFetch.d.ts +6 -7
  134. package/dist/src/hooks/useFetch.d.ts.map +1 -1
  135. package/dist/src/hooks/useFetch.js +2 -3
  136. package/dist/src/hooks/useFetch.js.map +1 -1
  137. package/dist/src/hooks/useFetchPeriodically.d.ts.map +1 -1
  138. package/dist/src/hooks/useFetchPeriodically.js +4 -9
  139. package/dist/src/hooks/useFetchPeriodically.js.map +1 -1
  140. package/dist/src/hooks/useSystemRestoreContext.d.ts +16 -0
  141. package/dist/src/hooks/useSystemRestoreContext.d.ts.map +1 -0
  142. package/dist/src/hooks/useSystemRestoreContext.js +45 -0
  143. package/dist/src/hooks/useSystemRestoreContext.js.map +1 -0
  144. package/dist/src/types/extraTypes.d.ts +17 -18
  145. package/dist/src/types/extraTypes.d.ts.map +1 -1
  146. package/dist/src/types/extraTypes.js +1 -6
  147. package/dist/src/types/extraTypes.js.map +1 -1
  148. package/dist/src/types/rbac.d.ts +1 -0
  149. package/dist/src/types/rbac.d.ts.map +1 -1
  150. package/dist/src/types/rbac.js +1 -0
  151. package/dist/src/types/rbac.js.map +1 -1
  152. package/dist/src/utils/api.d.ts +2 -15
  153. package/dist/src/utils/api.d.ts.map +1 -1
  154. package/dist/src/utils/api.js +1 -40
  155. package/dist/src/utils/api.js.map +1 -1
  156. package/dist/src/utils/devices.d.ts +2 -0
  157. package/dist/src/utils/devices.d.ts.map +1 -1
  158. package/dist/src/utils/devices.js +11 -1
  159. package/dist/src/utils/devices.js.map +1 -1
  160. package/dist/src/utils/organizationStorage.d.ts +4 -0
  161. package/dist/src/utils/organizationStorage.d.ts.map +1 -0
  162. package/dist/src/utils/organizationStorage.js +18 -0
  163. package/dist/src/utils/organizationStorage.js.map +1 -0
  164. package/dist/src/utils/query.d.ts +2 -0
  165. package/dist/src/utils/query.d.ts.map +1 -1
  166. package/dist/src/utils/query.js +16 -0
  167. package/dist/src/utils/query.js.map +1 -1
  168. package/dist/src/utils/status/common.d.ts +1 -0
  169. package/dist/src/utils/status/common.d.ts.map +1 -1
  170. package/dist/src/utils/status/common.js.map +1 -1
  171. package/dist/src/utils/status/devices.d.ts +5 -0
  172. package/dist/src/utils/status/devices.d.ts.map +1 -1
  173. package/dist/src/utils/status/devices.js +44 -5
  174. package/dist/src/utils/status/devices.js.map +1 -1
  175. package/dist/src/utils/status/fleet.js +1 -1
  176. package/dist/src/utils/status/fleet.js.map +1 -1
  177. package/dist/src/utils/status/repository.js +1 -1
  178. package/dist/src/utils/status/repository.js.map +1 -1
  179. package/package.json +1 -1
  180. package/src/components/DetailsPage/DetailsPage.tsx +3 -0
  181. package/src/components/DetailsPage/DetailsPageActions.tsx +45 -0
  182. package/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx +57 -5
  183. package/src/components/Device/DeviceDetails/DeviceDetailsTab.tsx +0 -5
  184. package/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.tsx +11 -3
  185. package/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx +0 -2
  186. package/src/components/Device/DevicesPage/DecommissionedDevicesTable.tsx +0 -3
  187. package/src/components/Device/DevicesPage/DeviceToolbarFilters.tsx +5 -1
  188. package/src/components/Device/DevicesPage/DevicesPage.tsx +1 -0
  189. package/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx +15 -3
  190. package/src/components/Device/DevicesPage/EnrolledDevicesTable.tsx +11 -5
  191. package/src/components/Device/SystemdUnitsModal/TrackSystemdUnitsForm.tsx +1 -1
  192. package/src/components/Events/useEvents.ts +12 -0
  193. package/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx +1 -1
  194. package/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx +4 -5
  195. package/src/components/Fleet/FleetDetails/FleetDevicesCharts.tsx +9 -3
  196. package/src/components/Fleet/FleetDetails/FleetRestoreBanner.tsx +46 -0
  197. package/src/components/Fleet/FleetsPage.tsx +2 -0
  198. package/src/components/ListPage/ListPageActions.tsx +46 -3
  199. package/src/components/Masthead/CommandLineToolsPage.tsx +17 -14
  200. package/src/components/OverviewPage/Cards/Alerts/AlertsCard.tsx +19 -5
  201. package/src/components/OverviewPage/Cards/Status/DeviceStatusChart.tsx +8 -2
  202. package/src/components/OverviewPage/Overview.tsx +5 -2
  203. package/src/components/Status/StatusDisplay.tsx +32 -23
  204. package/src/components/SystemRestore/PendingSyncDevicesAlert.tsx +36 -0
  205. package/src/components/SystemRestore/SuspendedDevicesAlert.tsx +144 -0
  206. package/src/components/SystemRestore/SystemRestoreBanners.css +6 -0
  207. package/src/components/SystemRestore/SystemRestoreBanners.tsx +82 -0
  208. package/src/components/charts/utils.ts +1 -1
  209. package/src/components/common/OrganizationGuard.tsx +124 -0
  210. package/src/components/common/OrganizationSelector.tsx +192 -0
  211. package/src/components/common/PageNavigation.tsx +103 -0
  212. package/src/components/form/FilterSelect.css +3 -4
  213. package/src/components/form/validations.ts +14 -0
  214. package/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.tsx +114 -0
  215. package/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.tsx +465 -0
  216. package/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.tsx +55 -0
  217. package/src/hooks/useAccessReview.ts +20 -4
  218. package/src/hooks/useAlertsEnabled.ts +50 -0
  219. package/src/hooks/useAppContext.tsx +9 -7
  220. package/src/hooks/useFetch.ts +1 -3
  221. package/src/hooks/useFetchPeriodically.ts +4 -11
  222. package/src/hooks/useSystemRestoreContext.tsx +54 -0
  223. package/src/types/extraTypes.ts +17 -23
  224. package/src/types/rbac.ts +1 -0
  225. package/src/utils/api.ts +2 -51
  226. package/src/utils/devices.ts +11 -1
  227. package/src/utils/organizationStorage.ts +13 -0
  228. package/src/utils/query.ts +22 -0
  229. package/src/utils/status/common.ts +1 -0
  230. package/src/utils/status/devices.ts +49 -2
  231. package/src/utils/status/fleet.ts +1 -1
  232. package/src/utils/status/repository.ts +1 -1
  233. package/dist/src/hooks/useAlerts.d.ts +0 -26
  234. package/dist/src/hooks/useAlerts.d.ts.map +0 -1
  235. package/dist/src/hooks/useAlerts.js +0 -114
  236. package/dist/src/hooks/useAlerts.js.map +0 -1
  237. package/dist/src/utils/metrics.d.ts +0 -9
  238. package/dist/src/utils/metrics.d.ts.map +0 -1
  239. package/dist/src/utils/metrics.js +0 -48
  240. package/dist/src/utils/metrics.js.map +0 -1
  241. package/src/hooks/useAlerts.ts +0 -147
  242. package/src/utils/metrics.ts +0 -49
@@ -0,0 +1,465 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Alert,
4
+ Button,
5
+ FormGroup,
6
+ MenuToggle,
7
+ MenuToggleElement,
8
+ Modal,
9
+ ModalVariant,
10
+ Radio,
11
+ Select,
12
+ SelectList,
13
+ SelectOption,
14
+ Spinner,
15
+ Stack,
16
+ StackItem,
17
+ Text,
18
+ TextContent,
19
+ } from '@patternfly/react-core';
20
+ import { Trans } from 'react-i18next';
21
+ import { Formik, useFormikContext } from 'formik';
22
+ import { DeviceList, DeviceResumeRequest, DeviceResumeResponse, Fleet } from '@flightctl/types';
23
+
24
+ import { FlightCtlLabel } from '../../../../types/extraTypes';
25
+ import { useTranslation } from '../../../../hooks/useTranslation';
26
+ import { useFetch } from '../../../../hooks/useFetch';
27
+ import LabelsField from '../../../form/LabelsField';
28
+ import FlightCtlForm from '../../../form/FlightCtlForm';
29
+ import { createMassResumeValidationSchema } from '../../../form/validations';
30
+ import { getErrorMessage } from '../../../../utils/error';
31
+ import { commonQueries } from '../../../../utils/query';
32
+ import { getApiListCount } from '../../../../utils/api';
33
+ import { fromAPILabel, labelToExactApiMatchString } from '../../../../utils/labels';
34
+ import { useFleets } from '../../../Fleet/useFleets';
35
+ import ResumeAllDevicesConfirmationDialog from './ResumeAllDevicesConfirmationDialog';
36
+
37
+ // Adds an artificial delay to make sure that the user notices the count is refreshing.
38
+ // This is specially needed when users switch between modes, and the selection for the new mode is already valid.
39
+ const showSpinnerBriefly = () => new Promise((resolve) => setTimeout(resolve, 450));
40
+
41
+ type MassResumeFormValues = {
42
+ mode: SelectionMode;
43
+ fleetId: string;
44
+ labels: FlightCtlLabel[];
45
+ };
46
+
47
+ interface MassResumeDevicesModalProps {
48
+ onClose: (hasResumed?: boolean) => void;
49
+ }
50
+
51
+ enum SelectionMode {
52
+ FLEET = 'fleet',
53
+ LABELS = 'labels',
54
+ ALL = 'all',
55
+ }
56
+
57
+ const getSelectedFleetLabels = (fleets: Fleet[], fleetId: string) => {
58
+ const selectedFleet = fleets.find((fleet) => fleet.metadata.name === fleetId);
59
+ if (!selectedFleet) {
60
+ throw new Error('Selected fleet not found');
61
+ }
62
+ return fromAPILabel(selectedFleet.spec.selector?.matchLabels || {});
63
+ };
64
+
65
+ const MassResumeDevicesModalContent = ({ onClose }: MassResumeDevicesModalProps) => {
66
+ const { t } = useTranslation();
67
+ const { get, post } = useFetch();
68
+ const { values, setFieldValue, isValid, dirty } = useFormikContext<MassResumeFormValues>();
69
+
70
+ const { fleets, isLoading: fleetsLoading } = useFleets({});
71
+ const [isFleetListOpen, setIsFleetSelectOpen] = React.useState(false);
72
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
73
+ const [submitError, setSubmitError] = React.useState<string | undefined>(undefined);
74
+ const [showResumeAllConfirmation, setShowResumeAllConfirmation] = React.useState(false);
75
+
76
+ // Resume result state
77
+ const [resumedCount, setResumedCount] = React.useState<number | undefined>(undefined);
78
+
79
+ // Device count state
80
+ const [deviceCountNum, setDeviceCountNum] = React.useState<number>(0);
81
+ const [isCountLoading, setIsCountLoading] = React.useState(false);
82
+ const [countError, setCountError] = React.useState<string | null>(null);
83
+
84
+ const hasResumedAtLeastOne = resumedCount !== undefined && resumedCount > 0;
85
+ const hasResumedAllExpected = deviceCountNum > 0 && resumedCount === deviceCountNum;
86
+ const isSubmitEnabled =
87
+ (values.mode === SelectionMode.ALL || deviceCountNum > 0) &&
88
+ !isSubmitting &&
89
+ !isCountLoading &&
90
+ isValid &&
91
+ resumedCount === undefined;
92
+ const deviceCount = deviceCountNum.toString();
93
+
94
+ const loadMatchingDevicesCount = React.useCallback(
95
+ async (criteria: { fleetId?: string; labels?: FlightCtlLabel[]; all?: boolean }) => {
96
+ setIsCountLoading(true);
97
+ setCountError(null);
98
+ setDeviceCountNum(0);
99
+
100
+ try {
101
+ let queryEndpoint: string;
102
+
103
+ if (criteria.all) {
104
+ queryEndpoint = commonQueries.getAllSuspendedDevicesCount();
105
+ } else {
106
+ const fleetLabels = criteria.fleetId
107
+ ? getSelectedFleetLabels(fleets, criteria.fleetId)
108
+ : criteria.labels || [];
109
+ if (fleetLabels.length === 0) {
110
+ throw new Error('Invalid criteria: must provide either fleetId or labels');
111
+ }
112
+ queryEndpoint = commonQueries.getSuspendedDeviceCountByLabels(fleetLabels);
113
+ }
114
+
115
+ const deviceResult = await get<DeviceList>(queryEndpoint);
116
+ await showSpinnerBriefly();
117
+ setDeviceCountNum(getApiListCount(deviceResult) || 0);
118
+ } catch (error) {
119
+ await showSpinnerBriefly();
120
+ setCountError(t('Failed to obtain the number of matching devices'));
121
+ } finally {
122
+ setIsCountLoading(false);
123
+ }
124
+ },
125
+ [get, t, fleets],
126
+ );
127
+
128
+ const performResume = async () => {
129
+ setIsSubmitting(true);
130
+ setSubmitError(undefined);
131
+
132
+ try {
133
+ let labels: FlightCtlLabel[];
134
+
135
+ if (values.mode === SelectionMode.ALL) {
136
+ labels = [];
137
+ } else {
138
+ if (values.mode === SelectionMode.FLEET) {
139
+ labels = getSelectedFleetLabels(fleets, values.fleetId);
140
+ } else {
141
+ labels = values.labels;
142
+ }
143
+ // This shouldn't happen due to validations, but since an empty label selector would target all existing devices,
144
+ // wake sure the UI does't accidentally submit a request with an empty selector
145
+ const emptySelection = labels.every((label) => label.key === '');
146
+ if (labels.length === 0 || emptySelection) {
147
+ throw new Error('The current selection would target all devices.');
148
+ }
149
+ }
150
+
151
+ const resumeRequest: DeviceResumeRequest = {
152
+ labelSelector: labels.map((label) => labelToExactApiMatchString(label)).join(','),
153
+ };
154
+
155
+ const resumeResponse = await post<DeviceResumeRequest, DeviceResumeResponse>(
156
+ 'deviceactions/resume',
157
+ resumeRequest,
158
+ );
159
+ setResumedCount(resumeResponse.resumedDevices || 0);
160
+ } catch (error) {
161
+ setSubmitError(getErrorMessage(error));
162
+ } finally {
163
+ setIsSubmitting(false);
164
+ }
165
+ };
166
+
167
+ const handleResume = () => {
168
+ if (values.mode === SelectionMode.ALL) {
169
+ setShowResumeAllConfirmation(true);
170
+ } else {
171
+ performResume();
172
+ }
173
+ };
174
+
175
+ const handleSelectionModeChanged = (mode: SelectionMode) => {
176
+ setFieldValue('mode', mode);
177
+
178
+ if (mode === SelectionMode.ALL) {
179
+ // Load count for all suspended devices
180
+ loadMatchingDevicesCount({ all: true });
181
+ } else if (mode === SelectionMode.FLEET && values.fleetId) {
182
+ // If switching to a mode that already had a valid selection, we refresh the count
183
+ loadMatchingDevicesCount({ fleetId: values.fleetId });
184
+ } else if (mode === SelectionMode.LABELS && values.labels.length > 0) {
185
+ loadMatchingDevicesCount({ labels: values.labels });
186
+ } else {
187
+ // Clear the count if there isn't a valid selection
188
+ setDeviceCountNum(0);
189
+ setCountError(null);
190
+ setIsCountLoading(false);
191
+ }
192
+ };
193
+
194
+ const handleFleetSelected = (fleetId: string) => {
195
+ setFieldValue('fleetId', fleetId);
196
+ setIsFleetSelectOpen(false);
197
+
198
+ if (fleetId) {
199
+ loadMatchingDevicesCount({ fleetId });
200
+ } else {
201
+ setDeviceCountNum(0);
202
+ setCountError(null);
203
+ }
204
+ };
205
+
206
+ const handleLabelsChanged = (newLabels: FlightCtlLabel[], hasErrors: boolean) => {
207
+ if (hasErrors || newLabels.length === 0) {
208
+ setDeviceCountNum(0);
209
+ setCountError(null);
210
+ return;
211
+ } else if (newLabels.length > 0) {
212
+ loadMatchingDevicesCount({ labels: newLabels });
213
+ }
214
+ };
215
+
216
+ return (
217
+ <Modal
218
+ variant={ModalVariant.medium}
219
+ title={t('Resume devices')}
220
+ isOpen
221
+ onClose={() => onClose(hasResumedAtLeastOne)}
222
+ actions={[
223
+ <Button
224
+ key="resume"
225
+ variant="primary"
226
+ onClick={handleResume}
227
+ isLoading={isSubmitting}
228
+ isDisabled={!isSubmitEnabled}
229
+ >
230
+ {t('Resume selection')}
231
+ </Button>,
232
+ <Button key="cancel" variant="link" onClick={() => onClose(hasResumedAtLeastOne)} isDisabled={isSubmitting}>
233
+ {hasResumedAtLeastOne ? t('Close') : t('Cancel')}
234
+ </Button>,
235
+ ]}
236
+ >
237
+ <Stack hasGutter>
238
+ <FlightCtlForm>
239
+ <StackItem>
240
+ <TextContent>
241
+ <Text>
242
+ {t(
243
+ "Following a system restore, devices have been identified with configurations newer than the server's records. To prevent data loss, they have been suspended from receiving updates.",
244
+ )}
245
+ </Text>
246
+ </TextContent>
247
+ </StackItem>
248
+ <StackItem>
249
+ <TextContent>
250
+ <Text>{t('Choose the criteria to select the devices to resume')}:</Text>
251
+ </TextContent>
252
+ </StackItem>
253
+
254
+ <StackItem>
255
+ <FormGroup isRequired fieldId="selection-mode">
256
+ <Radio
257
+ label={t('Fleet')}
258
+ id="selectionModeFleet"
259
+ name="selectionMode"
260
+ isChecked={values.mode === SelectionMode.FLEET}
261
+ onChange={() => {
262
+ handleSelectionModeChanged(SelectionMode.FLEET);
263
+ }}
264
+ description={t('Resume all suspended devices associated with a given fleet')}
265
+ />
266
+ <Radio
267
+ label={t('Labels')}
268
+ id="selectionModeLabels"
269
+ name="selectionMode"
270
+ isChecked={values.mode === SelectionMode.LABELS}
271
+ onChange={() => {
272
+ handleSelectionModeChanged(SelectionMode.LABELS);
273
+ }}
274
+ description={t('Resume all suspended devices matching the specified labels')}
275
+ />
276
+ <Radio
277
+ label={t('All suspended devices')}
278
+ id="selectionModeAll"
279
+ name="selectionMode"
280
+ isChecked={values.mode === SelectionMode.ALL}
281
+ onChange={() => {
282
+ handleSelectionModeChanged(SelectionMode.ALL);
283
+ }}
284
+ description={t('Resume all suspended devices')}
285
+ />
286
+ </FormGroup>
287
+ </StackItem>
288
+
289
+ {values.mode === SelectionMode.FLEET && (
290
+ <StackItem>
291
+ <FormGroup label={t('Fleet')} isRequired fieldId="fleetId">
292
+ <Select
293
+ id="fleetSelection"
294
+ isOpen={isFleetListOpen}
295
+ selected={values.fleetId}
296
+ onSelect={(_, selection) => handleFleetSelected(selection as string)}
297
+ onOpenChange={(isOpen) => setIsFleetSelectOpen(isOpen)}
298
+ toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
299
+ <MenuToggle
300
+ ref={toggleRef}
301
+ onClick={() => setIsFleetSelectOpen(!isFleetListOpen)}
302
+ isExpanded={isFleetListOpen}
303
+ isDisabled={fleetsLoading}
304
+ style={{ width: '100%' }}
305
+ >
306
+ {values.fleetId || t('Select a fleet')}
307
+ </MenuToggle>
308
+ )}
309
+ shouldFocusToggleOnSelect
310
+ >
311
+ <SelectList>
312
+ {fleets.map((fleet) => {
313
+ const fleetId = fleet.metadata.name || '';
314
+ const deviceSelectorStr = Object.entries(fleet.spec?.selector?.matchLabels || {})
315
+ .map(([key, value]) => (value ? `${key}=${value}` : key))
316
+ .join(',');
317
+
318
+ let description = '';
319
+ if (deviceSelectorStr) {
320
+ description = `${t('Device selector labels')}: ${deviceSelectorStr}`;
321
+ } else {
322
+ description = t('This fleet does not select any devices');
323
+ }
324
+
325
+ return (
326
+ <SelectOption
327
+ key={fleetId}
328
+ value={fleetId}
329
+ description={description}
330
+ isDisabled={!deviceSelectorStr}
331
+ >
332
+ {fleetId}
333
+ </SelectOption>
334
+ );
335
+ })}
336
+ </SelectList>
337
+ </Select>
338
+ </FormGroup>
339
+ </StackItem>
340
+ )}
341
+
342
+ {values.mode === SelectionMode.LABELS && (
343
+ <StackItem>
344
+ <FormGroup label={t('Device labels')} fieldId="labelsSelection" isRequired>
345
+ <LabelsField name="labels" onChangeCallback={handleLabelsChanged} />
346
+ </FormGroup>
347
+ </StackItem>
348
+ )}
349
+ </FlightCtlForm>
350
+
351
+ {isValid && dirty && resumedCount === undefined && (
352
+ <StackItem>
353
+ {isCountLoading ? (
354
+ <Alert variant="info" isInline title={t('Refreshing device count')}>
355
+ <Spinner size="md" className="pf-v5-u-mr-sm" />
356
+ {t('Checking how many suspended devices match your criteria...')}
357
+ </Alert>
358
+ ) : countError ? (
359
+ <Alert variant="warning" isInline title={t('Unable to refresh device count')}>
360
+ {countError}
361
+ </Alert>
362
+ ) : deviceCountNum > 0 ? (
363
+ <Alert
364
+ variant={values.mode === SelectionMode.ALL ? 'warning' : 'success'}
365
+ isInline
366
+ title={t('Devices found')}
367
+ >
368
+ {values.mode === SelectionMode.FLEET ? (
369
+ <Trans t={t}>
370
+ <strong>{deviceCount}</strong> suspended devices are currently associated with fleet{' '}
371
+ <strong>{values.fleetId}</strong>.
372
+ </Trans>
373
+ ) : values.mode === SelectionMode.LABELS ? (
374
+ <Trans t={t}>
375
+ <strong>{deviceCount}</strong> suspended devices match the specified labels.
376
+ </Trans>
377
+ ) : (
378
+ <>
379
+ <Trans t={t} count={deviceCountNum}>
380
+ You are about to resume all <strong>{deviceCount}</strong> suspended devices.
381
+ </Trans>
382
+ {t(
383
+ 'This action is irreversible and will allow all affected devices to receive new configuration updates from the server.',
384
+ )}
385
+ </>
386
+ )}
387
+ </Alert>
388
+ ) : (
389
+ <Alert variant="warning" isInline title={t('No devices found')}>
390
+ {values.mode === SelectionMode.FLEET ? (
391
+ <Trans t={t}>
392
+ No suspended devices are associated with fleet <strong>{values.fleetId}</strong>.
393
+ </Trans>
394
+ ) : values.mode === SelectionMode.ALL ? (
395
+ t('No suspended devices found.')
396
+ ) : (
397
+ t('No suspended devices match the specified labels.')
398
+ )}
399
+ </Alert>
400
+ )}
401
+ </StackItem>
402
+ )}
403
+
404
+ {submitError && (
405
+ <StackItem>
406
+ <Alert isInline variant="danger" title={t('Resume devices failed')}>
407
+ {submitError}
408
+ </Alert>
409
+ </StackItem>
410
+ )}
411
+ {resumedCount !== undefined && hasResumedAllExpected && (
412
+ <StackItem>
413
+ <Alert isInline variant="success" title={t('Resume successful')}>
414
+ {t('{{ resumedCount }} devices were resumed', { resumedCount })}
415
+ </Alert>
416
+ </StackItem>
417
+ )}
418
+
419
+ {resumedCount !== undefined && !hasResumedAllExpected && (
420
+ <StackItem>
421
+ <Alert isInline variant="warning" title={t('Resumed with warnings')}>
422
+ {t('{{ expectedCount }} devices to resume, and {{ resumedCount }} resumed successfully', {
423
+ expectedCount: deviceCountNum,
424
+ resumedCount,
425
+ })}
426
+ </Alert>
427
+ </StackItem>
428
+ )}
429
+ </Stack>
430
+ {showResumeAllConfirmation && (
431
+ <ResumeAllDevicesConfirmationDialog
432
+ deviceCountNum={deviceCountNum}
433
+ onClose={(doConfirm) => {
434
+ setShowResumeAllConfirmation(false);
435
+ if (doConfirm) {
436
+ performResume();
437
+ }
438
+ }}
439
+ />
440
+ )}
441
+ </Modal>
442
+ );
443
+ };
444
+
445
+ const MassResumeDevicesModal = ({ onClose }: MassResumeDevicesModalProps) => {
446
+ const { t } = useTranslation();
447
+
448
+ return (
449
+ <Formik<MassResumeFormValues>
450
+ initialValues={{
451
+ mode: SelectionMode.FLEET,
452
+ fleetId: '',
453
+ labels: [],
454
+ }}
455
+ validationSchema={createMassResumeValidationSchema(t)}
456
+ onSubmit={() => {
457
+ // This will be handled by the inner component
458
+ }}
459
+ >
460
+ <MassResumeDevicesModalContent onClose={onClose} />
461
+ </Formik>
462
+ );
463
+ };
464
+
465
+ export default MassResumeDevicesModal;
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+ import { Button, Modal, ModalVariant, Stack, StackItem, Text, TextContent } from '@patternfly/react-core';
3
+ import { Trans } from 'react-i18next';
4
+
5
+ import { useTranslation } from '../../../../hooks/useTranslation';
6
+
7
+ interface ResumeAllDevicesConfirmationModalProps {
8
+ deviceCountNum: number;
9
+ onClose: (doConfirm: boolean) => void;
10
+ isSubmitting?: boolean;
11
+ }
12
+
13
+ const ResumeAllDevicesConfirmationModal = ({ deviceCountNum, onClose }: ResumeAllDevicesConfirmationModalProps) => {
14
+ const { t } = useTranslation();
15
+
16
+ const deviceCount = deviceCountNum.toString();
17
+
18
+ return (
19
+ <Modal
20
+ variant={ModalVariant.small}
21
+ title={t('Resume all {{ deviceCount }} devices?', { deviceCount })}
22
+ isOpen
23
+ onClose={() => onClose(false)}
24
+ actions={[
25
+ <Button key="confirm" variant="primary" onClick={() => onClose(true)}>
26
+ {t('Resume all devices')}
27
+ </Button>,
28
+ <Button key="cancel" variant="link" onClick={() => onClose(false)}>
29
+ {t('Cancel')}
30
+ </Button>,
31
+ ]}
32
+ >
33
+ <Stack hasGutter>
34
+ <StackItem>
35
+ <TextContent>
36
+ <Text>
37
+ <Trans t={t} count={deviceCountNum}>
38
+ You are about to resume all <strong>{deviceCount}</strong> suspended devices.
39
+ </Trans>
40
+ </Text>
41
+ </TextContent>
42
+ <TextContent>
43
+ <Text>
44
+ {t(
45
+ 'This action is irreversible and will allow all affected devices to receive new configuration updates from the server.',
46
+ )}
47
+ </Text>
48
+ </TextContent>
49
+ </StackItem>
50
+ </Stack>
51
+ </Modal>
52
+ );
53
+ };
54
+
55
+ export default ResumeAllDevicesConfirmationModal;
@@ -14,19 +14,35 @@ export const useAccessReview = (kind: RESOURCE, verb: VERB): AccessReviewResult
14
14
  fetch: { checkPermissions },
15
15
  } = useAppContext();
16
16
  React.useEffect(() => {
17
+ let isMounted = true;
18
+
17
19
  const doItAsync = async () => {
20
+ if (!isMounted) return;
21
+
18
22
  setIsLoading(true);
19
23
  try {
20
24
  const allowed = await checkPermissions(kind, verb);
21
- setIsAllowed(allowed);
25
+ if (isMounted) {
26
+ setIsAllowed(allowed);
27
+ }
22
28
  } catch (err) {
23
- setError(getErrorMessage(err));
24
- setIsAllowed(false);
29
+ if (isMounted) {
30
+ setError(getErrorMessage(err));
31
+ setIsAllowed(false);
32
+ }
25
33
  } finally {
26
- setIsLoading(false);
34
+ if (isMounted) {
35
+ setIsLoading(false);
36
+ }
27
37
  }
28
38
  };
39
+
29
40
  doItAsync();
41
+
42
+ // Cleanup function to prevent state updates after unmount
43
+ return () => {
44
+ isMounted = false;
45
+ };
30
46
  }, [kind, verb, checkPermissions]);
31
47
 
32
48
  return [isAllowed, isLoading, error];
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { RESOURCE, VERB } from '../types/rbac';
3
+ import { useAccessReview } from './useAccessReview';
4
+ import { useFetch } from './useFetch';
5
+
6
+ // Alerts are considered disabled if the service returns either 501 (Not Implemented) or 500
7
+ const isDisabledAlertManagerService = (error: Error): boolean =>
8
+ Number(error.message) === 501 || Number(error.message) === 500;
9
+
10
+ export const useAlertsEnabled = (): boolean => {
11
+ const { get } = useFetch();
12
+ const [alertsEnabled, setAlertsEnabled] = React.useState(false);
13
+
14
+ const [canListAlerts, alertsLoading] = useAccessReview(RESOURCE.ALERTS, VERB.LIST);
15
+
16
+ React.useEffect(() => {
17
+ let abortController: AbortController;
18
+
19
+ const checkAlertServiceEnabled = async () => {
20
+ try {
21
+ abortController = new AbortController();
22
+ await get('alerts', abortController.signal);
23
+ setAlertsEnabled(true);
24
+ } catch (err) {
25
+ if (!abortController.signal.aborted) {
26
+ if (isDisabledAlertManagerService(err as Error)) {
27
+ setAlertsEnabled(false);
28
+ } else {
29
+ // For other errors, assume alerts are enabled but there's a temporary issue
30
+ setAlertsEnabled(true);
31
+ }
32
+ }
33
+ }
34
+ };
35
+
36
+ if (!alertsLoading && canListAlerts) {
37
+ // Check only if we know that the user has permissions to read the alerts
38
+ checkAlertServiceEnabled();
39
+ } else {
40
+ // If user doesn't have permissions, set to disabled
41
+ setAlertsEnabled(false);
42
+ }
43
+
44
+ return () => {
45
+ abortController?.abort();
46
+ };
47
+ }, [get, alertsLoading, canListAlerts]);
48
+
49
+ return alertsEnabled;
50
+ };
@@ -13,7 +13,6 @@ import {
13
13
  useSearchParams,
14
14
  } from 'react-router-dom';
15
15
  import { PatchRequest } from '@flightctl/types';
16
- import { CliArtifactsResponse } from '@flightctl/ui-components/src/types/extraTypes';
17
16
  import { ROUTE } from './useNavigate';
18
17
  import { RESOURCE, VERB } from '../types/rbac';
19
18
 
@@ -71,16 +70,18 @@ export type AppContextProps = {
71
70
  fetch: {
72
71
  getWsEndpoint: (deviceId: string) => string;
73
72
  get: <R>(kind: string, abortSignal?: AbortSignal) => Promise<R>;
74
- post: <R>(kind: string, data: R, abortSignal?: AbortSignal) => Promise<R>;
75
- put: <R>(kind: string, data: R, abortSignal?: AbortSignal) => Promise<R>;
73
+ post: <TRequest, TResponse = TRequest>(
74
+ kind: string,
75
+ data: TRequest,
76
+ abortSignal?: AbortSignal,
77
+ ) => Promise<TResponse>;
78
+ put: <TRequest>(kind: string, data: TRequest, abortSignal?: AbortSignal) => Promise<TRequest>;
76
79
  remove: <R>(kind: string, abortSignal?: AbortSignal) => Promise<R>;
77
80
  patch: <R>(kind: string, patches: PatchRequest, abortSignal?: AbortSignal) => Promise<R>;
78
81
  checkPermissions: (resource: RESOURCE, verb: VERB) => Promise<boolean>;
82
+ // All methods to the UI proxy are handled in the same method - returns raw Response
83
+ proxyFetch: (endpoint: string, requestInit: RequestInit) => Promise<Response>;
79
84
  };
80
- // Extra fetch functions
81
- getAlerts?: <R>(abortSignal?: AbortSignal) => Promise<R>;
82
- getMetrics?: <R>(query: string, abortSignal?: AbortSignal) => Promise<R>;
83
- getCliArtifacts?: (abortSignal?: AbortSignal) => Promise<CliArtifactsResponse>;
84
85
  };
85
86
 
86
87
  export const AppContext = React.createContext<AppContextProps>({
@@ -113,6 +114,7 @@ export const AppContext = React.createContext<AppContextProps>({
113
114
  remove: async () => ({}) as any,
114
115
  patch: async () => ({}) as any,
115
116
  checkPermissions: async () => true,
117
+ proxyFetch: async () => ({}) as any,
116
118
  },
117
119
  /* eslint-enable */
118
120
  });
@@ -1,11 +1,9 @@
1
1
  import { useAppContext } from './useAppContext';
2
2
 
3
3
  export const useFetch = () => {
4
- const { fetch, getCliArtifacts, getMetrics } = useAppContext();
4
+ const { fetch } = useAppContext();
5
5
 
6
6
  return {
7
7
  ...fetch,
8
- getCliArtifacts,
9
- getMetrics,
10
8
  };
11
9
  };