@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.
- package/dist/src/components/DetailsPage/DetailsPage.d.ts +2 -1
- package/dist/src/components/DetailsPage/DetailsPage.d.ts.map +1 -1
- package/dist/src/components/DetailsPage/DetailsPage.js +2 -1
- package/dist/src/components/DetailsPage/DetailsPage.js.map +1 -1
- package/dist/src/components/DetailsPage/DetailsPageActions.d.ts +10 -0
- package/dist/src/components/DetailsPage/DetailsPageActions.d.ts.map +1 -1
- package/dist/src/components/DetailsPage/DetailsPageActions.js +23 -1
- package/dist/src/components/DetailsPage/DetailsPageActions.js.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.d.ts.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.js +29 -3
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.js.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTab.d.ts.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTab.js +0 -4
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTab.js.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.d.ts.map +1 -1
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js +8 -2
- package/dist/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.js.map +1 -1
- package/dist/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.d.ts.map +1 -1
- package/dist/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.js +1 -3
- package/dist/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.js.map +1 -1
- package/dist/src/components/Device/DevicesPage/DecommissionedDevicesTable.d.ts.map +1 -1
- package/dist/src/components/Device/DevicesPage/DecommissionedDevicesTable.js +0 -3
- package/dist/src/components/Device/DevicesPage/DecommissionedDevicesTable.js.map +1 -1
- package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.d.ts.map +1 -1
- package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.js +3 -1
- package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.js.map +1 -1
- package/dist/src/components/Device/DevicesPage/DevicesPage.d.ts.map +1 -1
- package/dist/src/components/Device/DevicesPage/DevicesPage.js +1 -1
- package/dist/src/components/Device/DevicesPage/DevicesPage.js.map +1 -1
- package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.d.ts +3 -1
- package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.d.ts.map +1 -1
- package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.js +12 -4
- package/dist/src/components/Device/DevicesPage/EnrolledDeviceTableRow.js.map +1 -1
- package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.d.ts +2 -1
- package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.d.ts.map +1 -1
- package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.js +7 -6
- package/dist/src/components/Device/DevicesPage/EnrolledDevicesTable.js.map +1 -1
- package/dist/src/components/Events/useEvents.d.ts.map +1 -1
- package/dist/src/components/Events/useEvents.js +12 -0
- package/dist/src/components/Events/useEvents.js.map +1 -1
- package/dist/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.js +1 -1
- package/dist/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.js.map +1 -1
- package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.d.ts.map +1 -1
- package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.js +4 -3
- package/dist/src/components/Fleet/FleetDetails/FleetDetailsPage.js.map +1 -1
- package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.d.ts.map +1 -1
- package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.js +7 -1
- package/dist/src/components/Fleet/FleetDetails/FleetDevicesCharts.js.map +1 -1
- package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.d.ts +8 -0
- package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.d.ts.map +1 -0
- package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.js +36 -0
- package/dist/src/components/Fleet/FleetDetails/FleetRestoreBanner.js.map +1 -0
- package/dist/src/components/Fleet/FleetsPage.d.ts.map +1 -1
- package/dist/src/components/Fleet/FleetsPage.js +2 -0
- package/dist/src/components/Fleet/FleetsPage.js.map +1 -1
- package/dist/src/components/ListPage/ListPageActions.d.ts +3 -2
- package/dist/src/components/ListPage/ListPageActions.d.ts.map +1 -1
- package/dist/src/components/ListPage/ListPageActions.js +27 -1
- package/dist/src/components/ListPage/ListPageActions.js.map +1 -1
- package/dist/src/components/Masthead/CommandLineToolsPage.d.ts.map +1 -1
- package/dist/src/components/Masthead/CommandLineToolsPage.js +18 -14
- package/dist/src/components/Masthead/CommandLineToolsPage.js.map +1 -1
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.d.ts.map +1 -1
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js +15 -5
- package/dist/src/components/OverviewPage/Cards/Alerts/AlertsCard.js.map +1 -1
- package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.d.ts.map +1 -1
- package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.js +7 -1
- package/dist/src/components/OverviewPage/Cards/Status/DeviceStatusChart.js.map +1 -1
- package/dist/src/components/OverviewPage/Overview.d.ts.map +1 -1
- package/dist/src/components/OverviewPage/Overview.js +4 -2
- package/dist/src/components/OverviewPage/Overview.js.map +1 -1
- package/dist/src/components/Status/StatusDisplay.d.ts +3 -1
- package/dist/src/components/Status/StatusDisplay.d.ts.map +1 -1
- package/dist/src/components/Status/StatusDisplay.js +8 -8
- package/dist/src/components/Status/StatusDisplay.js.map +1 -1
- package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.d.ts +7 -0
- package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.d.ts.map +1 -0
- package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.js +22 -0
- package/dist/src/components/SystemRestore/PendingSyncDevicesAlert.js.map +1 -0
- package/dist/src/components/SystemRestore/SuspendedDevicesAlert.d.ts +16 -0
- package/dist/src/components/SystemRestore/SuspendedDevicesAlert.d.ts.map +1 -0
- package/dist/src/components/SystemRestore/SuspendedDevicesAlert.js +75 -0
- package/dist/src/components/SystemRestore/SuspendedDevicesAlert.js.map +1 -0
- package/dist/src/components/SystemRestore/SystemRestoreBanners.css +6 -0
- package/dist/src/components/SystemRestore/SystemRestoreBanners.d.ts +28 -0
- package/dist/src/components/SystemRestore/SystemRestoreBanners.d.ts.map +1 -0
- package/dist/src/components/SystemRestore/SystemRestoreBanners.js +38 -0
- package/dist/src/components/SystemRestore/SystemRestoreBanners.js.map +1 -0
- package/dist/src/components/charts/utils.js +1 -1
- package/dist/src/components/charts/utils.js.map +1 -1
- package/dist/src/components/common/OrganizationGuard.d.ts +13 -0
- package/dist/src/components/common/OrganizationGuard.d.ts.map +1 -0
- package/dist/src/components/common/OrganizationGuard.js +106 -0
- package/dist/src/components/common/OrganizationGuard.js.map +1 -0
- package/dist/src/components/common/OrganizationSelector.d.ts +8 -0
- package/dist/src/components/common/OrganizationSelector.d.ts.map +1 -0
- package/dist/src/components/common/OrganizationSelector.js +92 -0
- package/dist/src/components/common/OrganizationSelector.js.map +1 -0
- package/dist/src/components/common/PageNavigation.d.ts +4 -0
- package/dist/src/components/common/PageNavigation.d.ts.map +1 -0
- package/dist/src/components/common/PageNavigation.js +46 -0
- package/dist/src/components/common/PageNavigation.js.map +1 -0
- package/dist/src/components/form/FilterSelect.css +3 -4
- package/dist/src/components/form/validations.d.ts +9 -0
- package/dist/src/components/form/validations.d.ts.map +1 -1
- package/dist/src/components/form/validations.js +12 -1
- package/dist/src/components/form/validations.js.map +1 -1
- package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.d.ts +15 -0
- package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.d.ts.map +1 -0
- package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.js +56 -0
- package/dist/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.js.map +1 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.d.ts +7 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.d.ts.map +1 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.js +265 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.js.map +1 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.d.ts +9 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.d.ts.map +1 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.js +27 -0
- package/dist/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.js.map +1 -0
- package/dist/src/hooks/useAccessReview.d.ts.map +1 -1
- package/dist/src/hooks/useAccessReview.js +17 -4
- package/dist/src/hooks/useAccessReview.js.map +1 -1
- package/dist/src/hooks/useAlertsEnabled.d.ts +2 -0
- package/dist/src/hooks/useAlertsEnabled.d.ts.map +1 -0
- package/dist/src/hooks/useAlertsEnabled.js +50 -0
- package/dist/src/hooks/useAlertsEnabled.js.map +1 -0
- package/dist/src/hooks/useAppContext.d.ts +3 -6
- package/dist/src/hooks/useAppContext.d.ts.map +1 -1
- package/dist/src/hooks/useAppContext.js +1 -0
- package/dist/src/hooks/useAppContext.js.map +1 -1
- package/dist/src/hooks/useFetch.d.ts +6 -7
- package/dist/src/hooks/useFetch.d.ts.map +1 -1
- package/dist/src/hooks/useFetch.js +2 -3
- package/dist/src/hooks/useFetch.js.map +1 -1
- package/dist/src/hooks/useFetchPeriodically.d.ts.map +1 -1
- package/dist/src/hooks/useFetchPeriodically.js +4 -9
- package/dist/src/hooks/useFetchPeriodically.js.map +1 -1
- package/dist/src/hooks/useSystemRestoreContext.d.ts +16 -0
- package/dist/src/hooks/useSystemRestoreContext.d.ts.map +1 -0
- package/dist/src/hooks/useSystemRestoreContext.js +45 -0
- package/dist/src/hooks/useSystemRestoreContext.js.map +1 -0
- package/dist/src/types/extraTypes.d.ts +17 -18
- package/dist/src/types/extraTypes.d.ts.map +1 -1
- package/dist/src/types/extraTypes.js +1 -6
- package/dist/src/types/extraTypes.js.map +1 -1
- package/dist/src/types/rbac.d.ts +1 -0
- 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/api.d.ts +2 -15
- package/dist/src/utils/api.d.ts.map +1 -1
- package/dist/src/utils/api.js +1 -40
- package/dist/src/utils/api.js.map +1 -1
- package/dist/src/utils/devices.d.ts +2 -0
- package/dist/src/utils/devices.d.ts.map +1 -1
- package/dist/src/utils/devices.js +11 -1
- package/dist/src/utils/devices.js.map +1 -1
- package/dist/src/utils/organizationStorage.d.ts +4 -0
- package/dist/src/utils/organizationStorage.d.ts.map +1 -0
- package/dist/src/utils/organizationStorage.js +18 -0
- package/dist/src/utils/organizationStorage.js.map +1 -0
- package/dist/src/utils/query.d.ts +2 -0
- package/dist/src/utils/query.d.ts.map +1 -1
- package/dist/src/utils/query.js +16 -0
- package/dist/src/utils/query.js.map +1 -1
- package/dist/src/utils/status/common.d.ts +1 -0
- package/dist/src/utils/status/common.d.ts.map +1 -1
- package/dist/src/utils/status/common.js.map +1 -1
- package/dist/src/utils/status/devices.d.ts +5 -0
- package/dist/src/utils/status/devices.d.ts.map +1 -1
- package/dist/src/utils/status/devices.js +44 -5
- package/dist/src/utils/status/devices.js.map +1 -1
- package/dist/src/utils/status/fleet.js +1 -1
- package/dist/src/utils/status/fleet.js.map +1 -1
- package/dist/src/utils/status/repository.js +1 -1
- package/dist/src/utils/status/repository.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DetailsPage/DetailsPage.tsx +3 -0
- package/src/components/DetailsPage/DetailsPageActions.tsx +45 -0
- package/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx +57 -5
- package/src/components/Device/DeviceDetails/DeviceDetailsTab.tsx +0 -5
- package/src/components/Device/DeviceDetails/DeviceDetailsTabContent/StatusContent.tsx +11 -3
- package/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx +0 -2
- package/src/components/Device/DevicesPage/DecommissionedDevicesTable.tsx +0 -3
- package/src/components/Device/DevicesPage/DeviceToolbarFilters.tsx +5 -1
- package/src/components/Device/DevicesPage/DevicesPage.tsx +1 -0
- package/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx +15 -3
- package/src/components/Device/DevicesPage/EnrolledDevicesTable.tsx +11 -5
- package/src/components/Events/useEvents.ts +12 -0
- package/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx +1 -1
- package/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx +4 -5
- package/src/components/Fleet/FleetDetails/FleetDevicesCharts.tsx +9 -3
- package/src/components/Fleet/FleetDetails/FleetRestoreBanner.tsx +46 -0
- package/src/components/Fleet/FleetsPage.tsx +2 -0
- package/src/components/ListPage/ListPageActions.tsx +46 -3
- package/src/components/Masthead/CommandLineToolsPage.tsx +17 -14
- package/src/components/OverviewPage/Cards/Alerts/AlertsCard.tsx +19 -5
- package/src/components/OverviewPage/Cards/Status/DeviceStatusChart.tsx +8 -2
- package/src/components/OverviewPage/Overview.tsx +5 -2
- package/src/components/Status/StatusDisplay.tsx +32 -23
- package/src/components/SystemRestore/PendingSyncDevicesAlert.tsx +36 -0
- package/src/components/SystemRestore/SuspendedDevicesAlert.tsx +144 -0
- package/src/components/SystemRestore/SystemRestoreBanners.css +6 -0
- package/src/components/SystemRestore/SystemRestoreBanners.tsx +82 -0
- package/src/components/charts/utils.ts +1 -1
- package/src/components/common/OrganizationGuard.tsx +124 -0
- package/src/components/common/OrganizationSelector.tsx +192 -0
- package/src/components/common/PageNavigation.tsx +103 -0
- package/src/components/form/FilterSelect.css +3 -4
- package/src/components/form/validations.ts +14 -0
- package/src/components/modals/ResumeDevicesModal/ResumeDevicesModal.tsx +114 -0
- package/src/components/modals/massModals/ResumeDevicesModal/MassResumeDevicesModal.tsx +465 -0
- package/src/components/modals/massModals/ResumeDevicesModal/ResumeAllDevicesConfirmationDialog.tsx +55 -0
- package/src/hooks/useAccessReview.ts +20 -4
- package/src/hooks/useAlertsEnabled.ts +50 -0
- package/src/hooks/useAppContext.tsx +9 -7
- package/src/hooks/useFetch.ts +1 -3
- package/src/hooks/useFetchPeriodically.ts +4 -11
- package/src/hooks/useSystemRestoreContext.tsx +54 -0
- package/src/types/extraTypes.ts +17 -23
- package/src/types/rbac.ts +1 -0
- package/src/utils/api.ts +2 -51
- package/src/utils/devices.ts +11 -1
- package/src/utils/organizationStorage.ts +13 -0
- package/src/utils/query.ts +22 -0
- package/src/utils/status/common.ts +1 -0
- package/src/utils/status/devices.ts +49 -2
- package/src/utils/status/fleet.ts +1 -1
- package/src/utils/status/repository.ts +1 -1
- package/dist/src/hooks/useAlerts.d.ts +0 -26
- package/dist/src/hooks/useAlerts.d.ts.map +0 -1
- package/dist/src/hooks/useAlerts.js +0 -114
- package/dist/src/hooks/useAlerts.js.map +0 -1
- package/dist/src/utils/metrics.d.ts +0 -9
- package/dist/src/utils/metrics.d.ts.map +0 -1
- package/dist/src/utils/metrics.js +0 -48
- package/dist/src/utils/metrics.js.map +0 -1
- package/src/hooks/useAlerts.ts +0 -147
- 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
|
-
|
|
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
|
-
|
|
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;
|