@flightctl/ui-components 0.9.3 → 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 (239) 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/Events/useEvents.d.ts.map +1 -1
  39. package/dist/src/components/Events/useEvents.js +12 -0
  40. package/dist/src/components/Events/useEvents.js.map +1 -1
  41. package/dist/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.js +1 -1
  42. package/dist/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.js.map +1 -1
  43. package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.d.ts.map +1 -1
  44. package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.js +4 -3
  45. package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.js.map +1 -1
  46. package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.d.ts.map +1 -1
  47. package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.js +7 -1
  48. package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.js.map +1 -1
  49. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.d.ts +8 -0
  50. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.d.ts.map +1 -0
  51. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.js +36 -0
  52. package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.js.map +1 -0
  53. package/dist/src/components/Fleet/FleetsPage.d.ts.map +1 -1
  54. package/dist/src/components/Fleet/FleetsPage.js +2 -0
  55. package/dist/src/components/Fleet/FleetsPage.js.map +1 -1
  56. package/dist/src/components/ListPage/ListPageActions.d.ts +3 -2
  57. package/dist/src/components/ListPage/ListPageActions.d.ts.map +1 -1
  58. package/dist/src/components/ListPage/ListPageActions.js +27 -1
  59. package/dist/src/components/ListPage/ListPageActions.js.map +1 -1
  60. package/dist/src/components/Masthead/CommandLineToolsPage.d.ts.map +1 -1
  61. package/dist/src/components/Masthead/CommandLineToolsPage.js +18 -14
  62. package/dist/src/components/Masthead/CommandLineToolsPage.js.map +1 -1
  63. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.d.ts.map +1 -1
  64. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js +15 -5
  65. package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js.map +1 -1
  66. package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.d.ts.map +1 -1
  67. package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.js +7 -1
  68. package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.js.map +1 -1
  69. package/dist/src/components/OverviewPage/Overview.d.ts.map +1 -1
  70. package/dist/src/components/OverviewPage/Overview.js +4 -2
  71. package/dist/src/components/OverviewPage/Overview.js.map +1 -1
  72. package/dist/src/components/Status/StatusDisplay.d.ts +3 -1
  73. package/dist/src/components/Status/StatusDisplay.d.ts.map +1 -1
  74. package/dist/src/components/Status/StatusDisplay.js +8 -8
  75. package/dist/src/components/Status/StatusDisplay.js.map +1 -1
  76. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.d.ts +7 -0
  77. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.d.ts.map +1 -0
  78. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.js +22 -0
  79. package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.js.map +1 -0
  80. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.d.ts +16 -0
  81. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.d.ts.map +1 -0
  82. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.js +75 -0
  83. package/dist/src/components/SystemRestore/SuspendedDevicesAlert.js.map +1 -0
  84. package/dist/src/components/SystemRestore/SystemRestoreBanners.css +6 -0
  85. package/dist/src/components/SystemRestore/SystemRestoreBanners.d.ts +28 -0
  86. package/dist/src/components/SystemRestore/SystemRestoreBanners.d.ts.map +1 -0
  87. package/dist/src/components/SystemRestore/SystemRestoreBanners.js +38 -0
  88. package/dist/src/components/SystemRestore/SystemRestoreBanners.js.map +1 -0
  89. package/dist/src/components/charts/utils.js +1 -1
  90. package/dist/src/components/charts/utils.js.map +1 -1
  91. package/dist/src/components/common/OrganizationGuard.d.ts +13 -0
  92. package/dist/src/components/common/OrganizationGuard.d.ts.map +1 -0
  93. package/dist/src/components/common/OrganizationGuard.js +106 -0
  94. package/dist/src/components/common/OrganizationGuard.js.map +1 -0
  95. package/dist/src/components/common/OrganizationSelector.d.ts +8 -0
  96. package/dist/src/components/common/OrganizationSelector.d.ts.map +1 -0
  97. package/dist/src/components/common/OrganizationSelector.js +92 -0
  98. package/dist/src/components/common/OrganizationSelector.js.map +1 -0
  99. package/dist/src/components/common/PageNavigation.d.ts +4 -0
  100. package/dist/src/components/common/PageNavigation.d.ts.map +1 -0
  101. package/dist/src/components/common/PageNavigation.js +46 -0
  102. package/dist/src/components/common/PageNavigation.js.map +1 -0
  103. package/dist/src/components/form/FilterSelect.css +3 -4
  104. package/dist/src/components/form/validations.d.ts +9 -0
  105. package/dist/src/components/form/validations.d.ts.map +1 -1
  106. package/dist/src/components/form/validations.js +12 -1
  107. package/dist/src/components/form/validations.js.map +1 -1
  108. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.d.ts +15 -0
  109. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.d.ts.map +1 -0
  110. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.js +56 -0
  111. package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.js.map +1 -0
  112. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.d.ts +7 -0
  113. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.d.ts.map +1 -0
  114. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.js +265 -0
  115. package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.js.map +1 -0
  116. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.d.ts +9 -0
  117. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.d.ts.map +1 -0
  118. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.js +27 -0
  119. package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.js.map +1 -0
  120. package/dist/src/hooks/useAccessReview.d.ts.map +1 -1
  121. package/dist/src/hooks/useAccessReview.js +17 -4
  122. package/dist/src/hooks/useAccessReview.js.map +1 -1
  123. package/dist/src/hooks/useAlertsEnabled.d.ts +2 -0
  124. package/dist/src/hooks/useAlertsEnabled.d.ts.map +1 -0
  125. package/dist/src/hooks/useAlertsEnabled.js +50 -0
  126. package/dist/src/hooks/useAlertsEnabled.js.map +1 -0
  127. package/dist/src/hooks/useAppContext.d.ts +3 -6
  128. package/dist/src/hooks/useAppContext.d.ts.map +1 -1
  129. package/dist/src/hooks/useAppContext.js +1 -0
  130. package/dist/src/hooks/useAppContext.js.map +1 -1
  131. package/dist/src/hooks/useFetch.d.ts +6 -7
  132. package/dist/src/hooks/useFetch.d.ts.map +1 -1
  133. package/dist/src/hooks/useFetch.js +2 -3
  134. package/dist/src/hooks/useFetch.js.map +1 -1
  135. package/dist/src/hooks/useFetchPeriodically.d.ts.map +1 -1
  136. package/dist/src/hooks/useFetchPeriodically.js +4 -9
  137. package/dist/src/hooks/useFetchPeriodically.js.map +1 -1
  138. package/dist/src/hooks/useSystemRestoreContext.d.ts +16 -0
  139. package/dist/src/hooks/useSystemRestoreContext.d.ts.map +1 -0
  140. package/dist/src/hooks/useSystemRestoreContext.js +45 -0
  141. package/dist/src/hooks/useSystemRestoreContext.js.map +1 -0
  142. package/dist/src/types/extraTypes.d.ts +17 -18
  143. package/dist/src/types/extraTypes.d.ts.map +1 -1
  144. package/dist/src/types/extraTypes.js +1 -6
  145. package/dist/src/types/extraTypes.js.map +1 -1
  146. package/dist/src/types/rbac.d.ts +1 -0
  147. package/dist/src/types/rbac.d.ts.map +1 -1
  148. package/dist/src/types/rbac.js +1 -0
  149. package/dist/src/types/rbac.js.map +1 -1
  150. package/dist/src/utils/api.d.ts +2 -15
  151. package/dist/src/utils/api.d.ts.map +1 -1
  152. package/dist/src/utils/api.js +1 -40
  153. package/dist/src/utils/api.js.map +1 -1
  154. package/dist/src/utils/devices.d.ts +2 -0
  155. package/dist/src/utils/devices.d.ts.map +1 -1
  156. package/dist/src/utils/devices.js +11 -1
  157. package/dist/src/utils/devices.js.map +1 -1
  158. package/dist/src/utils/organizationStorage.d.ts +4 -0
  159. package/dist/src/utils/organizationStorage.d.ts.map +1 -0
  160. package/dist/src/utils/organizationStorage.js +18 -0
  161. package/dist/src/utils/organizationStorage.js.map +1 -0
  162. package/dist/src/utils/query.d.ts +2 -0
  163. package/dist/src/utils/query.d.ts.map +1 -1
  164. package/dist/src/utils/query.js +16 -0
  165. package/dist/src/utils/query.js.map +1 -1
  166. package/dist/src/utils/status/common.d.ts +1 -0
  167. package/dist/src/utils/status/common.d.ts.map +1 -1
  168. package/dist/src/utils/status/common.js.map +1 -1
  169. package/dist/src/utils/status/devices.d.ts +5 -0
  170. package/dist/src/utils/status/devices.d.ts.map +1 -1
  171. package/dist/src/utils/status/devices.js +44 -5
  172. package/dist/src/utils/status/devices.js.map +1 -1
  173. package/dist/src/utils/status/fleet.js +1 -1
  174. package/dist/src/utils/status/fleet.js.map +1 -1
  175. package/dist/src/utils/status/repository.js +1 -1
  176. package/dist/src/utils/status/repository.js.map +1 -1
  177. package/package.json +1 -1
  178. package/src/components/DetailsPage/DetailsPage.tsx +3 -0
  179. package/src/components/DetailsPage/DetailsPageActions.tsx +45 -0
  180. package/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx +57 -5
  181. package/src/components/Device/DeviceDetails/DeviceDetailsTab.tsx +0 -5
  182. package/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.tsx +11 -3
  183. package/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx +0 -2
  184. package/src/components/Device/DevicesPage/DecommissionedDevicesTable.tsx +0 -3
  185. package/src/components/Device/DevicesPage/DeviceToolbarFilters.tsx +5 -1
  186. package/src/components/Device/DevicesPage/DevicesPage.tsx +1 -0
  187. package/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx +15 -3
  188. package/src/components/Device/DevicesPage/EnrolledDevicesTable.tsx +11 -5
  189. package/src/components/Events/useEvents.ts +12 -0
  190. package/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx +1 -1
  191. package/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx +4 -5
  192. package/src/components/Fleet/FleetDetails/FleetDevicesCharts.tsx +9 -3
  193. package/src/components/Fleet/FleetDetails/FleetRestoreBanner.tsx +46 -0
  194. package/src/components/Fleet/FleetsPage.tsx +2 -0
  195. package/src/components/ListPage/ListPageActions.tsx +46 -3
  196. package/src/components/Masthead/CommandLineToolsPage.tsx +17 -14
  197. package/src/components/OverviewPage/Cards/Alerts/AlertsCard.tsx +19 -5
  198. package/src/components/OverviewPage/Cards/Status/DeviceStatusChart.tsx +8 -2
  199. package/src/components/OverviewPage/Overview.tsx +5 -2
  200. package/src/components/Status/StatusDisplay.tsx +32 -23
  201. package/src/components/SystemRestore/PendingSyncDevicesAlert.tsx +36 -0
  202. package/src/components/SystemRestore/SuspendedDevicesAlert.tsx +144 -0
  203. package/src/components/SystemRestore/SystemRestoreBanners.css +6 -0
  204. package/src/components/SystemRestore/SystemRestoreBanners.tsx +82 -0
  205. package/src/components/charts/utils.ts +1 -1
  206. package/src/components/common/OrganizationGuard.tsx +124 -0
  207. package/src/components/common/OrganizationSelector.tsx +192 -0
  208. package/src/components/common/PageNavigation.tsx +103 -0
  209. package/src/components/form/FilterSelect.css +3 -4
  210. package/src/components/form/validations.ts +14 -0
  211. package/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.tsx +114 -0
  212. package/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.tsx +465 -0
  213. package/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.tsx +55 -0
  214. package/src/hooks/useAccessReview.ts +20 -4
  215. package/src/hooks/useAlertsEnabled.ts +50 -0
  216. package/src/hooks/useAppContext.tsx +9 -7
  217. package/src/hooks/useFetch.ts +1 -3
  218. package/src/hooks/useFetchPeriodically.ts +4 -11
  219. package/src/hooks/useSystemRestoreContext.tsx +54 -0
  220. package/src/types/extraTypes.ts +17 -23
  221. package/src/types/rbac.ts +1 -0
  222. package/src/utils/api.ts +2 -51
  223. package/src/utils/devices.ts +11 -1
  224. package/src/utils/organizationStorage.ts +13 -0
  225. package/src/utils/query.ts +22 -0
  226. package/src/utils/status/common.ts +1 -0
  227. package/src/utils/status/devices.ts +49 -2
  228. package/src/utils/status/fleet.ts +1 -1
  229. package/src/utils/status/repository.ts +1 -1
  230. package/dist/src/hooks/useAlerts.d.ts +0 -26
  231. package/dist/src/hooks/useAlerts.d.ts.map +0 -1
  232. package/dist/src/hooks/useAlerts.js +0 -114
  233. package/dist/src/hooks/useAlerts.js.map +0 -1
  234. package/dist/src/utils/metrics.d.ts +0 -9
  235. package/dist/src/utils/metrics.d.ts.map +0 -1
  236. package/dist/src/utils/metrics.js +0 -48
  237. package/dist/src/utils/metrics.js.map +0 -1
  238. package/src/hooks/useAlerts.ts +0 -147
  239. package/src/utils/metrics.ts +0 -49
@@ -0,0 +1,82 @@
1
+ import * as React from 'react';
2
+ import { DeviceResumeRequest, DeviceSummaryStatusType, DevicesSummary } from '@flightctl/types';
3
+ import { useSystemRestore } from '../../hooks/useSystemRestoreContext';
4
+ import SuspendedDevicesAlert, { ResumeMode } from './SuspendedDevicesAlert';
5
+ import { PendingSyncDevicesAlert } from './PendingSyncDevicesAlert';
6
+
7
+ import './SystemRestoreBanners.css';
8
+
9
+ interface SystemRestoreBannersProps {
10
+ mode: ResumeMode;
11
+ // Allows the banner to have an extra resume action for a subset of devices
12
+ resumeAction?: {
13
+ actionText: string;
14
+ title: React.ReactNode;
15
+ requestSelector: DeviceResumeRequest;
16
+ };
17
+ summaryStatus?: DevicesSummary['summaryStatus'];
18
+ onResumeComplete?: VoidFunction;
19
+ className?: string;
20
+ }
21
+
22
+ /**
23
+ * Uses the received devicesSummary to show the pending sync and suspended devices banners
24
+ */
25
+ export const SystemRestoreBanners = ({
26
+ mode,
27
+ resumeAction,
28
+ summaryStatus,
29
+ onResumeComplete,
30
+ className,
31
+ }: SystemRestoreBannersProps) => {
32
+ const pendingSyncCount = summaryStatus?.[DeviceSummaryStatusType.DeviceSummaryStatusAwaitingReconnect] || 0;
33
+ const suspendedCount = summaryStatus?.[DeviceSummaryStatusType.DeviceSummaryStatusConflictPaused] || 0;
34
+
35
+ const showPendingSyncBanner = pendingSyncCount > 0;
36
+ const showSuspendedBanner = suspendedCount > 0;
37
+
38
+ if (!showPendingSyncBanner && !showSuspendedBanner) {
39
+ return null;
40
+ }
41
+
42
+ return (
43
+ <div className={`fctl-system-restore-banners ${className}`}>
44
+ {showPendingSyncBanner && <PendingSyncDevicesAlert forSingleDevice={mode === 'device'} />}
45
+
46
+ {showSuspendedBanner && (
47
+ <SuspendedDevicesAlert
48
+ mode={mode}
49
+ suspendedCount={suspendedCount}
50
+ extraAction={resumeAction}
51
+ onResumeComplete={onResumeComplete}
52
+ />
53
+ )}
54
+ </div>
55
+ );
56
+ };
57
+
58
+ /**
59
+ * Shows the banners related to device summary data based on all existing devices
60
+ */
61
+ export const GlobalSystemRestoreBanners = ({
62
+ onResumeComplete,
63
+ className,
64
+ }: {
65
+ onResumeComplete?: VoidFunction;
66
+ className?: string;
67
+ }) => {
68
+ const { summaryStatus, isLoading } = useSystemRestore();
69
+
70
+ if (isLoading) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <SystemRestoreBanners
76
+ mode="global"
77
+ summaryStatus={summaryStatus}
78
+ onResumeComplete={onResumeComplete}
79
+ className={className}
80
+ />
81
+ );
82
+ };
@@ -27,7 +27,7 @@ export const toChartData = <T extends string>(
27
27
  return {
28
28
  x: `${statusItem.label}`,
29
29
  y: entryIndex === -1 ? 0 : percentages[entryIndex],
30
- color: getDefaultStatusColor(statusItem.level),
30
+ color: statusItem.customColor || getDefaultStatusColor(statusItem.level),
31
31
  link: {
32
32
  to: ROUTE.DEVICES as Route,
33
33
  query: query.toString(),
@@ -0,0 +1,124 @@
1
+ import * as React from 'react';
2
+ import { Organization, OrganizationList } from '@flightctl/types';
3
+ import { useAppContext } from '../../hooks/useAppContext';
4
+ import { getErrorMessage } from '../../utils/error';
5
+ import { getCurrentOrganizationId, storeCurrentOrganizationId } from '../../utils/organizationStorage';
6
+
7
+ interface OrganizationContextType {
8
+ currentOrganization?: Organization;
9
+ availableOrganizations: Organization[];
10
+ isOrganizationSelectionRequired: boolean;
11
+ selectOrganization: (org: Organization) => void;
12
+ selectionError?: string;
13
+ }
14
+
15
+ const OrganizationContext = React.createContext<OrganizationContextType | null>(null);
16
+
17
+ export const useOrganizationGuardContext = (): OrganizationContextType => {
18
+ const context = React.useContext(OrganizationContext);
19
+ if (!context) {
20
+ throw new Error('useOrganizationGuardContext must be used within OrganizationGuard');
21
+ }
22
+ return context;
23
+ };
24
+
25
+ const OrganizationGuard = ({ children }: React.PropsWithChildren) => {
26
+ const { fetch } = useAppContext();
27
+ const proxyFetch = fetch.proxyFetch;
28
+
29
+ const [isOrganizationsEnabled, setIsOrganizationsEnabled] = React.useState<boolean | null>(null); // null = loading
30
+ const [currentOrganization, setCurrentOrganization] = React.useState<Organization | undefined>();
31
+ const [availableOrganizations, setAvailableOrganizations] = React.useState<Organization[]>([]);
32
+ const [organizationsLoaded, setOrganizationsLoaded] = React.useState(false);
33
+ const [selectionError, setSelectionError] = React.useState<string | undefined>();
34
+
35
+ const selectOrganization = React.useCallback((org: Organization) => {
36
+ const organizationId = org.metadata?.name || '';
37
+
38
+ try {
39
+ // Store organization in localStorage - headers will handle the rest
40
+ storeCurrentOrganizationId(organizationId);
41
+ setCurrentOrganization(org);
42
+ } catch (error) {
43
+ setSelectionError(getErrorMessage(error));
44
+ }
45
+ }, []);
46
+
47
+ // Determine if multi-orgs are enabled. If so, check if an organization is already selected
48
+ React.useEffect(() => {
49
+ const initializeOrganizations = async () => {
50
+ try {
51
+ // First, check if organizations are enabled via proxy endpoint
52
+ const organizationsEnabledResponse = await proxyFetch('organizations-enabled', {
53
+ method: 'GET',
54
+ headers: { Accept: 'application/json' },
55
+ credentials: 'include',
56
+ });
57
+
58
+ const organizationsEnabled = organizationsEnabledResponse.status !== 501;
59
+ setIsOrganizationsEnabled(organizationsEnabled);
60
+
61
+ if (!organizationsEnabled) {
62
+ return;
63
+ }
64
+
65
+ // Organizations are enabled - load available organizations
66
+ const organizations = await fetch.get<OrganizationList>('organizations');
67
+ setAvailableOrganizations(organizations.items);
68
+
69
+ const currentOrgId = getCurrentOrganizationId();
70
+
71
+ // Validate current organization against available organizations
72
+ const currentOrg = currentOrgId
73
+ ? organizations.items.find((org) => org.metadata?.name === currentOrgId)
74
+ : undefined;
75
+
76
+ if (currentOrg) {
77
+ // The previously selected organization exists - use it
78
+ selectOrganization(currentOrg);
79
+ } else {
80
+ if (organizations.items?.length === 1) {
81
+ // Only one organization available - select it automatically
82
+ selectOrganization(organizations.items[0]);
83
+ } else if (currentOrgId) {
84
+ // Previously set organization does not exist anymore - remove it from localStorage so the user can select a new organization
85
+ setCurrentOrganization(undefined);
86
+ storeCurrentOrganizationId('');
87
+ }
88
+ }
89
+ } catch (error) {
90
+ setSelectionError(getErrorMessage(error));
91
+ setIsOrganizationsEnabled(false);
92
+ setAvailableOrganizations([]);
93
+ } finally {
94
+ setOrganizationsLoaded(true);
95
+ }
96
+ };
97
+
98
+ void initializeOrganizations();
99
+ }, [fetch, proxyFetch, selectOrganization]);
100
+
101
+ const isOrganizationSelectionRequired = React.useMemo(() => {
102
+ // Don't show selector while still loading
103
+ if (isOrganizationsEnabled === null || !organizationsLoaded) {
104
+ return false;
105
+ }
106
+
107
+ return isOrganizationsEnabled && availableOrganizations.length > 1 && !currentOrganization;
108
+ }, [isOrganizationsEnabled, organizationsLoaded, availableOrganizations.length, currentOrganization]);
109
+
110
+ const contextValue = React.useMemo(
111
+ () => ({
112
+ currentOrganization,
113
+ availableOrganizations,
114
+ isOrganizationSelectionRequired,
115
+ selectOrganization,
116
+ selectionError,
117
+ }),
118
+ [currentOrganization, availableOrganizations, isOrganizationSelectionRequired, selectOrganization, selectionError],
119
+ );
120
+
121
+ return <OrganizationContext.Provider value={contextValue}>{children}</OrganizationContext.Provider>;
122
+ };
123
+
124
+ export default OrganizationGuard;
@@ -0,0 +1,192 @@
1
+ import * as React from 'react';
2
+ import {
3
+ ActionList,
4
+ ActionListGroup,
5
+ ActionListItem,
6
+ Bullseye,
7
+ Button,
8
+ Card,
9
+ CardBody,
10
+ CardTitle,
11
+ Flex,
12
+ FlexItem,
13
+ Menu,
14
+ MenuContent,
15
+ MenuItem,
16
+ MenuList,
17
+ PageSection,
18
+ Stack,
19
+ StackItem,
20
+ Text,
21
+ TextContent,
22
+ Title,
23
+ } from '@patternfly/react-core';
24
+ import { Modal, ModalBody, ModalHeader } from '@patternfly/react-core/next';
25
+
26
+ import { Organization } from '@flightctl/types';
27
+ import { useTranslation } from '../../hooks/useTranslation';
28
+ import { useOrganizationGuardContext } from './OrganizationGuard';
29
+ import { ORGANIZATION_STORAGE_KEY } from '../../utils/organizationStorage';
30
+
31
+ interface OrganizationSelectorContentProps {
32
+ defaultOrganizationId?: string;
33
+ organizations: Organization[];
34
+ onSelect: (orgId: string) => void;
35
+ onCancel?: () => void;
36
+ allowCancel?: boolean;
37
+ isFirstLogin?: boolean;
38
+ }
39
+
40
+ const MAX_ORGANIZATIONS_FOR_SCROLL = 4;
41
+
42
+ const OrganizationSelectorContent = ({
43
+ defaultOrganizationId,
44
+ organizations,
45
+ onSelect,
46
+ onCancel,
47
+ allowCancel = false,
48
+ isFirstLogin = false,
49
+ }: OrganizationSelectorContentProps) => {
50
+ const { t } = useTranslation();
51
+ const [selectedOrg, setSelectedOrg] = React.useState<string | undefined>(defaultOrganizationId);
52
+ const needsScroll = organizations.length > MAX_ORGANIZATIONS_FOR_SCROLL;
53
+
54
+ return (
55
+ <Stack hasGutter>
56
+ <StackItem>
57
+ <TextContent>
58
+ <Text>
59
+ {isFirstLogin
60
+ ? t('You have access to multiple organizations. Please select one to continue.')
61
+ : t('Please select an organization to continue. This will refresh the application.')}
62
+ </Text>
63
+ </TextContent>
64
+ </StackItem>
65
+ <StackItem>
66
+ <Menu
67
+ activeItemId={selectedOrg}
68
+ selected={selectedOrg}
69
+ onSelect={(_ev, orgId) => setSelectedOrg(orgId as string)}
70
+ isScrollable={needsScroll}
71
+ >
72
+ <MenuContent menuHeight={needsScroll ? '230px' : 'auto'}>
73
+ <MenuList>
74
+ {organizations.map((org) => {
75
+ const orgId = org.metadata?.name as string;
76
+ return (
77
+ <MenuItem itemId={orgId} key={orgId}>
78
+ {org.spec?.displayName || orgId}
79
+ </MenuItem>
80
+ );
81
+ })}
82
+ </MenuList>
83
+ </MenuContent>
84
+ </Menu>
85
+ </StackItem>
86
+ <StackItem>
87
+ <ActionList>
88
+ <ActionListGroup>
89
+ <ActionListItem>
90
+ <Button variant="primary" onClick={() => onSelect(selectedOrg as string)} isDisabled={!selectedOrg}>
91
+ {t('Continue')}
92
+ </Button>
93
+ </ActionListItem>
94
+ {allowCancel && (
95
+ <ActionListItem>
96
+ <Button variant="link" onClick={onCancel}>
97
+ {t('Cancel')}
98
+ </Button>
99
+ </ActionListItem>
100
+ )}
101
+ </ActionListGroup>
102
+ </ActionList>
103
+ </StackItem>
104
+ </Stack>
105
+ );
106
+ };
107
+
108
+ const OrganizationSelectorCustomModal = (props: OrganizationSelectorContentProps) => {
109
+ const { t } = useTranslation();
110
+
111
+ return (
112
+ <PageSection variant="light">
113
+ <Bullseye>
114
+ <Card isLarge>
115
+ <CardTitle>
116
+ <Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
117
+ <FlexItem>
118
+ <Title headingLevel="h1" size="2xl">
119
+ {t('Select Organization')}
120
+ </Title>
121
+ </FlexItem>
122
+ </Flex>
123
+ </CardTitle>
124
+ <CardBody>
125
+ <OrganizationSelectorContent {...props} isFirstLogin />
126
+ </CardBody>
127
+ </Card>
128
+ </Bullseye>
129
+ </PageSection>
130
+ );
131
+ };
132
+
133
+ interface OrganizationSelectorProps {
134
+ onClose?: (isChanged: boolean) => void;
135
+ isFirstLogin: boolean;
136
+ }
137
+
138
+ const OrganizationSelector = ({ onClose, isFirstLogin = true }: OrganizationSelectorProps) => {
139
+ const { availableOrganizations, selectOrganization, isOrganizationSelectionRequired } = useOrganizationGuardContext();
140
+ const { t } = useTranslation();
141
+
142
+ const getLastSelectedOrganization = React.useCallback(() => {
143
+ try {
144
+ const savedOrgId = localStorage.getItem(ORGANIZATION_STORAGE_KEY);
145
+ if (savedOrgId && availableOrganizations.some((org) => org.metadata?.name === savedOrgId)) {
146
+ return savedOrgId;
147
+ }
148
+ } catch (error) {
149
+ // Ignore - we won't preselect the previously selected organization
150
+ }
151
+ return undefined;
152
+ }, [availableOrganizations]);
153
+
154
+ const handleSelect = React.useCallback(
155
+ (orgId: string) => {
156
+ const org = availableOrganizations.find((org) => org.metadata?.name === orgId);
157
+ if (org) {
158
+ try {
159
+ selectOrganization(org);
160
+ onClose?.(true);
161
+ } catch (error) {
162
+ onClose?.(false);
163
+ }
164
+ }
165
+ },
166
+ [availableOrganizations, selectOrganization, onClose],
167
+ );
168
+
169
+ const commonProps = {
170
+ defaultOrganizationId: getLastSelectedOrganization(),
171
+ organizations: availableOrganizations,
172
+ onSelect: handleSelect,
173
+ onCancel: () => onClose?.(false),
174
+ allowCancel: !isOrganizationSelectionRequired,
175
+ };
176
+
177
+ // The modal for selecting an organization can be displayed in two ways:
178
+ // If the user has not yet selected an organization - the modal is not dismissable and it's a custom "Modal" which allows interacting with the user menu.
179
+ // If the user already logged in and selected an organization - the modal is dismissable and it overlays the entire page.
180
+ return isFirstLogin ? (
181
+ <OrganizationSelectorCustomModal {...commonProps} />
182
+ ) : (
183
+ <Modal variant="medium" isOpen onClose={() => onClose?.(false)}>
184
+ <ModalHeader title={t('Select Organization')} />
185
+ <ModalBody>
186
+ <OrganizationSelectorContent {...commonProps} isFirstLogin={false} />
187
+ </ModalBody>
188
+ </Modal>
189
+ );
190
+ };
191
+
192
+ export default OrganizationSelector;
@@ -0,0 +1,103 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Dropdown,
4
+ DropdownItem,
5
+ DropdownList,
6
+ MenuToggle,
7
+ MenuToggleElement,
8
+ PageSection,
9
+ Toolbar,
10
+ ToolbarContent,
11
+ ToolbarItem,
12
+ } from '@patternfly/react-core';
13
+ import { useTranslation } from '../../hooks/useTranslation';
14
+ import { useOrganizationGuardContext } from './OrganizationGuard';
15
+ import OrganizationSelector from './OrganizationSelector';
16
+
17
+ type OrganizationDropdownProps = {
18
+ organizationName?: string;
19
+ onSwitchOrganization: () => void;
20
+ };
21
+
22
+ const OrganizationDropdown = ({ organizationName, onSwitchOrganization }: OrganizationDropdownProps) => {
23
+ const { t } = useTranslation();
24
+ const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
25
+
26
+ const onDropdownToggle = () => {
27
+ setIsDropdownOpen(!isDropdownOpen);
28
+ };
29
+
30
+ return (
31
+ <Dropdown
32
+ isOpen={isDropdownOpen}
33
+ onSelect={onDropdownToggle}
34
+ onOpenChange={setIsDropdownOpen}
35
+ toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
36
+ <MenuToggle
37
+ ref={toggleRef}
38
+ onClick={onDropdownToggle}
39
+ id="organizationMenu"
40
+ isFullHeight
41
+ isExpanded={isDropdownOpen}
42
+ variant="plainText"
43
+ >
44
+ {organizationName}
45
+ </MenuToggle>
46
+ )}
47
+ >
48
+ <DropdownList>
49
+ <DropdownItem onClick={onSwitchOrganization}>{t('Change Organization')}</DropdownItem>
50
+ </DropdownList>
51
+ </Dropdown>
52
+ );
53
+ };
54
+
55
+ const PageNavigation = ({ children }: React.PropsWithChildren) => {
56
+ const { currentOrganization, availableOrganizations } = useOrganizationGuardContext();
57
+ const [showOrganizationModal, setShowOrganizationModal] = React.useState(false);
58
+
59
+ const showOrganizationSelection = availableOrganizations.length > 1;
60
+ const hasChildren = React.Children.count(children) > 0;
61
+
62
+ if (!showOrganizationSelection && !hasChildren) {
63
+ return null;
64
+ }
65
+
66
+ const currentOrgDisplayName = currentOrganization?.spec?.displayName || currentOrganization?.metadata?.name || '';
67
+
68
+ return (
69
+ <>
70
+ <PageSection variant="light" padding={{ default: 'noPadding' }}>
71
+ <Toolbar isFullHeight isStatic className="fctl-app_toolbar">
72
+ <ToolbarContent>
73
+ {hasChildren && <ToolbarItem>{children}</ToolbarItem>}
74
+ {showOrganizationSelection && (
75
+ <ToolbarItem>
76
+ <OrganizationDropdown
77
+ organizationName={currentOrgDisplayName}
78
+ onSwitchOrganization={() => {
79
+ setShowOrganizationModal(true);
80
+ }}
81
+ />
82
+ </ToolbarItem>
83
+ )}
84
+ </ToolbarContent>
85
+ </Toolbar>
86
+ </PageSection>
87
+
88
+ {showOrganizationModal && (
89
+ <OrganizationSelector
90
+ isFirstLogin={false}
91
+ onClose={(isChanged) => {
92
+ setShowOrganizationModal(false);
93
+ if (isChanged) {
94
+ window.location.reload();
95
+ }
96
+ }}
97
+ />
98
+ )}
99
+ </>
100
+ );
101
+ };
102
+
103
+ export default PageNavigation;
@@ -1,6 +1,5 @@
1
1
  .fctl-filter-select__group {
2
- height: 18rem;
3
- overflow: auto;
2
+ overflow: auto;
4
3
  }
5
4
 
6
5
  .fctl-toggle-content {
@@ -8,11 +7,11 @@
8
7
  }
9
8
 
10
9
  .fctl-toggle-content__badge {
11
- margin-left: var(--pf-v5-global--spacer--sm);
10
+ margin-left: var(--pf-v5-global--spacer--sm);
12
11
  }
13
12
 
14
13
  .fctl-toggle-content__loader {
15
14
  position: absolute;
16
15
  top: 0;
17
16
  right: 2.5rem;
18
- }
17
+ }
@@ -695,3 +695,17 @@ export const deviceApprovalValidationSchema = (t: TFunction, conf: { isSingleDev
695
695
  ),
696
696
  labels: validLabelsSchema(t, forbiddenDeviceLabels),
697
697
  });
698
+
699
+ export const createMassResumeValidationSchema = (t: TFunction) =>
700
+ Yup.object().shape({
701
+ mode: Yup.string().oneOf(['fleet', 'labels', 'all']).required(),
702
+ fleetId: Yup.string().when('mode', ([mode]) =>
703
+ mode === 'fleet' ? Yup.string().required(t('Fleet selection is required')) : Yup.string(),
704
+ ),
705
+ labels: Yup.array().when('mode', ([mode]) => {
706
+ if (mode === 'labels') {
707
+ return validLabelsSchema(t).min(1, t('At least one label is required'));
708
+ }
709
+ return Yup.array();
710
+ }),
711
+ });
@@ -0,0 +1,114 @@
1
+ import * as React from 'react';
2
+ import { Alert, Button, Modal, ModalVariant, Stack, StackItem, Text, TextContent } from '@patternfly/react-core';
3
+ import { DeviceResumeRequest, DeviceResumeResponse } from '@flightctl/types';
4
+
5
+ import { useTranslation } from '../../../hooks/useTranslation';
6
+ import { useFetch } from '../../../hooks/useFetch';
7
+ import { getErrorMessage } from '../../../utils/error';
8
+
9
+ interface ResumeDeviceModalProps {
10
+ mode: 'device' | 'fleet';
11
+ title: React.ReactNode;
12
+ selector: DeviceResumeRequest;
13
+ expectedCount: number;
14
+ onClose: (hasResumed: boolean | undefined) => void;
15
+ }
16
+
17
+ /**
18
+ * Modal to resume an individual device, or all the devices in a fleet
19
+ */
20
+ const ResumeDevicesModal = ({ mode, title, selector, expectedCount, onClose }: ResumeDeviceModalProps) => {
21
+ const { t } = useTranslation();
22
+ const { post } = useFetch();
23
+ const [resumedCount, setResumedCount] = React.useState<number | undefined>(undefined);
24
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
25
+ const [submitError, setSubmitError] = React.useState<string | undefined>(undefined);
26
+ const hasResumed = Boolean(resumedCount && resumedCount > 0);
27
+
28
+ const pluralCount = mode === 'device' ? 1 : 2; // Used to generate translations for one/multiple cases
29
+
30
+ const handleConfirm = async () => {
31
+ setIsSubmitting(true);
32
+ setSubmitError(undefined);
33
+
34
+ try {
35
+ const resumeResponse = await post<DeviceResumeRequest, DeviceResumeResponse>('deviceactions/resume', selector);
36
+ setResumedCount(resumeResponse.resumedDevices || 0);
37
+ } catch (error) {
38
+ setSubmitError(getErrorMessage(error));
39
+ } finally {
40
+ setIsSubmitting(false);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <Modal
46
+ variant={ModalVariant.small}
47
+ title={t('Resume devices?', { count: pluralCount })}
48
+ isOpen
49
+ onClose={() => onClose(hasResumed)}
50
+ actions={[
51
+ <Button
52
+ key="confirm"
53
+ variant="primary"
54
+ onClick={handleConfirm}
55
+ isLoading={isSubmitting}
56
+ isDisabled={isSubmitting || resumedCount !== undefined}
57
+ >
58
+ {mode === 'device' ? t('Resume') : t('Resume all')}
59
+ </Button>,
60
+ <Button key="cancel" variant="link" onClick={() => onClose(hasResumed)} isDisabled={isSubmitting}>
61
+ {hasResumed ? t('Close') : t('Cancel')}
62
+ </Button>,
63
+ ]}
64
+ >
65
+ <Stack hasGutter>
66
+ <StackItem>
67
+ <TextContent>
68
+ <Text>{title}</Text>
69
+ </TextContent>
70
+ </StackItem>
71
+
72
+ <StackItem>
73
+ <TextContent>
74
+ <Text>
75
+ {t(
76
+ "This action will resolve the configuration conflict and allow the devices to receive new updates from the server. This action is irreversible, please ensure the devices' assigned configuration is correct before proceeding.",
77
+ { count: pluralCount },
78
+ )}
79
+ </Text>
80
+ </TextContent>
81
+ </StackItem>
82
+
83
+ {submitError && (
84
+ <StackItem>
85
+ <Alert isInline variant="danger" title={t('Resuming devices failed', { count: pluralCount })}>
86
+ {submitError}
87
+ </Alert>
88
+ </StackItem>
89
+ )}
90
+
91
+ {resumedCount === expectedCount && (
92
+ <StackItem>
93
+ <Alert isInline variant="success" title={t('Resume successful')}>
94
+ {t('All {{ resumedCount }} devices resumed successfully', { resumedCount })}
95
+ </Alert>
96
+ </StackItem>
97
+ )}
98
+
99
+ {resumedCount !== undefined && resumedCount !== expectedCount && (
100
+ <StackItem>
101
+ <Alert isInline variant="warning" title={t('Resume with warnings')}>
102
+ {t('{{ expectedCount }} devices to resume, and {{ resumedCount }} resumed successfully', {
103
+ expectedCount,
104
+ resumedCount,
105
+ })}
106
+ </Alert>
107
+ </StackItem>
108
+ )}
109
+ </Stack>
110
+ </Modal>
111
+ );
112
+ };
113
+
114
+ export default ResumeDevicesModal;