@northern.tech/store 0.1.0
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/.eslintrc.cjs +9 -0
- package/.prettierignore +1 -0
- package/CHANGELOG.md +12 -0
- package/actions.ts +32 -0
- package/api/general-api.test.ts +51 -0
- package/api/general-api.ts +60 -0
- package/api/types/AWSCredentials.ts +16 -0
- package/api/types/Actor.ts +35 -0
- package/api/types/Addon.ts +17 -0
- package/api/types/AddonTenantadm.ts +17 -0
- package/api/types/Alert.ts +34 -0
- package/api/types/AlertDetails.ts +23 -0
- package/api/types/AlertSubject.ts +23 -0
- package/api/types/ApiBurst.ts +21 -0
- package/api/types/ApiBurstTenantadm.ts +21 -0
- package/api/types/ApiLimits.ts +16 -0
- package/api/types/ApiLimitsTenantadm.ts +16 -0
- package/api/types/ApiQuota.ts +17 -0
- package/api/types/ApiQuotaTenantadm.ts +17 -0
- package/api/types/Artifact.ts +48 -0
- package/api/types/ArtifactDeployments.ts +48 -0
- package/api/types/ArtifactInfo.ts +11 -0
- package/api/types/ArtifactInfoDeployments.ts +11 -0
- package/api/types/ArtifactLink.ts +11 -0
- package/api/types/ArtifactTypeInfo.ts +13 -0
- package/api/types/ArtifactTypeInfoDeployments.ts +13 -0
- package/api/types/ArtifactUpdate.ts +10 -0
- package/api/types/ArtifactUploadLink.ts +15 -0
- package/api/types/Attribute.ts +36 -0
- package/api/types/AttributeInventory.ts +32 -0
- package/api/types/AuditLog.ts +26 -0
- package/api/types/AuthSet.ts +33 -0
- package/api/types/AuthSetDeviceauth.ts +37 -0
- package/api/types/AutoAuthRequest.ts +23 -0
- package/api/types/AzureSharedAccessSecret.ts +11 -0
- package/api/types/BillingInfo.ts +13 -0
- package/api/types/BinaryDeltaConfiguration.ts +15 -0
- package/api/types/BinaryDeltaConfigurationDeployments.ts +15 -0
- package/api/types/BinaryDeltaLimits.ts +15 -0
- package/api/types/BinaryDeltaLimitsDeployments.ts +15 -0
- package/api/types/BoundingBox.ts +11 -0
- package/api/types/CancelRequest.ts +10 -0
- package/api/types/CardData.ts +49 -0
- package/api/types/CardSetupData.ts +17 -0
- package/api/types/CheckoutData.ts +13 -0
- package/api/types/Configuration.ts +11 -0
- package/api/types/Count.ts +13 -0
- package/api/types/Credentials.ts +23 -0
- package/api/types/DBusSubsystem.ts +27 -0
- package/api/types/DeltaConfiguration.ts +14 -0
- package/api/types/DeltaConfigurationDeployments.ts +14 -0
- package/api/types/Deployment.ts +95 -0
- package/api/types/DeploymentAggregation.ts +18 -0
- package/api/types/DeploymentAggregationItem.ts +18 -0
- package/api/types/DeploymentAggregationTerm.ts +23 -0
- package/api/types/DeploymentAggregationTerms.ts +18 -0
- package/api/types/DeploymentAttributeProjection.ts +11 -0
- package/api/types/DeploymentAuditlogs.ts +11 -0
- package/api/types/DeploymentDeployments.ts +99 -0
- package/api/types/DeploymentFilterTerm.ts +37 -0
- package/api/types/DeploymentIdentifier.ts +10 -0
- package/api/types/DeploymentPhase.ts +23 -0
- package/api/types/DeploymentReporting.ts +46 -0
- package/api/types/DeploymentSearchTerms.ts +39 -0
- package/api/types/DeploymentSortTerm.ts +25 -0
- package/api/types/DeploymentStatistics.ts +15 -0
- package/api/types/DeploymentStatusStatistics.ts +54 -0
- package/api/types/Device.ts +29 -0
- package/api/types/DeviceAggregation.ts +18 -0
- package/api/types/DeviceAggregationItem.ts +18 -0
- package/api/types/DeviceAggregationTerm.ts +27 -0
- package/api/types/DeviceAggregationTerms.ts +22 -0
- package/api/types/DeviceAttribute.ts +23 -0
- package/api/types/DeviceAttributeProjection.ts +15 -0
- package/api/types/DeviceAuditlogs.ts +17 -0
- package/api/types/DeviceAuthEvent.ts +23 -0
- package/api/types/DeviceConfiguration.ts +16 -0
- package/api/types/DeviceDeployment.ts +11 -0
- package/api/types/DeviceDeployments.ts +31 -0
- package/api/types/DeviceDeviceauth.ts +42 -0
- package/api/types/DeviceFilterAttribute.ts +22 -0
- package/api/types/DeviceFilterTerm.ts +41 -0
- package/api/types/DeviceInventory.ts +19 -0
- package/api/types/DeviceInventoryInventory.ts +19 -0
- package/api/types/DeviceReporting.ts +22 -0
- package/api/types/DeviceSearchTerms.ts +39 -0
- package/api/types/DeviceSortTerm.ts +29 -0
- package/api/types/DeviceState.ts +32 -0
- package/api/types/DeviceStateDeviceconnect.ts +33 -0
- package/api/types/DeviceStateIot_manager.ts +14 -0
- package/api/types/DeviceStatus.ts +19 -0
- package/api/types/DeviceWithImage.ts +80 -0
- package/api/types/DirectUploadMetadata.ts +18 -0
- package/api/types/EmailVerificationCompletion.ts +13 -0
- package/api/types/EmailVerificationRequest.ts +13 -0
- package/api/types/Error.ts +17 -0
- package/api/types/ErrorAuditlogs.ts +13 -0
- package/api/types/ErrorDeployments.ts +17 -0
- package/api/types/ErrorDeviceauth.ts +13 -0
- package/api/types/ErrorDeviceconfig.ts +17 -0
- package/api/types/ErrorDeviceconnect.ts +17 -0
- package/api/types/ErrorDevicemonitor.ts +17 -0
- package/api/types/ErrorExt.ts +18 -0
- package/api/types/ErrorInventory.ts +17 -0
- package/api/types/ErrorIot_manager.ts +17 -0
- package/api/types/ErrorNotFound.ts +17 -0
- package/api/types/ErrorReporting.ts +18 -0
- package/api/types/ErrorTenantadm.ts +13 -0
- package/api/types/ErrorUseradm.ts +17 -0
- package/api/types/Event.ts +48 -0
- package/api/types/ExternalIdentity.ts +17 -0
- package/api/types/Features.ts +17 -0
- package/api/types/FileUpload.ts +23 -0
- package/api/types/Filter.ts +19 -0
- package/api/types/FilterAttribute.ts +21 -0
- package/api/types/FilterDefinition.ts +18 -0
- package/api/types/FilterDeployments.ts +19 -0
- package/api/types/FilterInventory.ts +20 -0
- package/api/types/FilterPredicate.ts +78 -0
- package/api/types/FilterPredicateDeployments.ts +48 -0
- package/api/types/FilterPredicateInventory.ts +76 -0
- package/api/types/GeoBoundingBox.ts +10 -0
- package/api/types/GeoBoundingBoxFilter.ts +6 -0
- package/api/types/GeoDistance.ts +17 -0
- package/api/types/GeoDistanceFilter.ts +10 -0
- package/api/types/GeoPoint.ts +15 -0
- package/api/types/Group.ts +10 -0
- package/api/types/HTTP.ts +19 -0
- package/api/types/IdentityData.ts +25 -0
- package/api/types/InputParameter.ts +8 -0
- package/api/types/Integration.ts +24 -0
- package/api/types/JobStatus.ts +13 -0
- package/api/types/Limit.ts +21 -0
- package/api/types/LimitDeployments.ts +21 -0
- package/api/types/LimitDeviceauth.ts +10 -0
- package/api/types/LimitUseradm.ts +11 -0
- package/api/types/Limits.ts +11 -0
- package/api/types/LineDescriptor.ts +11 -0
- package/api/types/LogSubsystem.ts +23 -0
- package/api/types/LoginOptions.ts +18 -0
- package/api/types/ManagementAPIConfiguration.ts +5 -0
- package/api/types/MenderTypes.ts +183 -0
- package/api/types/MonitorConfiguration.ts +22 -0
- package/api/types/NewConfigurationDeployment.ts +15 -0
- package/api/types/NewConfigurationDeploymentResponse.ts +10 -0
- package/api/types/NewDeployment.ts +42 -0
- package/api/types/NewDeploymentForGroup.ts +33 -0
- package/api/types/NewDeploymentPhase.ts +37 -0
- package/api/types/NewDeploymentPhaseDeployments.ts +20 -0
- package/api/types/NewDeploymentV2.ts +31 -0
- package/api/types/NewTenant.ts +41 -0
- package/api/types/Object.ts +35 -0
- package/api/types/OwnUserUpdate.ts +21 -0
- package/api/types/PasswordResetCompletion.ts +17 -0
- package/api/types/PasswordResetRequest.ts +13 -0
- package/api/types/Permission.ts +21 -0
- package/api/types/PermissionObject.ts +14 -0
- package/api/types/PermissionSet.ts +22 -0
- package/api/types/PermissionSetWithScope.ts +15 -0
- package/api/types/PersonalAccessToken.ts +29 -0
- package/api/types/PersonalAccessTokenRequest.ts +18 -0
- package/api/types/Plan.ts +19 -0
- package/api/types/PlanBindingDetails.ts +13 -0
- package/api/types/PlanChangeRequest.ts +14 -0
- package/api/types/PlanLimits.ts +12 -0
- package/api/types/PreAuthSet.ts +16 -0
- package/api/types/Release.ts +32 -0
- package/api/types/ReleaseDeployments.ts +32 -0
- package/api/types/ReleaseUpdate.ts +13 -0
- package/api/types/Releases.ts +9 -0
- package/api/types/ReleasesDeleteError.ts +21 -0
- package/api/types/ReleasesDeployments.ts +9 -0
- package/api/types/Role.ts +19 -0
- package/api/types/RolePermission.ts +25 -0
- package/api/types/RolePermissionObject.ts +17 -0
- package/api/types/RoleUseradm.ts +19 -0
- package/api/types/SAMLMetadata.ts +21 -0
- package/api/types/Scope.ts +19 -0
- package/api/types/SelectAttribute.ts +17 -0
- package/api/types/ServiceSubsystem.ts +19 -0
- package/api/types/Settings.ts +23 -0
- package/api/types/SortCriteria.ts +30 -0
- package/api/types/SsoObject.ts +21 -0
- package/api/types/Status.ts +17 -0
- package/api/types/StatusDeviceauth.ts +18 -0
- package/api/types/StatusTenantadm.ts +19 -0
- package/api/types/StorageLimit.ts +17 -0
- package/api/types/SubscriptionData.ts +8 -0
- package/api/types/SupportRequest.ts +17 -0
- package/api/types/Tag.ts +27 -0
- package/api/types/Tags.ts +10 -0
- package/api/types/TaskResult.ts +13 -0
- package/api/types/TaskResultCLI.ts +10 -0
- package/api/types/TaskResultHTTPRequest.ts +8 -0
- package/api/types/TaskResultHTTPResponse.ts +10 -0
- package/api/types/Tenant.ts +38 -0
- package/api/types/TenantApiLimits.ts +12 -0
- package/api/types/TenantApiLimitsTenantadm.ts +12 -0
- package/api/types/TenantIdName.ts +14 -0
- package/api/types/TenantIdsData.ts +10 -0
- package/api/types/TenantInfo.ts +14 -0
- package/api/types/TenantShortInfo.ts +21 -0
- package/api/types/TenantTenantadm.ts +38 -0
- package/api/types/TenantsIdName.ts +6 -0
- package/api/types/Update.ts +17 -0
- package/api/types/UpdateDeployments.ts +18 -0
- package/api/types/UpdateFile.ts +13 -0
- package/api/types/UpdateFileDeployments.ts +13 -0
- package/api/types/UpdateTypes.ts +8 -0
- package/api/types/UpgradeCompleteRequest.ts +23 -0
- package/api/types/User.ts +13 -0
- package/api/types/UserAuditlogs.ts +13 -0
- package/api/types/UserNew.ts +26 -0
- package/api/types/UserUpdate.ts +25 -0
- package/api/types/UserUseradm.ts +45 -0
- package/api/types/UserWithTenantInfo.ts +8 -0
- package/api/types/XDeltaArgs.ts +9 -0
- package/api/types/XDeltaArgsDeployments.ts +9 -0
- package/api/types/XDeltaArgsLimits.ts +10 -0
- package/api/types/XDeltaArgsLimitsDeployments.ts +10 -0
- package/api/users-api.ts +34 -0
- package/appSlice/constants.ts +166 -0
- package/appSlice/index.ts +159 -0
- package/appSlice/reducer.test.ts +97 -0
- package/appSlice/selectors.ts +33 -0
- package/appSlice/thunks.test.ts +118 -0
- package/appSlice/thunks.ts +148 -0
- package/auth.test.ts +36 -0
- package/auth.ts +82 -0
- package/commonConstants.tsx +252 -0
- package/commonSelectors.ts +255 -0
- package/constants.ts +22 -0
- package/deploymentsSlice/constants.ts +175 -0
- package/deploymentsSlice/index.ts +145 -0
- package/deploymentsSlice/reducer.test.ts +108 -0
- package/deploymentsSlice/selectors.ts +58 -0
- package/deploymentsSlice/thunks.test.ts +425 -0
- package/deploymentsSlice/thunks.ts +382 -0
- package/devicesSlice/constants.ts +56 -0
- package/devicesSlice/index.ts +207 -0
- package/devicesSlice/reducer.test.ts +194 -0
- package/devicesSlice/selectors.ts +127 -0
- package/devicesSlice/thunks.test.tsx +1245 -0
- package/devicesSlice/thunks.tsx +1098 -0
- package/monitorSlice/constants.ts +17 -0
- package/monitorSlice/index.ts +65 -0
- package/monitorSlice/reducer.test.ts +79 -0
- package/monitorSlice/selectors.ts +15 -0
- package/monitorSlice/thunks.test.ts +112 -0
- package/monitorSlice/thunks.ts +101 -0
- package/onboardingSlice/constants.ts +53 -0
- package/onboardingSlice/index.ts +60 -0
- package/onboardingSlice/reducer.test.ts +45 -0
- package/onboardingSlice/selectors.ts +15 -0
- package/onboardingSlice/thunks.test.ts +235 -0
- package/onboardingSlice/thunks.ts +142 -0
- package/organizationSlice/constants.ts +82 -0
- package/organizationSlice/index.ts +111 -0
- package/organizationSlice/reducer.test.ts +78 -0
- package/organizationSlice/selectors.ts +45 -0
- package/organizationSlice/thunks.test.ts +619 -0
- package/organizationSlice/thunks.ts +383 -0
- package/package.json +38 -0
- package/releasesSlice/constants.ts +18 -0
- package/releasesSlice/index.ts +116 -0
- package/releasesSlice/reducer.test.ts +78 -0
- package/releasesSlice/selectors.ts +33 -0
- package/releasesSlice/thunks.test.ts +389 -0
- package/releasesSlice/thunks.ts +379 -0
- package/selectors.ts +22 -0
- package/store.ts +89 -0
- package/storehooks.test.tsx +386 -0
- package/storehooks.ts +250 -0
- package/thunks.ts +21 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +11 -0
- package/usersSlice/constants.ts +374 -0
- package/usersSlice/index.ts +151 -0
- package/usersSlice/reducer.test.ts +141 -0
- package/usersSlice/selectors.ts +60 -0
- package/usersSlice/thunks.test.ts +660 -0
- package/usersSlice/thunks.ts +773 -0
- package/utils.test.ts +187 -0
- package/utils.ts +249 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
// Copyright 2024 Northern.tech AS
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
// @ts-nocheck
|
|
15
|
+
|
|
16
|
+
/*eslint import/namespace: ['error', { allowComputed: true }]*/
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { Link } from 'react-router-dom';
|
|
19
|
+
|
|
20
|
+
import storeActions from '@northern.tech/store/actions';
|
|
21
|
+
import GeneralApi from '@northern.tech/store/api/general-api';
|
|
22
|
+
import {
|
|
23
|
+
ALL_DEVICES,
|
|
24
|
+
DEVICE_FILTERING_OPTIONS,
|
|
25
|
+
DEVICE_LIST_DEFAULTS,
|
|
26
|
+
EXTERNAL_PROVIDER,
|
|
27
|
+
MAX_PAGE_SIZE,
|
|
28
|
+
SORTING_OPTIONS,
|
|
29
|
+
TIMEOUTS,
|
|
30
|
+
UNGROUPED_GROUP,
|
|
31
|
+
auditLogsApiUrl,
|
|
32
|
+
defaultReports,
|
|
33
|
+
headerNames,
|
|
34
|
+
rootfsImageVersion
|
|
35
|
+
} from '@northern.tech/store/constants';
|
|
36
|
+
import {
|
|
37
|
+
getAttrsEndpoint,
|
|
38
|
+
getCurrentUser,
|
|
39
|
+
getDeviceTwinIntegrations,
|
|
40
|
+
getGlobalSettings,
|
|
41
|
+
getIdAttribute,
|
|
42
|
+
getSearchEndpoint,
|
|
43
|
+
getTenantCapabilities,
|
|
44
|
+
getUserCapabilities,
|
|
45
|
+
getUserSettings
|
|
46
|
+
} from '@northern.tech/store/selectors';
|
|
47
|
+
import { commonErrorFallback, commonErrorHandler } from '@northern.tech/store/store';
|
|
48
|
+
import { getDeviceMonitorConfig, getLatestDeviceAlerts, getSingleDeployment, saveGlobalSettings } from '@northern.tech/store/thunks';
|
|
49
|
+
import {
|
|
50
|
+
convertDeviceListStateToFilters,
|
|
51
|
+
extractErrorMessage,
|
|
52
|
+
filtersFilter,
|
|
53
|
+
mapDeviceAttributes,
|
|
54
|
+
mapFiltersToTerms,
|
|
55
|
+
mapTermsToFilters,
|
|
56
|
+
progress
|
|
57
|
+
} from '@northern.tech/store/utils';
|
|
58
|
+
import { attributeDuplicateFilter, deepCompare, getSnackbarMessage } from '@northern.tech/utils/helpers';
|
|
59
|
+
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
60
|
+
import { isCancel } from 'axios';
|
|
61
|
+
import pluralize from 'pluralize';
|
|
62
|
+
import { v4 as uuid } from 'uuid';
|
|
63
|
+
|
|
64
|
+
import { actions, sliceName } from '.';
|
|
65
|
+
import {
|
|
66
|
+
ALL_DEVICE_STATES,
|
|
67
|
+
DEVICE_STATES,
|
|
68
|
+
deviceAuthV2,
|
|
69
|
+
deviceConfig,
|
|
70
|
+
deviceConnect,
|
|
71
|
+
emptyFilter,
|
|
72
|
+
geoAttributes,
|
|
73
|
+
inventoryApiUrl,
|
|
74
|
+
inventoryApiUrlV2,
|
|
75
|
+
iotManagerBaseURL,
|
|
76
|
+
reportingApiUrl
|
|
77
|
+
} from './constants';
|
|
78
|
+
import {
|
|
79
|
+
getDeviceById as getDeviceByIdSelector,
|
|
80
|
+
getDeviceFilters,
|
|
81
|
+
getDeviceListState,
|
|
82
|
+
getDevicesById,
|
|
83
|
+
getGroupsById,
|
|
84
|
+
getGroups as getGroupsSelector,
|
|
85
|
+
getSelectedGroup
|
|
86
|
+
} from './selectors';
|
|
87
|
+
|
|
88
|
+
const { cleanUpUpload, initUpload, setSnackbar, uploadProgress } = storeActions;
|
|
89
|
+
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
|
|
90
|
+
|
|
91
|
+
const defaultAttributes = [
|
|
92
|
+
{ scope: 'identity', attribute: 'status' },
|
|
93
|
+
{ scope: 'inventory', attribute: 'artifact_name' },
|
|
94
|
+
{ scope: 'inventory', attribute: 'device_type' },
|
|
95
|
+
{ scope: 'inventory', attribute: 'mender_is_gateway' },
|
|
96
|
+
{ scope: 'inventory', attribute: 'mender_gateway_system_id' },
|
|
97
|
+
{ scope: 'inventory', attribute: rootfsImageVersion },
|
|
98
|
+
{ scope: 'monitor', attribute: 'alerts' },
|
|
99
|
+
{ scope: 'system', attribute: 'created_ts' },
|
|
100
|
+
{ scope: 'system', attribute: 'updated_ts' },
|
|
101
|
+
{ scope: 'system', attribute: 'check_in_time' },
|
|
102
|
+
{ scope: 'system', attribute: 'group' },
|
|
103
|
+
{ scope: 'tags', attribute: 'name' }
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
export const getGroups = createAsyncThunk(`${sliceName}/getGroups`, (_, { dispatch, getState }) =>
|
|
107
|
+
GeneralApi.get(`${inventoryApiUrl}/groups`).then(res => {
|
|
108
|
+
const state = getGroupsById(getState());
|
|
109
|
+
const dynamicGroups = Object.entries(state).reduce((accu, [id, group]) => {
|
|
110
|
+
if (group.id || (group.filters?.length && id !== UNGROUPED_GROUP.id)) {
|
|
111
|
+
accu[id] = group;
|
|
112
|
+
}
|
|
113
|
+
return accu;
|
|
114
|
+
}, {});
|
|
115
|
+
const groups = res.data.reduce((accu, group) => {
|
|
116
|
+
accu[group] = { deviceIds: [], filters: [], total: 0, ...state[group] };
|
|
117
|
+
return accu;
|
|
118
|
+
}, dynamicGroups);
|
|
119
|
+
const filters = [{ key: 'group', value: res.data, operator: DEVICE_FILTERING_OPTIONS.$nin.key, scope: 'system' }];
|
|
120
|
+
return Promise.all([
|
|
121
|
+
dispatch(actions.receivedGroups(groups)),
|
|
122
|
+
dispatch(getDevicesByStatus({ filterSelection: filters, group: 0, page: 1, perPage: 1, status: undefined }))
|
|
123
|
+
]).then(promises => {
|
|
124
|
+
const devicesRetrieval = promises[promises.length - 1] || [];
|
|
125
|
+
const { payload } = devicesRetrieval || {};
|
|
126
|
+
const result = payload[payload.length - 1] || {};
|
|
127
|
+
if (!result.total) {
|
|
128
|
+
return Promise.resolve();
|
|
129
|
+
}
|
|
130
|
+
return Promise.resolve(
|
|
131
|
+
dispatch(
|
|
132
|
+
actions.addGroup({
|
|
133
|
+
groupName: UNGROUPED_GROUP.id,
|
|
134
|
+
group: { filters: [{ key: 'group', value: res.data, operator: DEVICE_FILTERING_OPTIONS.$nin.key, scope: 'system' }] }
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
export const addDevicesToGroup = createAsyncThunk(`${sliceName}/addDevicesToGroup`, ({ group, deviceIds, isCreation }, { dispatch }) =>
|
|
143
|
+
GeneralApi.patch(`${inventoryApiUrl}/groups/${group}/devices`, deviceIds)
|
|
144
|
+
.then(() => dispatch(actions.addToGroup({ group, deviceIds })))
|
|
145
|
+
.finally(() => (isCreation ? Promise.resolve(dispatch(getGroups())) : {}))
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
export const removeDevicesFromGroup = createAsyncThunk(`${sliceName}/removeDevicesFromGroup`, ({ group, deviceIds }, { dispatch }) =>
|
|
149
|
+
GeneralApi.delete(`${inventoryApiUrl}/groups/${group}/devices`, deviceIds).then(() =>
|
|
150
|
+
Promise.all([
|
|
151
|
+
dispatch(actions.removeFromGroup({ group, deviceIds })),
|
|
152
|
+
dispatch(setSnackbar(`The ${pluralize('devices', deviceIds.length)} ${pluralize('were', deviceIds.length)} removed from the group`, TIMEOUTS.fiveSeconds))
|
|
153
|
+
])
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const getGroupNotification = (newGroup, selectedGroup) => {
|
|
158
|
+
const successMessage = 'The group was updated successfully';
|
|
159
|
+
if (newGroup === selectedGroup) {
|
|
160
|
+
return [successMessage, TIMEOUTS.fiveSeconds];
|
|
161
|
+
}
|
|
162
|
+
return [
|
|
163
|
+
<>
|
|
164
|
+
{successMessage} - <Link to={`/devices?inventory=group:eq:${newGroup}`}>click here</Link> to see it.
|
|
165
|
+
</>,
|
|
166
|
+
5000,
|
|
167
|
+
undefined,
|
|
168
|
+
undefined,
|
|
169
|
+
() => {}
|
|
170
|
+
];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const addStaticGroup = createAsyncThunk(`${sliceName}/addStaticGroup`, ({ group, devices }, { dispatch, getState }) =>
|
|
174
|
+
Promise.resolve(dispatch(addDevicesToGroup({ group, deviceIds: devices.map(({ id }) => id), isCreation: true })))
|
|
175
|
+
.then(() =>
|
|
176
|
+
Promise.resolve(
|
|
177
|
+
dispatch(
|
|
178
|
+
actions.addGroup({
|
|
179
|
+
group: { deviceIds: [], total: 0, filters: [], ...getState().devices.groups.byId[group] },
|
|
180
|
+
groupName: group
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
).then(() =>
|
|
184
|
+
Promise.all([
|
|
185
|
+
dispatch(setDeviceListState({ setOnly: true })),
|
|
186
|
+
dispatch(getGroups()),
|
|
187
|
+
dispatch(setSnackbar(...getGroupNotification(group, getState().devices.groups.selectedGroup)))
|
|
188
|
+
])
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
.catch(err => commonErrorHandler(err, `Group could not be updated:`, dispatch))
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
export const removeStaticGroup = createAsyncThunk(`${sliceName}/removeStaticGroup`, (groupName, { dispatch }) =>
|
|
195
|
+
GeneralApi.delete(`${inventoryApiUrl}/groups/${groupName}`).then(() =>
|
|
196
|
+
Promise.all([
|
|
197
|
+
dispatch(actions.removeGroup(groupName)),
|
|
198
|
+
dispatch(getGroups()),
|
|
199
|
+
dispatch(setSnackbar('Group was removed successfully', TIMEOUTS.fiveSeconds))
|
|
200
|
+
])
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
export const getDynamicGroups = createAsyncThunk(`${sliceName}/getDynamicGroups`, (_, { dispatch, getState }) =>
|
|
205
|
+
GeneralApi.get(`${inventoryApiUrlV2}/filters?per_page=${MAX_PAGE_SIZE}`)
|
|
206
|
+
.then(({ data: filters }) => {
|
|
207
|
+
const state = getGroupsById(getState());
|
|
208
|
+
const staticGroups = Object.entries(state).reduce((accu, [id, group]) => {
|
|
209
|
+
if (!(group.id || group.filters?.length)) {
|
|
210
|
+
accu[id] = group;
|
|
211
|
+
}
|
|
212
|
+
return accu;
|
|
213
|
+
}, {});
|
|
214
|
+
const groups = (filters || []).reduce((accu, filter) => {
|
|
215
|
+
accu[filter.name] = {
|
|
216
|
+
deviceIds: [],
|
|
217
|
+
total: 0,
|
|
218
|
+
...state[filter.name],
|
|
219
|
+
id: filter.id,
|
|
220
|
+
filters: mapTermsToFilters(filter.terms)
|
|
221
|
+
};
|
|
222
|
+
return accu;
|
|
223
|
+
}, staticGroups);
|
|
224
|
+
return Promise.resolve(dispatch(actions.receivedGroups(groups)));
|
|
225
|
+
})
|
|
226
|
+
.catch(() => console.log('Dynamic group retrieval failed - likely accessing a non-enterprise backend'))
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
export const addDynamicGroup = createAsyncThunk(`${sliceName}/addDynamicGroup`, ({ groupName, filterPredicates }, { dispatch, getState }) =>
|
|
230
|
+
GeneralApi.post(`${inventoryApiUrlV2}/filters`, { name: groupName, terms: mapFiltersToTerms(filterPredicates) })
|
|
231
|
+
.then(res =>
|
|
232
|
+
Promise.resolve(
|
|
233
|
+
dispatch(
|
|
234
|
+
actions.addGroup({
|
|
235
|
+
groupName,
|
|
236
|
+
group: {
|
|
237
|
+
id: res.headers[headerNames.location].substring(res.headers[headerNames.location].lastIndexOf('/') + 1),
|
|
238
|
+
filters: filterPredicates
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
)
|
|
242
|
+
).then(() => {
|
|
243
|
+
const { cleanedFilters } = getGroupFilters(groupName, getState().devices.groups);
|
|
244
|
+
return Promise.all([
|
|
245
|
+
dispatch(actions.setDeviceFilters(cleanedFilters)),
|
|
246
|
+
dispatch(setSnackbar(...getGroupNotification(groupName, getState().devices.groups.selectedGroup))),
|
|
247
|
+
dispatch(getDynamicGroups())
|
|
248
|
+
]);
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
.catch(err => commonErrorHandler(err, `Group could not be updated:`, dispatch))
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
export const updateDynamicGroup = createAsyncThunk(`${sliceName}/updateDynamicGroup`, ({ groupName, filterPredicates }, { dispatch, getState }) => {
|
|
255
|
+
const filterId = getState().devices.groups.byId[groupName].id;
|
|
256
|
+
return GeneralApi.delete(`${inventoryApiUrlV2}/filters/${filterId}`).then(() => Promise.resolve(dispatch(addDynamicGroup({ groupName, filterPredicates }))));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export const removeDynamicGroup = createAsyncThunk(`${sliceName}/removeDynamicGroup`, (groupName, { dispatch, getState }) => {
|
|
260
|
+
const filterId = getState().devices.groups.byId[groupName].id;
|
|
261
|
+
return GeneralApi.delete(`${inventoryApiUrlV2}/filters/${filterId}`).then(() =>
|
|
262
|
+
Promise.all([dispatch(actions.removeGroup(groupName)), dispatch(setSnackbar('Group was removed successfully', TIMEOUTS.fiveSeconds))])
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
/*
|
|
267
|
+
* Device inventory functions
|
|
268
|
+
*/
|
|
269
|
+
const getGroupFilters = (group, groupsState, filters = []) => {
|
|
270
|
+
const groupName = group === UNGROUPED_GROUP.id || group === UNGROUPED_GROUP.name ? UNGROUPED_GROUP.id : group;
|
|
271
|
+
const selectedGroup = groupsState.byId[groupName];
|
|
272
|
+
const groupFilterLength = selectedGroup?.filters?.length || 0;
|
|
273
|
+
const cleanedFilters = groupFilterLength ? [...filters, ...selectedGroup.filters].filter(filtersFilter) : filters;
|
|
274
|
+
return { cleanedFilters, groupName, selectedGroup, groupFilterLength };
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const selectGroup = createAsyncThunk(`${sliceName}/selectGroup`, ({ group, filters = [] }, { dispatch, getState }) => {
|
|
278
|
+
const { cleanedFilters, groupName, selectedGroup, groupFilterLength } = getGroupFilters(group, getState().devices.groups, filters);
|
|
279
|
+
if (getSelectedGroup(getState()) === groupName && ((filters.length === 0 && !groupFilterLength) || filters.length === cleanedFilters.length)) {
|
|
280
|
+
return Promise.resolve();
|
|
281
|
+
}
|
|
282
|
+
let tasks = [];
|
|
283
|
+
if (groupFilterLength) {
|
|
284
|
+
tasks.push(dispatch(actions.setDeviceFilters(cleanedFilters)));
|
|
285
|
+
} else {
|
|
286
|
+
tasks.push(dispatch(actions.setDeviceFilters(filters)));
|
|
287
|
+
tasks.push(dispatch(getGroupDevices({ group: groupName, perPage: 1, shouldIncludeAllStates: true })));
|
|
288
|
+
}
|
|
289
|
+
const selectedGroupName = selectedGroup || !Object.keys(getGroupsById(getState())).length ? groupName : undefined;
|
|
290
|
+
tasks.push(dispatch(actions.selectGroup(selectedGroupName)));
|
|
291
|
+
return Promise.all(tasks);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const getEarliestTs = (dateA = '', dateB = '') => (!dateA || !dateB ? dateA || dateB : dateA < dateB ? dateA : dateB);
|
|
295
|
+
|
|
296
|
+
const reduceReceivedDevices = (devices, ids, state, status) =>
|
|
297
|
+
devices.reduce(
|
|
298
|
+
(accu, device) => {
|
|
299
|
+
const stateDevice = getDeviceByIdSelector(state, device.id);
|
|
300
|
+
const {
|
|
301
|
+
attributes: storedAttributes = {},
|
|
302
|
+
identity_data: storedIdentity = {},
|
|
303
|
+
monitor: storedMonitor = {},
|
|
304
|
+
tags: storedTags = {},
|
|
305
|
+
group: storedGroup
|
|
306
|
+
} = stateDevice;
|
|
307
|
+
const { identity, inventory, monitor, system = {}, tags } = mapDeviceAttributes(device.attributes);
|
|
308
|
+
device.tags = { ...storedTags, ...tags };
|
|
309
|
+
device.group = system.group ?? storedGroup;
|
|
310
|
+
device.monitor = { ...storedMonitor, ...monitor };
|
|
311
|
+
device.identity_data = { ...storedIdentity, ...identity, ...(device.identity_data ? device.identity_data : {}) };
|
|
312
|
+
device.status = status ? status : device.status || identity.status;
|
|
313
|
+
device.check_in_time_rounded = system.check_in_time ?? stateDevice.check_in_time_rounded;
|
|
314
|
+
device.check_in_time_exact = device.check_in_time ?? stateDevice.check_in_time_exact;
|
|
315
|
+
device.created_ts = getEarliestTs(getEarliestTs(system.created_ts, device.created_ts), stateDevice.created_ts);
|
|
316
|
+
device.updated_ts = device.attributes ? device.updated_ts : stateDevice.updated_ts;
|
|
317
|
+
device.isNew = new Date(device.created_ts) > new Date(state.app.newThreshold);
|
|
318
|
+
device.isOffline = new Date(device.check_in_time_rounded) < new Date(state.app.offlineThreshold) || device.check_in_time_rounded === undefined;
|
|
319
|
+
// all the other mapped attributes return as empty objects if there are no attributes to map, but identity will be initialized with an empty state
|
|
320
|
+
// for device_type and artifact_name, potentially overwriting existing info, so rely on stored information instead if there are no attributes
|
|
321
|
+
device.attributes = device.attributes ? { ...storedAttributes, ...inventory } : storedAttributes;
|
|
322
|
+
accu.devicesById[device.id] = { ...stateDevice, ...device };
|
|
323
|
+
accu.ids.push(device.id);
|
|
324
|
+
return accu;
|
|
325
|
+
},
|
|
326
|
+
{ ids, devicesById: {} }
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
export const getGroupDevices = createAsyncThunk(`${sliceName}/getGroupDevices`, (options, { dispatch, getState }) => {
|
|
330
|
+
const { group, shouldIncludeAllStates, ...remainder } = options;
|
|
331
|
+
const { cleanedFilters: filterSelection } = getGroupFilters(group, getState().devices.groups);
|
|
332
|
+
return Promise.resolve(
|
|
333
|
+
dispatch(getDevicesByStatus({ ...remainder, filterSelection, group, status: shouldIncludeAllStates ? undefined : DEVICE_STATES.accepted }))
|
|
334
|
+
)
|
|
335
|
+
.unwrap()
|
|
336
|
+
.then(results => {
|
|
337
|
+
if (!group) {
|
|
338
|
+
return Promise.resolve();
|
|
339
|
+
}
|
|
340
|
+
const { deviceAccu, total } = results[results.length - 1];
|
|
341
|
+
const stateGroup = getState().devices.groups.byId[group];
|
|
342
|
+
if (!stateGroup && !total && !deviceAccu.ids.length) {
|
|
343
|
+
return Promise.resolve();
|
|
344
|
+
}
|
|
345
|
+
return Promise.resolve(
|
|
346
|
+
dispatch(
|
|
347
|
+
actions.addGroup({
|
|
348
|
+
group: {
|
|
349
|
+
deviceIds: deviceAccu.ids.length === total || deviceAccu.ids.length > stateGroup?.deviceIds ? deviceAccu.ids : stateGroup.deviceIds,
|
|
350
|
+
total
|
|
351
|
+
},
|
|
352
|
+
groupName: group
|
|
353
|
+
})
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
export const getAllGroupDevices = createAsyncThunk(`${sliceName}/getAllGroupDevices`, (group, { dispatch, getState }) => {
|
|
360
|
+
if (!group || (!!group && (!getGroupsById(getState())[group] || getGroupsById(getState())[group].filters.length))) {
|
|
361
|
+
return Promise.resolve();
|
|
362
|
+
}
|
|
363
|
+
const { attributes, filterTerms } = prepareSearchArguments({
|
|
364
|
+
filters: [],
|
|
365
|
+
group,
|
|
366
|
+
state: getState(),
|
|
367
|
+
status: DEVICE_STATES.accepted
|
|
368
|
+
});
|
|
369
|
+
const getAllDevices = (perPage = MAX_PAGE_SIZE, page = defaultPage, devices = []) =>
|
|
370
|
+
GeneralApi.post(getSearchEndpoint(getState()), {
|
|
371
|
+
page,
|
|
372
|
+
per_page: perPage,
|
|
373
|
+
filters: filterTerms,
|
|
374
|
+
attributes
|
|
375
|
+
}).then(res => {
|
|
376
|
+
const state = getState();
|
|
377
|
+
const deviceAccu = reduceReceivedDevices(res.data, devices, state);
|
|
378
|
+
dispatch(actions.receivedDevices(deviceAccu.devicesById));
|
|
379
|
+
const total = Number(res.headers[headerNames.total]);
|
|
380
|
+
if (total > perPage * page) {
|
|
381
|
+
return getAllDevices(perPage, page + 1, deviceAccu.ids);
|
|
382
|
+
}
|
|
383
|
+
return Promise.resolve(dispatch(actions.addGroup({ group: { deviceIds: deviceAccu.ids, total: deviceAccu.ids.length }, groupName: group })));
|
|
384
|
+
});
|
|
385
|
+
return getAllDevices();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
export const getAllDynamicGroupDevices = createAsyncThunk(`${sliceName}/getAllDynamicGroupDevices`, (group, { dispatch, getState }) => {
|
|
389
|
+
if (!!group && (!getGroupsById(getState())[group] || !getGroupsById(getState())[group].filters.length)) {
|
|
390
|
+
return Promise.resolve();
|
|
391
|
+
}
|
|
392
|
+
const { attributes, filterTerms: filters } = prepareSearchArguments({
|
|
393
|
+
filters: getState().devices.groups.byId[group].filters,
|
|
394
|
+
state: getState(),
|
|
395
|
+
status: DEVICE_STATES.accepted
|
|
396
|
+
});
|
|
397
|
+
const getAllDevices = (perPage = MAX_PAGE_SIZE, page = defaultPage, devices = []) =>
|
|
398
|
+
GeneralApi.post(getSearchEndpoint(getState()), { page, per_page: perPage, filters, attributes }).then(res => {
|
|
399
|
+
const state = getState();
|
|
400
|
+
const deviceAccu = reduceReceivedDevices(res.data, devices, state);
|
|
401
|
+
dispatch(actions.receivedDevices(deviceAccu.devicesById));
|
|
402
|
+
const total = Number(res.headers[headerNames.total]);
|
|
403
|
+
if (total > deviceAccu.ids.length) {
|
|
404
|
+
return getAllDevices(perPage, page + 1, deviceAccu.ids);
|
|
405
|
+
}
|
|
406
|
+
return Promise.resolve(dispatch(actions.addGroup({ group: { deviceIds: deviceAccu.ids, total }, groupName: group })));
|
|
407
|
+
});
|
|
408
|
+
return getAllDevices();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
export const getDeviceById = createAsyncThunk(`${sliceName}/getDeviceById`, (id, { dispatch, getState }) =>
|
|
412
|
+
GeneralApi.get(`${inventoryApiUrl}/devices/${id}`)
|
|
413
|
+
.then(res => {
|
|
414
|
+
const device = reduceReceivedDevices([res.data], [], getState()).devicesById[id];
|
|
415
|
+
device.etag = res.headers.etag;
|
|
416
|
+
dispatch(actions.receivedDevice(device));
|
|
417
|
+
return Promise.resolve(device);
|
|
418
|
+
})
|
|
419
|
+
.catch(err => {
|
|
420
|
+
const errMsg = extractErrorMessage(err);
|
|
421
|
+
if (errMsg.includes('Not Found')) {
|
|
422
|
+
console.log(`${id} does not have any inventory information`);
|
|
423
|
+
const device = reduceReceivedDevices(
|
|
424
|
+
[
|
|
425
|
+
{
|
|
426
|
+
id,
|
|
427
|
+
attributes: [
|
|
428
|
+
{ name: 'status', value: 'decomissioned', scope: 'identity' },
|
|
429
|
+
{ name: 'decomissioned', value: 'true', scope: 'inventory' }
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
],
|
|
433
|
+
[],
|
|
434
|
+
getState()
|
|
435
|
+
).devicesById[id];
|
|
436
|
+
dispatch(actions.receivedDevice(device));
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
export const getDeviceInfo = createAsyncThunk(`${sliceName}/getDeviceInfo`, (deviceId, { dispatch, getState }) => {
|
|
442
|
+
const device = getDeviceByIdSelector(getState(), deviceId);
|
|
443
|
+
const { hasDeviceConfig, hasDeviceConnect, hasMonitor } = getTenantCapabilities(getState());
|
|
444
|
+
const { canConfigure } = getUserCapabilities(getState());
|
|
445
|
+
const integrations = getDeviceTwinIntegrations(getState());
|
|
446
|
+
let tasks = [dispatch(getDeviceAuth(deviceId)), ...integrations.map(integration => dispatch(getDeviceTwin({ deviceId, integration })))];
|
|
447
|
+
if (hasDeviceConfig && canConfigure && [DEVICE_STATES.accepted, DEVICE_STATES.preauth].includes(device.status)) {
|
|
448
|
+
tasks.push(dispatch(getDeviceConfig(deviceId)));
|
|
449
|
+
}
|
|
450
|
+
if (device.status === DEVICE_STATES.accepted) {
|
|
451
|
+
// Get full device identity details for single selected device
|
|
452
|
+
tasks.push(dispatch(getDeviceById(deviceId)));
|
|
453
|
+
if (hasDeviceConnect) {
|
|
454
|
+
tasks.push(dispatch(getDeviceConnect(deviceId)));
|
|
455
|
+
}
|
|
456
|
+
if (hasMonitor) {
|
|
457
|
+
tasks.push(dispatch(getLatestDeviceAlerts({ id: deviceId })));
|
|
458
|
+
tasks.push(dispatch(getDeviceMonitorConfig(deviceId)));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return Promise.all(tasks);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
export const deriveInactiveDevices = createAsyncThunk(`${sliceName}/deriveInactiveDevices`, (deviceIds, { dispatch, getState }) => {
|
|
465
|
+
const yesterday = new Date();
|
|
466
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
467
|
+
const yesterdaysIsoString = yesterday.toISOString();
|
|
468
|
+
// now boil the list down to the ones that were not updated since yesterday
|
|
469
|
+
const devices = deviceIds.reduce(
|
|
470
|
+
(accu, id) => {
|
|
471
|
+
const device = getDeviceByIdSelector(getState(), id);
|
|
472
|
+
if (device && device.updated_ts > yesterdaysIsoString) {
|
|
473
|
+
accu.active.push(id);
|
|
474
|
+
} else {
|
|
475
|
+
accu.inactive.push(id);
|
|
476
|
+
}
|
|
477
|
+
return accu;
|
|
478
|
+
},
|
|
479
|
+
{ active: [], inactive: [] }
|
|
480
|
+
);
|
|
481
|
+
return dispatch(actions.setInactiveDevices({ activeDeviceTotal: devices.active.length, inactiveDeviceTotal: devices.inactive.length }));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
/*
|
|
485
|
+
Device Auth + admission
|
|
486
|
+
*/
|
|
487
|
+
export const getDeviceCount = createAsyncThunk(`${sliceName}/getDeviceCount`, (status, { dispatch, getState }) =>
|
|
488
|
+
GeneralApi.post(getSearchEndpoint(getState()), {
|
|
489
|
+
page: 1,
|
|
490
|
+
per_page: 1,
|
|
491
|
+
filters: mapFiltersToTerms([{ key: 'status', value: status, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'identity' }]),
|
|
492
|
+
attributes: defaultAttributes
|
|
493
|
+
}).then(response => {
|
|
494
|
+
const count = Number(response.headers[headerNames.total]);
|
|
495
|
+
if (status) {
|
|
496
|
+
return dispatch(actions.setDevicesCountByStatus({ count, status }));
|
|
497
|
+
}
|
|
498
|
+
return dispatch(actions.setTotalDevices(count));
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
export const getAllDeviceCounts = createAsyncThunk(`${sliceName}/getAllDeviceCounts`, (_, { dispatch }) =>
|
|
503
|
+
Promise.all([DEVICE_STATES.accepted, DEVICE_STATES.pending].map(status => dispatch(getDeviceCount(status))))
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
export const getDeviceLimit = createAsyncThunk(`${sliceName}/getDeviceLimit`, (_, { dispatch }) =>
|
|
507
|
+
GeneralApi.get(`${deviceAuthV2}/limits/max_devices`).then(res => dispatch(actions.setDeviceLimit(res.data.limit)))
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
export const setDeviceListState = createAsyncThunk(
|
|
511
|
+
`${sliceName}/setDeviceListState`,
|
|
512
|
+
({ shouldSelectDevices = true, forceRefresh, fetchAuth = true, ...selectionState }, { dispatch, getState }) => {
|
|
513
|
+
const currentState = getDeviceListState(getState());
|
|
514
|
+
const refreshTrigger = forceRefresh ? !currentState.refreshTrigger : selectionState.refreshTrigger;
|
|
515
|
+
let nextState = {
|
|
516
|
+
...currentState,
|
|
517
|
+
setOnly: false,
|
|
518
|
+
refreshTrigger,
|
|
519
|
+
...selectionState,
|
|
520
|
+
sort: { ...currentState.sort, ...selectionState.sort }
|
|
521
|
+
};
|
|
522
|
+
let tasks = [];
|
|
523
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
524
|
+
const { isLoading: currentLoading, deviceIds: currentDevices, selection: currentSelection, ...currentRequestState } = currentState;
|
|
525
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
526
|
+
const { isLoading: nextLoading, deviceIds: nextDevices, selection: nextSelection, ...nextRequestState } = nextState;
|
|
527
|
+
if (!nextState.setOnly && !deepCompare(currentRequestState, nextRequestState)) {
|
|
528
|
+
const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol, scope: sortScope } = nextState.sort ?? {};
|
|
529
|
+
const sortBy = sortCol ? [{ attribute: sortCol, order: sortDown, scope: sortScope }] : undefined;
|
|
530
|
+
const applicableSelectedState = nextState.state === ALL_DEVICE_STATES ? undefined : nextState.state;
|
|
531
|
+
nextState.isLoading = true;
|
|
532
|
+
tasks.push(
|
|
533
|
+
dispatch(getDevicesByStatus({ ...nextState, status: applicableSelectedState, sortOptions: sortBy, fetchAuth }))
|
|
534
|
+
.unwrap()
|
|
535
|
+
.then(results => {
|
|
536
|
+
const { deviceAccu, total } = results[results.length - 1];
|
|
537
|
+
const devicesState = shouldSelectDevices ? { deviceIds: deviceAccu.ids, total, isLoading: false } : { isLoading: false };
|
|
538
|
+
return Promise.resolve(dispatch(actions.setDeviceListState(devicesState)));
|
|
539
|
+
})
|
|
540
|
+
// whatever happens, change "loading" back to null
|
|
541
|
+
.catch(() => Promise.resolve({ isLoading: false }))
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
tasks.push(dispatch(actions.setDeviceListState(nextState)));
|
|
545
|
+
return Promise.all(tasks);
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// get devices from inventory
|
|
550
|
+
export const getDevicesByStatus = createAsyncThunk(`${sliceName}/getDevicesByStatus`, (options, { dispatch, getState }) => {
|
|
551
|
+
const {
|
|
552
|
+
status,
|
|
553
|
+
fetchAuth = true,
|
|
554
|
+
filterSelection,
|
|
555
|
+
group,
|
|
556
|
+
selectedIssues = [],
|
|
557
|
+
page = defaultPage,
|
|
558
|
+
perPage = defaultPerPage,
|
|
559
|
+
sortOptions = [],
|
|
560
|
+
selectedAttributes = []
|
|
561
|
+
} = options;
|
|
562
|
+
const state = getState();
|
|
563
|
+
const { applicableFilters, filterTerms } = convertDeviceListStateToFilters({
|
|
564
|
+
filters: filterSelection ?? getDeviceFilters(state),
|
|
565
|
+
group: group ?? getSelectedGroup(state),
|
|
566
|
+
groups: state.devices.groups,
|
|
567
|
+
offlineThreshold: state.app.offlineThreshold,
|
|
568
|
+
selectedIssues,
|
|
569
|
+
status
|
|
570
|
+
});
|
|
571
|
+
const attributes = [...defaultAttributes, getIdAttribute(getState()), ...selectedAttributes];
|
|
572
|
+
return GeneralApi.post(getSearchEndpoint(getState()), {
|
|
573
|
+
page,
|
|
574
|
+
per_page: perPage,
|
|
575
|
+
filters: filterTerms,
|
|
576
|
+
sort: sortOptions,
|
|
577
|
+
attributes
|
|
578
|
+
})
|
|
579
|
+
.then(response => {
|
|
580
|
+
const state = getState();
|
|
581
|
+
const deviceAccu = reduceReceivedDevices(response.data, [], state, status);
|
|
582
|
+
let total = !applicableFilters.length ? Number(response.headers[headerNames.total]) : null;
|
|
583
|
+
if (status && state.devices.byStatus[status].total === deviceAccu.ids.length) {
|
|
584
|
+
total = deviceAccu.ids.length;
|
|
585
|
+
}
|
|
586
|
+
let tasks = [dispatch(actions.receivedDevices(deviceAccu.devicesById))];
|
|
587
|
+
if (status) {
|
|
588
|
+
tasks.push(dispatch(actions.setDevicesByStatus({ deviceIds: deviceAccu.ids, status, total })));
|
|
589
|
+
}
|
|
590
|
+
// for each device, get device identity info
|
|
591
|
+
const receivedDevices = Object.values(deviceAccu.devicesById);
|
|
592
|
+
if (receivedDevices.length && fetchAuth) {
|
|
593
|
+
tasks.push(dispatch(getDevicesWithAuth(receivedDevices)));
|
|
594
|
+
}
|
|
595
|
+
tasks.push(Promise.resolve({ deviceAccu, total: Number(response.headers[headerNames.total]) }));
|
|
596
|
+
return Promise.all(tasks);
|
|
597
|
+
})
|
|
598
|
+
.catch(err => commonErrorHandler(err, `${status} devices couldn't be loaded.`, dispatch, commonErrorFallback));
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
export const getAllDevicesByStatus = createAsyncThunk(`${sliceName}/getAllDevicesByStatus`, (status, { dispatch, getState }) => {
|
|
602
|
+
const attributes = [...defaultAttributes, getIdAttribute(getState())];
|
|
603
|
+
const getAllDevices = (perPage = MAX_PAGE_SIZE, page = 1, devices = []) =>
|
|
604
|
+
GeneralApi.post(getSearchEndpoint(getState()), {
|
|
605
|
+
page,
|
|
606
|
+
per_page: perPage,
|
|
607
|
+
filters: mapFiltersToTerms([{ key: 'status', value: status, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'identity' }]),
|
|
608
|
+
attributes
|
|
609
|
+
}).then(res => {
|
|
610
|
+
const state = getState();
|
|
611
|
+
const deviceAccu = reduceReceivedDevices(res.data, devices, state, status);
|
|
612
|
+
dispatch(actions.receivedDevices(deviceAccu.devicesById));
|
|
613
|
+
const total = Number(res.headers[headerNames.total]);
|
|
614
|
+
if (total > state.deployments.deploymentDeviceLimit) {
|
|
615
|
+
return Promise.resolve();
|
|
616
|
+
}
|
|
617
|
+
if (total > perPage * page) {
|
|
618
|
+
return getAllDevices(perPage, page + 1, deviceAccu.ids);
|
|
619
|
+
}
|
|
620
|
+
let tasks = [dispatch(actions.setDevicesByStatus({ deviceIds: deviceAccu.ids, forceUpdate: true, status, total: deviceAccu.ids.length }))];
|
|
621
|
+
if (status === DEVICE_STATES.accepted && deviceAccu.ids.length === total) {
|
|
622
|
+
tasks.push(dispatch(deriveInactiveDevices(deviceAccu.ids)));
|
|
623
|
+
tasks.push(dispatch(deriveReportsData()));
|
|
624
|
+
}
|
|
625
|
+
return Promise.all(tasks);
|
|
626
|
+
});
|
|
627
|
+
return getAllDevices();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
export const searchDevices = createAsyncThunk(`${sliceName}/searchDevices`, (passedOptions = {}, { dispatch, getState }) => {
|
|
631
|
+
const state = getState();
|
|
632
|
+
let options = { ...state.app.searchState, ...passedOptions };
|
|
633
|
+
const { page = defaultPage, searchTerm, sortOptions = [] } = options;
|
|
634
|
+
const { columnSelection = [] } = getUserSettings(state);
|
|
635
|
+
const selectedAttributes = columnSelection.map(column => ({ attribute: column.key, scope: column.scope }));
|
|
636
|
+
const attributes = attributeDuplicateFilter([...defaultAttributes, getIdAttribute(state), ...selectedAttributes], 'attribute');
|
|
637
|
+
return GeneralApi.post(getSearchEndpoint(getState()), {
|
|
638
|
+
page,
|
|
639
|
+
per_page: 10,
|
|
640
|
+
filters: [],
|
|
641
|
+
sort: sortOptions,
|
|
642
|
+
text: searchTerm,
|
|
643
|
+
attributes
|
|
644
|
+
})
|
|
645
|
+
.then(response => {
|
|
646
|
+
const deviceAccu = reduceReceivedDevices(response.data, [], getState());
|
|
647
|
+
return Promise.all([
|
|
648
|
+
dispatch(actions.receivedDevices(deviceAccu.devicesById)),
|
|
649
|
+
Promise.resolve({ deviceIds: deviceAccu.ids, searchTotal: Number(response.headers[headerNames.total]) })
|
|
650
|
+
]);
|
|
651
|
+
})
|
|
652
|
+
.catch(err => commonErrorHandler(err, `devices couldn't be searched.`, dispatch, commonErrorFallback));
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const ATTRIBUTE_LIST_CUTOFF = 100;
|
|
656
|
+
const attributeReducer = (attributes = []) =>
|
|
657
|
+
attributes.slice(0, ATTRIBUTE_LIST_CUTOFF).reduce(
|
|
658
|
+
(accu, { name, scope }) => {
|
|
659
|
+
if (!accu[scope]) {
|
|
660
|
+
accu[scope] = [];
|
|
661
|
+
}
|
|
662
|
+
accu[scope].push(name);
|
|
663
|
+
return accu;
|
|
664
|
+
},
|
|
665
|
+
{ identity: [], inventory: [], system: [], tags: [] }
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
export const getDeviceAttributes = createAsyncThunk(`${sliceName}/getDeviceAttributes`, (_, { dispatch, getState }) =>
|
|
669
|
+
GeneralApi.get(getAttrsEndpoint(getState())).then(({ data }) => {
|
|
670
|
+
// TODO: remove the array fallback once the inventory attributes endpoint is fixed
|
|
671
|
+
const { identity: identityAttributes, inventory: inventoryAttributes, system: systemAttributes, tags: tagAttributes } = attributeReducer(data || []);
|
|
672
|
+
return dispatch(actions.setFilterAttributes({ identityAttributes, inventoryAttributes, systemAttributes, tagAttributes }));
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
export const getReportingLimits = createAsyncThunk(`${sliceName}/getReportingLimits`, (_, { dispatch }) =>
|
|
677
|
+
GeneralApi.get(`${reportingApiUrl}/devices/attributes`)
|
|
678
|
+
.catch(err => commonErrorHandler(err, `filterable attributes limit & usage could not be retrieved.`, dispatch, commonErrorFallback))
|
|
679
|
+
.then(({ data }) => {
|
|
680
|
+
const { attributes, count, limit } = data;
|
|
681
|
+
const groupedAttributes = attributeReducer(attributes);
|
|
682
|
+
return Promise.resolve(dispatch(actions.setFilterablesConfig({ count, limit, attributes: groupedAttributes })));
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
export const ensureVersionString = (software, fallback) =>
|
|
687
|
+
software.length && software !== 'artifact_name' ? (software.endsWith('.version') ? software : `${software}.version`) : fallback;
|
|
688
|
+
|
|
689
|
+
const REPORT_CHART_SIZE_LIMIT = 6;
|
|
690
|
+
const getSingleReportData = (reportConfig, groups) => {
|
|
691
|
+
const { attribute, group, software = '' } = reportConfig;
|
|
692
|
+
const filters = [{ key: 'status', scope: 'identity', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: 'accepted' }];
|
|
693
|
+
if (group) {
|
|
694
|
+
const staticGroupFilter = { key: 'group', scope: 'system', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: group };
|
|
695
|
+
const { cleanedFilters: groupFilters } = getGroupFilters(group, groups);
|
|
696
|
+
filters.push(...(groupFilters.length ? groupFilters : [staticGroupFilter]));
|
|
697
|
+
}
|
|
698
|
+
const aggregationAttribute = ensureVersionString(software, attribute);
|
|
699
|
+
return GeneralApi.post(`${reportingApiUrl}/devices/aggregate`, {
|
|
700
|
+
aggregations: [{ attribute: aggregationAttribute, name: '*', scope: 'inventory', size: REPORT_CHART_SIZE_LIMIT }],
|
|
701
|
+
filters: mapFiltersToTerms(filters)
|
|
702
|
+
}).then(({ data }) => ({ data, reportConfig }));
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
export const getReportsData = createAsyncThunk(`${sliceName}/getReportsData`, (_, { dispatch, getState }) => {
|
|
706
|
+
const state = getState();
|
|
707
|
+
const currentUserId = getCurrentUser(state).id;
|
|
708
|
+
const reports =
|
|
709
|
+
getUserSettings(state).reports || getGlobalSettings(state)[`${currentUserId}-reports`] || (Object.keys(getDevicesById(state)).length ? defaultReports : []);
|
|
710
|
+
return Promise.all(reports.map(report => getSingleReportData(report, getState().devices.groups))).then(results => {
|
|
711
|
+
const devicesState = getState().devices;
|
|
712
|
+
const totalDeviceCount = devicesState.byStatus.accepted.total;
|
|
713
|
+
const newReports = results.map(({ data, reportConfig }) => {
|
|
714
|
+
let { items, other_count } = data[0];
|
|
715
|
+
const { attribute, group, software = '' } = reportConfig;
|
|
716
|
+
const dataCount = items.reduce((accu, item) => accu + item.count, 0);
|
|
717
|
+
// the following is needed to show reports including both old (artifact_name) & current style (rootfs-image.version) device software
|
|
718
|
+
const otherCount = !group && (software === rootfsImageVersion || attribute === 'artifact_name') ? totalDeviceCount - dataCount : other_count;
|
|
719
|
+
return { items, otherCount, total: otherCount + dataCount };
|
|
720
|
+
});
|
|
721
|
+
return Promise.resolve(dispatch(actions.setDeviceReports(newReports)));
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
const initializeDistributionData = (report, groups, devices, totalDeviceCount) => {
|
|
726
|
+
const { attribute, group = '', software = '' } = report;
|
|
727
|
+
const effectiveAttribute = software ? software : attribute;
|
|
728
|
+
const { deviceIds, total = 0 } = groups[group] || {};
|
|
729
|
+
const relevantDevices = groups[group] ? deviceIds.map(id => devices[id]) : Object.values(devices);
|
|
730
|
+
const distributionByAttribute = relevantDevices.reduce((accu, item) => {
|
|
731
|
+
if (!item.attributes || item.status !== DEVICE_STATES.accepted) return accu;
|
|
732
|
+
if (!accu[item.attributes[effectiveAttribute]]) {
|
|
733
|
+
accu[item.attributes[effectiveAttribute]] = 0;
|
|
734
|
+
}
|
|
735
|
+
accu[item.attributes[effectiveAttribute]] = accu[item.attributes[effectiveAttribute]] + 1;
|
|
736
|
+
return accu;
|
|
737
|
+
}, {});
|
|
738
|
+
const distributionByAttributeSorted = Object.entries(distributionByAttribute).sort((pairA, pairB) => pairB[1] - pairA[1]);
|
|
739
|
+
const items = distributionByAttributeSorted.map(([key, count]) => ({ key, count }));
|
|
740
|
+
const dataCount = items.reduce((accu, item) => accu + item.count, 0);
|
|
741
|
+
// the following is needed to show reports including both old (artifact_name) & current style (rootfs-image.version) device software
|
|
742
|
+
const otherCount = (groups[group] ? total : totalDeviceCount) - dataCount;
|
|
743
|
+
return { items, otherCount, total: otherCount + dataCount };
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
export const deriveReportsData = createAsyncThunk(`${sliceName}/deriveReportsData`, (_, { dispatch, getState }) => {
|
|
747
|
+
const state = getState();
|
|
748
|
+
const {
|
|
749
|
+
groups: { byId: groupsById },
|
|
750
|
+
byId,
|
|
751
|
+
byStatus: {
|
|
752
|
+
accepted: { total }
|
|
753
|
+
}
|
|
754
|
+
} = state.devices;
|
|
755
|
+
const reports =
|
|
756
|
+
getUserSettings(state).reports || state.users.globalSettings[`${state.users.currentUser}-reports`] || (Object.keys(byId).length ? defaultReports : []);
|
|
757
|
+
const newReports = reports.map(report => initializeDistributionData(report, groupsById, byId, total));
|
|
758
|
+
return Promise.resolve(dispatch(actions.setDeviceReports(newReports)));
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
export const getReportsDataWithoutBackendSupport = createAsyncThunk(`${sliceName}/getReportsDataWithoutBackendSupport`, (_, { dispatch, getState }) =>
|
|
762
|
+
Promise.all([dispatch(getAllDevicesByStatus(DEVICE_STATES.accepted)), dispatch(getGroups()), dispatch(getDynamicGroups())]).then(() => {
|
|
763
|
+
const { dynamic: dynamicGroups, static: staticGroups } = getGroupsSelector(getState());
|
|
764
|
+
return Promise.all([
|
|
765
|
+
...staticGroups.map(({ groupId }) => dispatch(getAllGroupDevices(groupId))),
|
|
766
|
+
...dynamicGroups.map(({ groupId }) => dispatch(getAllDynamicGroupDevices(groupId)))
|
|
767
|
+
]).then(() => dispatch(deriveReportsData()));
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
export const getDeviceConnect = createAsyncThunk(`${sliceName}/getDeviceConnect`, (id, { dispatch }) =>
|
|
772
|
+
GeneralApi.get(`${deviceConnect}/devices/${id}`).then(({ data }) =>
|
|
773
|
+
Promise.all([dispatch(actions.receivedDevice({ connect_status: data.status, connect_updated_ts: data.updated_ts, id })), Promise.resolve(data)])
|
|
774
|
+
)
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
export const getSessionDetails = createAsyncThunk(`${sliceName}/getSessionDetails`, ({ sessionId, deviceId, userId, startDate, endDate }) => {
|
|
778
|
+
const createdAfter = startDate ? `&created_after=${Math.round(Date.parse(startDate) / 1000)}` : '';
|
|
779
|
+
const createdBefore = endDate ? `&created_before=${Math.round(Date.parse(endDate) / 1000)}` : '';
|
|
780
|
+
const objectSearch = `&object_id=${deviceId}`;
|
|
781
|
+
return GeneralApi.get(`${auditLogsApiUrl}/logs?per_page=500${createdAfter}${createdBefore}&actor_id=${userId}${objectSearch}`).then(
|
|
782
|
+
({ data: auditLogEntries }) => {
|
|
783
|
+
const { start, end } = auditLogEntries.reduce(
|
|
784
|
+
(accu, item) => {
|
|
785
|
+
if (item.meta?.session_id?.includes(sessionId)) {
|
|
786
|
+
accu.start = new Date(item.action.startsWith('open') ? item.time : accu.start);
|
|
787
|
+
accu.end = new Date(item.action.startsWith('close') ? item.time : accu.end);
|
|
788
|
+
}
|
|
789
|
+
return accu;
|
|
790
|
+
},
|
|
791
|
+
{ start: startDate || endDate, end: endDate || startDate }
|
|
792
|
+
);
|
|
793
|
+
return Promise.resolve({ start, end });
|
|
794
|
+
}
|
|
795
|
+
);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
export const getDeviceFileDownloadLink = createAsyncThunk(`${sliceName}/getDeviceFileDownloadLink`, ({ deviceId, path }) =>
|
|
799
|
+
Promise.resolve(`${window.location.origin}${deviceConnect}/devices/${deviceId}/download?path=${encodeURIComponent(path)}`)
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
export const deviceFileUpload = createAsyncThunk(`${sliceName}/deviceFileUpload`, ({ deviceId, path, file }, { dispatch }) => {
|
|
803
|
+
let formData = new FormData();
|
|
804
|
+
formData.append('path', path);
|
|
805
|
+
formData.append('file', file);
|
|
806
|
+
const uploadId = uuid();
|
|
807
|
+
const cancelSource = new AbortController();
|
|
808
|
+
return Promise.all([
|
|
809
|
+
dispatch(setSnackbar('Uploading file')),
|
|
810
|
+
dispatch(initUpload({ id: uploadId, upload: { inprogress: true, uploadProgress: 0, cancelSource } })),
|
|
811
|
+
GeneralApi.uploadPut(
|
|
812
|
+
`${deviceConnect}/devices/${deviceId}/upload`,
|
|
813
|
+
formData,
|
|
814
|
+
e => dispatch(uploadProgress({ id: uploadId, progress: progress(e) })),
|
|
815
|
+
cancelSource.signal
|
|
816
|
+
)
|
|
817
|
+
])
|
|
818
|
+
.then(() => Promise.resolve(dispatch(setSnackbar('Upload successful', TIMEOUTS.fiveSeconds))))
|
|
819
|
+
.catch(err => {
|
|
820
|
+
if (isCancel(err)) {
|
|
821
|
+
return dispatch(setSnackbar('The upload has been cancelled', TIMEOUTS.fiveSeconds));
|
|
822
|
+
}
|
|
823
|
+
return commonErrorHandler(err, `Error uploading file to device.`, dispatch);
|
|
824
|
+
})
|
|
825
|
+
.finally(() => dispatch(cleanUpUpload(uploadId)));
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
export const getDeviceAuth = createAsyncThunk(`${sliceName}/getDeviceAuth`, (id, { dispatch }) =>
|
|
829
|
+
dispatch(getDevicesWithAuth([{ id }]))
|
|
830
|
+
.unwrap()
|
|
831
|
+
.then(results => {
|
|
832
|
+
if (results[results.length - 1]) {
|
|
833
|
+
return Promise.resolve(results[results.length - 1][0]);
|
|
834
|
+
}
|
|
835
|
+
return Promise.resolve();
|
|
836
|
+
})
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
export const getDevicesWithAuth = createAsyncThunk(`${sliceName}/getDevicesWithAuth`, (devices, { dispatch, getState }) =>
|
|
840
|
+
devices.length
|
|
841
|
+
? GeneralApi.get(`${deviceAuthV2}/devices?id=${devices.map(device => device.id).join('&id=')}`)
|
|
842
|
+
.then(({ data: receivedDevices }) => {
|
|
843
|
+
const { devicesById } = reduceReceivedDevices(receivedDevices, [], getState());
|
|
844
|
+
return Promise.all([dispatch(actions.receivedDevices(devicesById)), Promise.resolve(receivedDevices)]);
|
|
845
|
+
})
|
|
846
|
+
.catch(err => commonErrorHandler(err, `Error: ${err}`, dispatch))
|
|
847
|
+
: Promise.resolve([[], []])
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
export const updateDeviceAuth = createAsyncThunk(`${sliceName}/updateDeviceAuth`, ({ deviceId, authId, status }, { dispatch, getState }) =>
|
|
851
|
+
GeneralApi.put(`${deviceAuthV2}/devices/${deviceId}/auth/${authId}/status`, { status })
|
|
852
|
+
.then(() => Promise.all([dispatch(getDeviceAuth(deviceId)), dispatch(setSnackbar('Device authorization status was updated successfully'))]))
|
|
853
|
+
.catch(err => commonErrorHandler(err, 'There was a problem updating the device authorization status:', dispatch))
|
|
854
|
+
.then(() => Promise.resolve(dispatch(actions.maybeUpdateDevicesByStatus({ deviceId, authId }))))
|
|
855
|
+
.finally(() => dispatch(setDeviceListState({ refreshTrigger: !getDeviceListState(getState()).refreshTrigger })))
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
export const updateDevicesAuth = createAsyncThunk(`${sliceName}/updateDevicesAuth`, ({ deviceIds, status }, { dispatch, getState }) => {
|
|
859
|
+
let devices = getDevicesById(getState());
|
|
860
|
+
const deviceIdsWithoutAuth = deviceIds.reduce((accu, id) => (devices[id].auth_sets ? accu : [...accu, { id }]), []);
|
|
861
|
+
return dispatch(getDevicesWithAuth(deviceIdsWithoutAuth)).then(() => {
|
|
862
|
+
devices = getDevicesById(getState());
|
|
863
|
+
// for each device, get id and id of authset & make api call to accept
|
|
864
|
+
// if >1 authset, skip instead
|
|
865
|
+
const deviceAuthUpdates = deviceIds.map(id => {
|
|
866
|
+
const device = devices[id];
|
|
867
|
+
if (device.auth_sets.length !== 1) {
|
|
868
|
+
return Promise.reject();
|
|
869
|
+
}
|
|
870
|
+
// api call device.id and device.authsets[0].id
|
|
871
|
+
return dispatch(updateDeviceAuth({ authId: device.auth_sets[0].id, deviceId: device.id, status }))
|
|
872
|
+
.unwrap()
|
|
873
|
+
.catch(err => commonErrorHandler(err, 'The action was stopped as there was a problem updating a device authorization status: ', dispatch, '', false));
|
|
874
|
+
});
|
|
875
|
+
return Promise.allSettled(deviceAuthUpdates).then(results => {
|
|
876
|
+
const { skipped, count } = results.reduce(
|
|
877
|
+
(accu, item) => {
|
|
878
|
+
if (item.status === 'rejected') {
|
|
879
|
+
accu.skipped = accu.skipped + 1;
|
|
880
|
+
} else {
|
|
881
|
+
accu.count = accu.count + 1;
|
|
882
|
+
}
|
|
883
|
+
return accu;
|
|
884
|
+
},
|
|
885
|
+
{ skipped: 0, count: 0 }
|
|
886
|
+
);
|
|
887
|
+
const message = getSnackbarMessage(skipped, count);
|
|
888
|
+
// break if an error occurs, display status up til this point before error message
|
|
889
|
+
return dispatch(setSnackbar(message));
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
export const deleteAuthset = createAsyncThunk(`${sliceName}/deleteAuthset`, ({ deviceId, authId }, { dispatch, getState }) =>
|
|
895
|
+
GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}/auth/${authId}`)
|
|
896
|
+
.then(() => Promise.all([dispatch(setSnackbar('Device authorization status was updated successfully'))]))
|
|
897
|
+
.catch(err => commonErrorHandler(err, 'There was a problem updating the device authorization status:', dispatch))
|
|
898
|
+
.then(() => Promise.resolve(dispatch(actions.maybeUpdateDevicesByStatus({ deviceId, authId }))))
|
|
899
|
+
.finally(() => dispatch(setDeviceListState({ refreshTrigger: !getState().devices.deviceList.refreshTrigger })))
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
export const preauthDevice = createAsyncThunk(`${sliceName}/preauthDevice`, (authset, { dispatch }) =>
|
|
903
|
+
GeneralApi.post(`${deviceAuthV2}/devices`, authset)
|
|
904
|
+
.catch(err => {
|
|
905
|
+
if (err.response.status === 409) {
|
|
906
|
+
return Promise.reject('A device with a matching identity data set already exists');
|
|
907
|
+
}
|
|
908
|
+
return commonErrorHandler(err, 'The device could not be added:', dispatch);
|
|
909
|
+
})
|
|
910
|
+
.then(() => Promise.resolve(dispatch(setSnackbar('Device was successfully added to the preauthorization list', TIMEOUTS.fiveSeconds))))
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
export const decommissionDevice = createAsyncThunk(`${sliceName}/decommissionDevice`, ({ deviceId, authId }, { dispatch, getState }) =>
|
|
914
|
+
GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}`)
|
|
915
|
+
.then(() => Promise.resolve(dispatch(setSnackbar('Device was decommissioned successfully'))))
|
|
916
|
+
.catch(err => commonErrorHandler(err, 'There was a problem decommissioning the device:', dispatch))
|
|
917
|
+
.then(() => Promise.resolve(dispatch(actions.maybeUpdateDevicesByStatus({ deviceId, authId }))))
|
|
918
|
+
// trigger reset of device list list!
|
|
919
|
+
.finally(() => dispatch(setDeviceListState({ refreshTrigger: !getState().devices.deviceList.refreshTrigger })))
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
export const getDeviceConfig = createAsyncThunk(`${sliceName}/getDeviceConfig`, (deviceId, { dispatch }) =>
|
|
923
|
+
GeneralApi.get(`${deviceConfig}/${deviceId}`)
|
|
924
|
+
.then(({ data }) => Promise.all([dispatch(actions.receivedDevice({ id: deviceId, config: data })), Promise.resolve(data)]))
|
|
925
|
+
.catch(err => {
|
|
926
|
+
// if we get a proper error response we most likely queried a device without an existing config check-in and we can just ignore the call
|
|
927
|
+
if (err.response?.data?.error.status_code !== 404) {
|
|
928
|
+
return commonErrorHandler(err, `There was an error retrieving the configuration for device ${deviceId}.`, dispatch, commonErrorFallback);
|
|
929
|
+
}
|
|
930
|
+
})
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
export const setDeviceConfig = createAsyncThunk(`${sliceName}/setDeviceConfig`, ({ deviceId, config }, { dispatch }) =>
|
|
934
|
+
GeneralApi.put(`${deviceConfig}/${deviceId}`, config)
|
|
935
|
+
.catch(err => commonErrorHandler(err, `There was an error setting the configuration for device ${deviceId}.`, dispatch, commonErrorFallback))
|
|
936
|
+
.then(() => Promise.resolve(dispatch(getDeviceConfig(deviceId))))
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
export const applyDeviceConfig = createAsyncThunk(
|
|
940
|
+
`${sliceName}/applyDeviceConfig`,
|
|
941
|
+
({ deviceId, configDeploymentConfiguration, isDefault, config }, { dispatch, getState }) =>
|
|
942
|
+
GeneralApi.post(`${deviceConfig}/${deviceId}/deploy`, configDeploymentConfiguration)
|
|
943
|
+
.catch(err => commonErrorHandler(err, `There was an error deploying the configuration to device ${deviceId}.`, dispatch, commonErrorFallback))
|
|
944
|
+
.then(({ data }) => {
|
|
945
|
+
const device = getDeviceByIdSelector(getState(), deviceId);
|
|
946
|
+
const { canManageUsers } = getUserCapabilities(getState());
|
|
947
|
+
let tasks = [
|
|
948
|
+
dispatch(actions.receivedDevice({ ...device, config: { ...device.config, deployment_id: data.deployment_id } })),
|
|
949
|
+
new Promise(resolve => setTimeout(() => resolve(dispatch(getSingleDeployment(data.deployment_id))), TIMEOUTS.oneSecond))
|
|
950
|
+
];
|
|
951
|
+
if (isDefault && canManageUsers) {
|
|
952
|
+
const { previous } = getGlobalSettings(getState()).defaultDeviceConfig ?? {};
|
|
953
|
+
tasks.push(dispatch(saveGlobalSettings({ defaultDeviceConfig: { current: config, previous } })));
|
|
954
|
+
}
|
|
955
|
+
return Promise.all(tasks);
|
|
956
|
+
})
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
export const setDeviceTags = createAsyncThunk(`${sliceName}/setDeviceTags`, ({ deviceId, tags }, { dispatch }) =>
|
|
960
|
+
// to prevent tag set failures, retrieve the device & use the freshest etag we can get
|
|
961
|
+
Promise.resolve(dispatch(getDeviceById(deviceId))).then(device => {
|
|
962
|
+
const headers = device.etag ? { 'If-Match': device.etag } : {};
|
|
963
|
+
return GeneralApi.put(
|
|
964
|
+
`${inventoryApiUrl}/devices/${deviceId}/tags`,
|
|
965
|
+
Object.entries(tags).map(([name, value]) => ({ name, value })),
|
|
966
|
+
{ headers }
|
|
967
|
+
)
|
|
968
|
+
.catch(err => commonErrorHandler(err, `There was an error setting tags for device ${deviceId}.`, dispatch, 'Please check your connection.'))
|
|
969
|
+
.then(() => Promise.all([dispatch(actions.receivedDevice({ ...device, id: deviceId, tags })), dispatch(setSnackbar('Device name changed'))]));
|
|
970
|
+
})
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
export const getDeviceTwin = createAsyncThunk(`${sliceName}/getDeviceTwin`, ({ deviceId, integration }, { dispatch, getState }) => {
|
|
974
|
+
let providerResult = {};
|
|
975
|
+
return GeneralApi.get(`${iotManagerBaseURL}/devices/${deviceId}/state`)
|
|
976
|
+
.then(({ data }) => {
|
|
977
|
+
providerResult = { ...data, twinError: '' };
|
|
978
|
+
})
|
|
979
|
+
.catch(err => {
|
|
980
|
+
providerResult = {
|
|
981
|
+
twinError: `There was an error getting the ${EXTERNAL_PROVIDER[integration.provider].twinTitle.toLowerCase()} for device ${deviceId}. ${err}`
|
|
982
|
+
};
|
|
983
|
+
})
|
|
984
|
+
.finally(() => {
|
|
985
|
+
const device = getDeviceByIdSelector(getState(), deviceId);
|
|
986
|
+
Promise.resolve(dispatch(actions.receivedDevice({ ...device, twinsByIntegration: { ...device.twinsByIntegration, ...providerResult } })));
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
export const setDeviceTwin = createAsyncThunk(`${sliceName}/setDeviceTwin`, ({ deviceId, integration, settings }, { dispatch, getState }) =>
|
|
991
|
+
GeneralApi.put(`${iotManagerBaseURL}/devices/${deviceId}/state/${integration.id}`, { desired: settings })
|
|
992
|
+
.catch(err =>
|
|
993
|
+
commonErrorHandler(
|
|
994
|
+
err,
|
|
995
|
+
`There was an error updating the ${EXTERNAL_PROVIDER[integration.provider].twinTitle.toLowerCase()} for device ${deviceId}.`,
|
|
996
|
+
dispatch
|
|
997
|
+
)
|
|
998
|
+
)
|
|
999
|
+
.then(() => {
|
|
1000
|
+
const device = getDeviceByIdSelector(getState(), deviceId);
|
|
1001
|
+
const { twinsByIntegration = {} } = device;
|
|
1002
|
+
const { [integration.id]: currentState = {} } = twinsByIntegration;
|
|
1003
|
+
return Promise.resolve(
|
|
1004
|
+
dispatch(actions.receivedDevice({ ...device, twinsByIntegration: { ...twinsByIntegration, [integration.id]: { ...currentState, desired: settings } } }))
|
|
1005
|
+
);
|
|
1006
|
+
})
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
const prepareSearchArguments = ({ filters, group, state, status }) => {
|
|
1010
|
+
const { filterTerms } = convertDeviceListStateToFilters({ filters, group, offlineThreshold: state.app.offlineThreshold, selectedIssues: [], status });
|
|
1011
|
+
const { columnSelection = [] } = getUserSettings(state);
|
|
1012
|
+
const selectedAttributes = columnSelection.map(column => ({ attribute: column.key, scope: column.scope }));
|
|
1013
|
+
const attributes = [...defaultAttributes, getIdAttribute(state), ...selectedAttributes];
|
|
1014
|
+
return { attributes, filterTerms };
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
export const getSystemDevices = createAsyncThunk(`${sliceName}/getSystemDevices`, (options, { dispatch, getState }) => {
|
|
1018
|
+
const { id, page = defaultPage, perPage = defaultPerPage, sortOptions = [] } = options;
|
|
1019
|
+
const state = getState();
|
|
1020
|
+
const { hasFullFiltering } = getTenantCapabilities(state);
|
|
1021
|
+
if (!hasFullFiltering) {
|
|
1022
|
+
return Promise.resolve();
|
|
1023
|
+
}
|
|
1024
|
+
const { attributes: deviceAttributes = {} } = getDeviceByIdSelector(state, id);
|
|
1025
|
+
const { mender_gateway_system_id = '' } = deviceAttributes;
|
|
1026
|
+
const filters = [
|
|
1027
|
+
{ ...emptyFilter, key: 'mender_is_gateway', operator: DEVICE_FILTERING_OPTIONS.$ne.key, value: 'true', scope: 'inventory' },
|
|
1028
|
+
{ ...emptyFilter, key: 'mender_gateway_system_id', value: mender_gateway_system_id, scope: 'inventory' }
|
|
1029
|
+
];
|
|
1030
|
+
const { attributes, filterTerms } = prepareSearchArguments({ filters, state });
|
|
1031
|
+
return GeneralApi.post(getSearchEndpoint(getState()), {
|
|
1032
|
+
page,
|
|
1033
|
+
per_page: perPage,
|
|
1034
|
+
filters: filterTerms,
|
|
1035
|
+
sort: sortOptions,
|
|
1036
|
+
attributes
|
|
1037
|
+
})
|
|
1038
|
+
.catch(err => commonErrorHandler(err, `There was an error getting system devices device ${id}.`, dispatch, 'Please check your connection.'))
|
|
1039
|
+
.then(({ data, headers }) => {
|
|
1040
|
+
const state = getState();
|
|
1041
|
+
const { devicesById, ids } = reduceReceivedDevices(data, [], state);
|
|
1042
|
+
const device = {
|
|
1043
|
+
...getDeviceByIdSelector(state, id),
|
|
1044
|
+
systemDeviceIds: ids,
|
|
1045
|
+
systemDeviceTotal: Number(headers[headerNames.total])
|
|
1046
|
+
};
|
|
1047
|
+
return Promise.resolve(dispatch(actions.receivedDevices({ ...devicesById, [id]: device })));
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
export const getGatewayDevices = createAsyncThunk(`${sliceName}/getGatewayDevices`, (deviceId, { dispatch, getState }) => {
|
|
1052
|
+
const state = getState();
|
|
1053
|
+
const { attributes = {} } = getDeviceByIdSelector(state, deviceId);
|
|
1054
|
+
const { mender_gateway_system_id = '' } = attributes;
|
|
1055
|
+
const filters = [
|
|
1056
|
+
{ ...emptyFilter, key: 'id', operator: DEVICE_FILTERING_OPTIONS.$ne.key, value: deviceId, scope: 'identity' },
|
|
1057
|
+
{ ...emptyFilter, key: 'mender_is_gateway', value: 'true', scope: 'inventory' },
|
|
1058
|
+
{ ...emptyFilter, key: 'mender_gateway_system_id', value: mender_gateway_system_id, scope: 'inventory' }
|
|
1059
|
+
];
|
|
1060
|
+
const { attributes: attributeSelection, filterTerms } = prepareSearchArguments({ filters, state });
|
|
1061
|
+
return GeneralApi.post(getSearchEndpoint(getState()), {
|
|
1062
|
+
page: 1,
|
|
1063
|
+
per_page: MAX_PAGE_SIZE,
|
|
1064
|
+
filters: filterTerms,
|
|
1065
|
+
attributes: attributeSelection
|
|
1066
|
+
}).then(({ data }) => {
|
|
1067
|
+
const { ids } = reduceReceivedDevices(data, [], getState());
|
|
1068
|
+
let tasks = ids.map(deviceId => dispatch(getDeviceInfo(deviceId)));
|
|
1069
|
+
tasks.push(dispatch(actions.receivedDevice({ id: deviceId, gatewayIds: ids })));
|
|
1070
|
+
return Promise.all(tasks);
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
export const getDevicesInBounds = createAsyncThunk(`${sliceName}/getDevicesInBounds`, ({ bounds, group }, { dispatch, getState }) => {
|
|
1075
|
+
const state = getState();
|
|
1076
|
+
const { filterTerms } = convertDeviceListStateToFilters({
|
|
1077
|
+
group: group === ALL_DEVICES ? undefined : group,
|
|
1078
|
+
groups: state.devices.groups,
|
|
1079
|
+
status: DEVICE_STATES.accepted
|
|
1080
|
+
});
|
|
1081
|
+
return GeneralApi.post(getSearchEndpoint(getState()), {
|
|
1082
|
+
page: 1,
|
|
1083
|
+
per_page: MAX_PAGE_SIZE,
|
|
1084
|
+
filters: filterTerms,
|
|
1085
|
+
attributes: geoAttributes,
|
|
1086
|
+
geo_bounding_box_filter: {
|
|
1087
|
+
geo_bounding_box: {
|
|
1088
|
+
location: {
|
|
1089
|
+
top_left: { lat: bounds._northEast.lat, lon: bounds._southWest.lng },
|
|
1090
|
+
bottom_right: { lat: bounds._southWest.lat, lon: bounds._northEast.lng }
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}).then(({ data }) => {
|
|
1095
|
+
const { devicesById } = reduceReceivedDevices(data, [], getState());
|
|
1096
|
+
return Promise.resolve(dispatch(actions.receivedDevices(devicesById)));
|
|
1097
|
+
});
|
|
1098
|
+
});
|