@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.
Files changed (284) hide show
  1. package/.eslintrc.cjs +9 -0
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +12 -0
  4. package/actions.ts +32 -0
  5. package/api/general-api.test.ts +51 -0
  6. package/api/general-api.ts +60 -0
  7. package/api/types/AWSCredentials.ts +16 -0
  8. package/api/types/Actor.ts +35 -0
  9. package/api/types/Addon.ts +17 -0
  10. package/api/types/AddonTenantadm.ts +17 -0
  11. package/api/types/Alert.ts +34 -0
  12. package/api/types/AlertDetails.ts +23 -0
  13. package/api/types/AlertSubject.ts +23 -0
  14. package/api/types/ApiBurst.ts +21 -0
  15. package/api/types/ApiBurstTenantadm.ts +21 -0
  16. package/api/types/ApiLimits.ts +16 -0
  17. package/api/types/ApiLimitsTenantadm.ts +16 -0
  18. package/api/types/ApiQuota.ts +17 -0
  19. package/api/types/ApiQuotaTenantadm.ts +17 -0
  20. package/api/types/Artifact.ts +48 -0
  21. package/api/types/ArtifactDeployments.ts +48 -0
  22. package/api/types/ArtifactInfo.ts +11 -0
  23. package/api/types/ArtifactInfoDeployments.ts +11 -0
  24. package/api/types/ArtifactLink.ts +11 -0
  25. package/api/types/ArtifactTypeInfo.ts +13 -0
  26. package/api/types/ArtifactTypeInfoDeployments.ts +13 -0
  27. package/api/types/ArtifactUpdate.ts +10 -0
  28. package/api/types/ArtifactUploadLink.ts +15 -0
  29. package/api/types/Attribute.ts +36 -0
  30. package/api/types/AttributeInventory.ts +32 -0
  31. package/api/types/AuditLog.ts +26 -0
  32. package/api/types/AuthSet.ts +33 -0
  33. package/api/types/AuthSetDeviceauth.ts +37 -0
  34. package/api/types/AutoAuthRequest.ts +23 -0
  35. package/api/types/AzureSharedAccessSecret.ts +11 -0
  36. package/api/types/BillingInfo.ts +13 -0
  37. package/api/types/BinaryDeltaConfiguration.ts +15 -0
  38. package/api/types/BinaryDeltaConfigurationDeployments.ts +15 -0
  39. package/api/types/BinaryDeltaLimits.ts +15 -0
  40. package/api/types/BinaryDeltaLimitsDeployments.ts +15 -0
  41. package/api/types/BoundingBox.ts +11 -0
  42. package/api/types/CancelRequest.ts +10 -0
  43. package/api/types/CardData.ts +49 -0
  44. package/api/types/CardSetupData.ts +17 -0
  45. package/api/types/CheckoutData.ts +13 -0
  46. package/api/types/Configuration.ts +11 -0
  47. package/api/types/Count.ts +13 -0
  48. package/api/types/Credentials.ts +23 -0
  49. package/api/types/DBusSubsystem.ts +27 -0
  50. package/api/types/DeltaConfiguration.ts +14 -0
  51. package/api/types/DeltaConfigurationDeployments.ts +14 -0
  52. package/api/types/Deployment.ts +95 -0
  53. package/api/types/DeploymentAggregation.ts +18 -0
  54. package/api/types/DeploymentAggregationItem.ts +18 -0
  55. package/api/types/DeploymentAggregationTerm.ts +23 -0
  56. package/api/types/DeploymentAggregationTerms.ts +18 -0
  57. package/api/types/DeploymentAttributeProjection.ts +11 -0
  58. package/api/types/DeploymentAuditlogs.ts +11 -0
  59. package/api/types/DeploymentDeployments.ts +99 -0
  60. package/api/types/DeploymentFilterTerm.ts +37 -0
  61. package/api/types/DeploymentIdentifier.ts +10 -0
  62. package/api/types/DeploymentPhase.ts +23 -0
  63. package/api/types/DeploymentReporting.ts +46 -0
  64. package/api/types/DeploymentSearchTerms.ts +39 -0
  65. package/api/types/DeploymentSortTerm.ts +25 -0
  66. package/api/types/DeploymentStatistics.ts +15 -0
  67. package/api/types/DeploymentStatusStatistics.ts +54 -0
  68. package/api/types/Device.ts +29 -0
  69. package/api/types/DeviceAggregation.ts +18 -0
  70. package/api/types/DeviceAggregationItem.ts +18 -0
  71. package/api/types/DeviceAggregationTerm.ts +27 -0
  72. package/api/types/DeviceAggregationTerms.ts +22 -0
  73. package/api/types/DeviceAttribute.ts +23 -0
  74. package/api/types/DeviceAttributeProjection.ts +15 -0
  75. package/api/types/DeviceAuditlogs.ts +17 -0
  76. package/api/types/DeviceAuthEvent.ts +23 -0
  77. package/api/types/DeviceConfiguration.ts +16 -0
  78. package/api/types/DeviceDeployment.ts +11 -0
  79. package/api/types/DeviceDeployments.ts +31 -0
  80. package/api/types/DeviceDeviceauth.ts +42 -0
  81. package/api/types/DeviceFilterAttribute.ts +22 -0
  82. package/api/types/DeviceFilterTerm.ts +41 -0
  83. package/api/types/DeviceInventory.ts +19 -0
  84. package/api/types/DeviceInventoryInventory.ts +19 -0
  85. package/api/types/DeviceReporting.ts +22 -0
  86. package/api/types/DeviceSearchTerms.ts +39 -0
  87. package/api/types/DeviceSortTerm.ts +29 -0
  88. package/api/types/DeviceState.ts +32 -0
  89. package/api/types/DeviceStateDeviceconnect.ts +33 -0
  90. package/api/types/DeviceStateIot_manager.ts +14 -0
  91. package/api/types/DeviceStatus.ts +19 -0
  92. package/api/types/DeviceWithImage.ts +80 -0
  93. package/api/types/DirectUploadMetadata.ts +18 -0
  94. package/api/types/EmailVerificationCompletion.ts +13 -0
  95. package/api/types/EmailVerificationRequest.ts +13 -0
  96. package/api/types/Error.ts +17 -0
  97. package/api/types/ErrorAuditlogs.ts +13 -0
  98. package/api/types/ErrorDeployments.ts +17 -0
  99. package/api/types/ErrorDeviceauth.ts +13 -0
  100. package/api/types/ErrorDeviceconfig.ts +17 -0
  101. package/api/types/ErrorDeviceconnect.ts +17 -0
  102. package/api/types/ErrorDevicemonitor.ts +17 -0
  103. package/api/types/ErrorExt.ts +18 -0
  104. package/api/types/ErrorInventory.ts +17 -0
  105. package/api/types/ErrorIot_manager.ts +17 -0
  106. package/api/types/ErrorNotFound.ts +17 -0
  107. package/api/types/ErrorReporting.ts +18 -0
  108. package/api/types/ErrorTenantadm.ts +13 -0
  109. package/api/types/ErrorUseradm.ts +17 -0
  110. package/api/types/Event.ts +48 -0
  111. package/api/types/ExternalIdentity.ts +17 -0
  112. package/api/types/Features.ts +17 -0
  113. package/api/types/FileUpload.ts +23 -0
  114. package/api/types/Filter.ts +19 -0
  115. package/api/types/FilterAttribute.ts +21 -0
  116. package/api/types/FilterDefinition.ts +18 -0
  117. package/api/types/FilterDeployments.ts +19 -0
  118. package/api/types/FilterInventory.ts +20 -0
  119. package/api/types/FilterPredicate.ts +78 -0
  120. package/api/types/FilterPredicateDeployments.ts +48 -0
  121. package/api/types/FilterPredicateInventory.ts +76 -0
  122. package/api/types/GeoBoundingBox.ts +10 -0
  123. package/api/types/GeoBoundingBoxFilter.ts +6 -0
  124. package/api/types/GeoDistance.ts +17 -0
  125. package/api/types/GeoDistanceFilter.ts +10 -0
  126. package/api/types/GeoPoint.ts +15 -0
  127. package/api/types/Group.ts +10 -0
  128. package/api/types/HTTP.ts +19 -0
  129. package/api/types/IdentityData.ts +25 -0
  130. package/api/types/InputParameter.ts +8 -0
  131. package/api/types/Integration.ts +24 -0
  132. package/api/types/JobStatus.ts +13 -0
  133. package/api/types/Limit.ts +21 -0
  134. package/api/types/LimitDeployments.ts +21 -0
  135. package/api/types/LimitDeviceauth.ts +10 -0
  136. package/api/types/LimitUseradm.ts +11 -0
  137. package/api/types/Limits.ts +11 -0
  138. package/api/types/LineDescriptor.ts +11 -0
  139. package/api/types/LogSubsystem.ts +23 -0
  140. package/api/types/LoginOptions.ts +18 -0
  141. package/api/types/ManagementAPIConfiguration.ts +5 -0
  142. package/api/types/MenderTypes.ts +183 -0
  143. package/api/types/MonitorConfiguration.ts +22 -0
  144. package/api/types/NewConfigurationDeployment.ts +15 -0
  145. package/api/types/NewConfigurationDeploymentResponse.ts +10 -0
  146. package/api/types/NewDeployment.ts +42 -0
  147. package/api/types/NewDeploymentForGroup.ts +33 -0
  148. package/api/types/NewDeploymentPhase.ts +37 -0
  149. package/api/types/NewDeploymentPhaseDeployments.ts +20 -0
  150. package/api/types/NewDeploymentV2.ts +31 -0
  151. package/api/types/NewTenant.ts +41 -0
  152. package/api/types/Object.ts +35 -0
  153. package/api/types/OwnUserUpdate.ts +21 -0
  154. package/api/types/PasswordResetCompletion.ts +17 -0
  155. package/api/types/PasswordResetRequest.ts +13 -0
  156. package/api/types/Permission.ts +21 -0
  157. package/api/types/PermissionObject.ts +14 -0
  158. package/api/types/PermissionSet.ts +22 -0
  159. package/api/types/PermissionSetWithScope.ts +15 -0
  160. package/api/types/PersonalAccessToken.ts +29 -0
  161. package/api/types/PersonalAccessTokenRequest.ts +18 -0
  162. package/api/types/Plan.ts +19 -0
  163. package/api/types/PlanBindingDetails.ts +13 -0
  164. package/api/types/PlanChangeRequest.ts +14 -0
  165. package/api/types/PlanLimits.ts +12 -0
  166. package/api/types/PreAuthSet.ts +16 -0
  167. package/api/types/Release.ts +32 -0
  168. package/api/types/ReleaseDeployments.ts +32 -0
  169. package/api/types/ReleaseUpdate.ts +13 -0
  170. package/api/types/Releases.ts +9 -0
  171. package/api/types/ReleasesDeleteError.ts +21 -0
  172. package/api/types/ReleasesDeployments.ts +9 -0
  173. package/api/types/Role.ts +19 -0
  174. package/api/types/RolePermission.ts +25 -0
  175. package/api/types/RolePermissionObject.ts +17 -0
  176. package/api/types/RoleUseradm.ts +19 -0
  177. package/api/types/SAMLMetadata.ts +21 -0
  178. package/api/types/Scope.ts +19 -0
  179. package/api/types/SelectAttribute.ts +17 -0
  180. package/api/types/ServiceSubsystem.ts +19 -0
  181. package/api/types/Settings.ts +23 -0
  182. package/api/types/SortCriteria.ts +30 -0
  183. package/api/types/SsoObject.ts +21 -0
  184. package/api/types/Status.ts +17 -0
  185. package/api/types/StatusDeviceauth.ts +18 -0
  186. package/api/types/StatusTenantadm.ts +19 -0
  187. package/api/types/StorageLimit.ts +17 -0
  188. package/api/types/SubscriptionData.ts +8 -0
  189. package/api/types/SupportRequest.ts +17 -0
  190. package/api/types/Tag.ts +27 -0
  191. package/api/types/Tags.ts +10 -0
  192. package/api/types/TaskResult.ts +13 -0
  193. package/api/types/TaskResultCLI.ts +10 -0
  194. package/api/types/TaskResultHTTPRequest.ts +8 -0
  195. package/api/types/TaskResultHTTPResponse.ts +10 -0
  196. package/api/types/Tenant.ts +38 -0
  197. package/api/types/TenantApiLimits.ts +12 -0
  198. package/api/types/TenantApiLimitsTenantadm.ts +12 -0
  199. package/api/types/TenantIdName.ts +14 -0
  200. package/api/types/TenantIdsData.ts +10 -0
  201. package/api/types/TenantInfo.ts +14 -0
  202. package/api/types/TenantShortInfo.ts +21 -0
  203. package/api/types/TenantTenantadm.ts +38 -0
  204. package/api/types/TenantsIdName.ts +6 -0
  205. package/api/types/Update.ts +17 -0
  206. package/api/types/UpdateDeployments.ts +18 -0
  207. package/api/types/UpdateFile.ts +13 -0
  208. package/api/types/UpdateFileDeployments.ts +13 -0
  209. package/api/types/UpdateTypes.ts +8 -0
  210. package/api/types/UpgradeCompleteRequest.ts +23 -0
  211. package/api/types/User.ts +13 -0
  212. package/api/types/UserAuditlogs.ts +13 -0
  213. package/api/types/UserNew.ts +26 -0
  214. package/api/types/UserUpdate.ts +25 -0
  215. package/api/types/UserUseradm.ts +45 -0
  216. package/api/types/UserWithTenantInfo.ts +8 -0
  217. package/api/types/XDeltaArgs.ts +9 -0
  218. package/api/types/XDeltaArgsDeployments.ts +9 -0
  219. package/api/types/XDeltaArgsLimits.ts +10 -0
  220. package/api/types/XDeltaArgsLimitsDeployments.ts +10 -0
  221. package/api/users-api.ts +34 -0
  222. package/appSlice/constants.ts +166 -0
  223. package/appSlice/index.ts +159 -0
  224. package/appSlice/reducer.test.ts +97 -0
  225. package/appSlice/selectors.ts +33 -0
  226. package/appSlice/thunks.test.ts +118 -0
  227. package/appSlice/thunks.ts +148 -0
  228. package/auth.test.ts +36 -0
  229. package/auth.ts +82 -0
  230. package/commonConstants.tsx +252 -0
  231. package/commonSelectors.ts +255 -0
  232. package/constants.ts +22 -0
  233. package/deploymentsSlice/constants.ts +175 -0
  234. package/deploymentsSlice/index.ts +145 -0
  235. package/deploymentsSlice/reducer.test.ts +108 -0
  236. package/deploymentsSlice/selectors.ts +58 -0
  237. package/deploymentsSlice/thunks.test.ts +425 -0
  238. package/deploymentsSlice/thunks.ts +382 -0
  239. package/devicesSlice/constants.ts +56 -0
  240. package/devicesSlice/index.ts +207 -0
  241. package/devicesSlice/reducer.test.ts +194 -0
  242. package/devicesSlice/selectors.ts +127 -0
  243. package/devicesSlice/thunks.test.tsx +1245 -0
  244. package/devicesSlice/thunks.tsx +1098 -0
  245. package/monitorSlice/constants.ts +17 -0
  246. package/monitorSlice/index.ts +65 -0
  247. package/monitorSlice/reducer.test.ts +79 -0
  248. package/monitorSlice/selectors.ts +15 -0
  249. package/monitorSlice/thunks.test.ts +112 -0
  250. package/monitorSlice/thunks.ts +101 -0
  251. package/onboardingSlice/constants.ts +53 -0
  252. package/onboardingSlice/index.ts +60 -0
  253. package/onboardingSlice/reducer.test.ts +45 -0
  254. package/onboardingSlice/selectors.ts +15 -0
  255. package/onboardingSlice/thunks.test.ts +235 -0
  256. package/onboardingSlice/thunks.ts +142 -0
  257. package/organizationSlice/constants.ts +82 -0
  258. package/organizationSlice/index.ts +111 -0
  259. package/organizationSlice/reducer.test.ts +78 -0
  260. package/organizationSlice/selectors.ts +45 -0
  261. package/organizationSlice/thunks.test.ts +619 -0
  262. package/organizationSlice/thunks.ts +383 -0
  263. package/package.json +38 -0
  264. package/releasesSlice/constants.ts +18 -0
  265. package/releasesSlice/index.ts +116 -0
  266. package/releasesSlice/reducer.test.ts +78 -0
  267. package/releasesSlice/selectors.ts +33 -0
  268. package/releasesSlice/thunks.test.ts +389 -0
  269. package/releasesSlice/thunks.ts +379 -0
  270. package/selectors.ts +22 -0
  271. package/store.ts +89 -0
  272. package/storehooks.test.tsx +386 -0
  273. package/storehooks.ts +250 -0
  274. package/thunks.ts +21 -0
  275. package/tsconfig.json +11 -0
  276. package/tsup.config.ts +11 -0
  277. package/usersSlice/constants.ts +374 -0
  278. package/usersSlice/index.ts +151 -0
  279. package/usersSlice/reducer.test.ts +141 -0
  280. package/usersSlice/selectors.ts +60 -0
  281. package/usersSlice/thunks.test.ts +660 -0
  282. package/usersSlice/thunks.ts +773 -0
  283. package/utils.test.ts +187 -0
  284. package/utils.ts +249 -0
@@ -0,0 +1,382 @@
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 storeActions from '@northern.tech/store/actions';
18
+ import GeneralApi from '@northern.tech/store/api/general-api';
19
+ import { DEVICE_LIST_DEFAULTS, SORTING_OPTIONS, TIMEOUTS, headerNames } from '@northern.tech/store/constants';
20
+ import { getDevicesById, getGlobalSettings, getUserCapabilities } from '@northern.tech/store/selectors';
21
+ import { commonErrorHandler } from '@northern.tech/store/store';
22
+ import { getDeviceAuth, getDeviceById, saveGlobalSettings } from '@northern.tech/store/thunks';
23
+ import { mapTermsToFilters } from '@northern.tech/store/utils';
24
+ import { deepCompare, isEmpty, standardizePhases, startTimeSort } from '@northern.tech/utils/helpers';
25
+ import { createAsyncThunk } from '@reduxjs/toolkit';
26
+ import isUUID from 'validator/lib/isUUID';
27
+
28
+ import { actions, sliceName } from '.';
29
+ import { DEPLOYMENT_ROUTES, DEPLOYMENT_STATES, DEPLOYMENT_TYPES, deploymentPrototype, deploymentsApiUrl, deploymentsApiUrlV2 } from './constants';
30
+ import { getDeploymentsById, getDeploymentsByStatus as getDeploymentsByStatusSelector } from './selectors';
31
+
32
+ const { receivedDevice, setSnackbar } = storeActions;
33
+
34
+ // default per page until pagination and counting integrated
35
+ const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
36
+
37
+ export const deriveDeploymentGroup = ({ filter = {}, group, groups = [], name }) => (group || (groups.length === 1 && !isUUID(name)) ? groups[0] : filter.name);
38
+
39
+ const transformDeployments = (deployments, deploymentsById) =>
40
+ deployments.sort(startTimeSort).reduce(
41
+ (accu, item) => {
42
+ const filter = item.filter ?? {};
43
+ let deployment = {
44
+ ...deploymentPrototype,
45
+ ...deploymentsById[item.id],
46
+ ...item,
47
+ filter: item.filter ? { ...filter, name: filter.name ?? filter.id, filters: mapTermsToFilters(filter.terms) } : undefined,
48
+ name: decodeURIComponent(item.name)
49
+ };
50
+ // deriving the group in a second step to potentially make use of the merged data from the existing group state + the decoded name
51
+ deployment = { ...deployment, group: deriveDeploymentGroup(deployment) };
52
+ accu.deployments[item.id] = deployment;
53
+ accu.deploymentIds.push(item.id);
54
+ return accu;
55
+ },
56
+ { deployments: {}, deploymentIds: [] }
57
+ );
58
+
59
+ /*Deployments */
60
+ export const getDeploymentsByStatus = createAsyncThunk(`${sliceName}/getDeploymentsByStatus`, (options = {}, { dispatch, getState }) => {
61
+ const { status, page = defaultPage, per_page = defaultPerPage, startDate, endDate, group, type, shouldSelect = true, sort = SORTING_OPTIONS.desc } = options;
62
+ const created_after = startDate ? `&created_after=${startDate}` : '';
63
+ const created_before = endDate ? `&created_before=${endDate}` : '';
64
+ const search = group ? `&search=${group}` : '';
65
+ const typeFilter = type ? `&type=${type}` : '';
66
+ return GeneralApi.get(
67
+ `${deploymentsApiUrl}/deployments?status=${status}&per_page=${per_page}&page=${page}${created_after}${created_before}${search}${typeFilter}&sort=${sort}`
68
+ ).then(res => {
69
+ const { deployments, deploymentIds } = transformDeployments(res.data, getState().deployments.byId);
70
+ const total = Number(res.headers[headerNames.total]);
71
+ let tasks = [
72
+ dispatch(actions.receivedDeployments(deployments)),
73
+ dispatch(
74
+ actions.receivedDeploymentsForStatus({
75
+ deploymentIds,
76
+ status,
77
+ total: !(startDate || endDate || group || type) ? total : getState().deployments.byStatus[status].total
78
+ })
79
+ )
80
+ ];
81
+ tasks = deploymentIds.reduce((accu, deploymentId) => {
82
+ if (deployments[deploymentId].type === DEPLOYMENT_TYPES.configuration) {
83
+ accu.push(dispatch(getSingleDeployment(deploymentId)));
84
+ }
85
+ return accu;
86
+ }, tasks);
87
+ if (shouldSelect) {
88
+ tasks.push(dispatch(actions.selectDeploymentsForStatus({ deploymentIds, status, total })));
89
+ }
90
+ tasks.push({ deploymentIds, total });
91
+ return Promise.all(tasks);
92
+ });
93
+ });
94
+
95
+ const MAX_PREVIOUS_PHASES_COUNT = 5;
96
+ export const createDeployment = createAsyncThunk(`${sliceName}/createDeployment`, ({ newDeployment, hasNewRetryDefault = false }, { dispatch, getState }) => {
97
+ let request;
98
+ if (newDeployment.filter_id) {
99
+ request = GeneralApi.post(`${deploymentsApiUrlV2}/deployments`, newDeployment);
100
+ } else if (newDeployment.group) {
101
+ request = GeneralApi.post(`${deploymentsApiUrl}/deployments/group/${newDeployment.group}`, newDeployment);
102
+ } else {
103
+ request = GeneralApi.post(`${deploymentsApiUrl}/deployments`, newDeployment);
104
+ }
105
+ return request
106
+ .catch(err => commonErrorHandler(err, 'Error creating deployment.', dispatch))
107
+ .then(data => {
108
+ const lastslashindex = data.headers.location.lastIndexOf('/');
109
+ const deploymentId = data.headers.location.substring(lastslashindex + 1);
110
+ const deployment = {
111
+ ...newDeployment,
112
+ id: deploymentId,
113
+ devices: newDeployment.devices ? newDeployment.devices.map(id => ({ id, status: 'pending' })) : [],
114
+ statistics: { status: {} }
115
+ };
116
+ let tasks = [
117
+ dispatch(actions.createdDeployment(deployment)),
118
+ dispatch(getSingleDeployment(deploymentId)),
119
+ dispatch(setSnackbar('Deployment created successfully', TIMEOUTS.fiveSeconds))
120
+ ];
121
+ const { canManageUsers } = getUserCapabilities(getState());
122
+ if (canManageUsers) {
123
+ const { phases, retries } = newDeployment;
124
+ const { previousPhases = [], retries: previousRetries = 0 } = getGlobalSettings(getState());
125
+ let newSettings = { retries: hasNewRetryDefault ? retries : previousRetries, hasDeployments: true };
126
+ if (phases) {
127
+ const standardPhases = standardizePhases(phases);
128
+ let prevPhases = previousPhases.map(standardizePhases);
129
+ if (!prevPhases.find(previousPhaseList => previousPhaseList.every(oldPhase => standardPhases.find(phase => deepCompare(phase, oldPhase))))) {
130
+ prevPhases.push(standardPhases);
131
+ }
132
+ newSettings.previousPhases = prevPhases.slice(-1 * MAX_PREVIOUS_PHASES_COUNT);
133
+ }
134
+ tasks.push(dispatch(saveGlobalSettings(newSettings)));
135
+ }
136
+ return Promise.all(tasks);
137
+ });
138
+ });
139
+
140
+ export const getDeploymentDevices = createAsyncThunk(`${sliceName}/getDeploymentDevices`, (options, { dispatch, getState }) => {
141
+ const { id, page = defaultPage, perPage = defaultPerPage } = options;
142
+ return GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}/devices/list?&page=${page}&per_page=${perPage}`).then(response => {
143
+ const { devices: deploymentDevices = {} } = getState().deployments.byId[id] || {};
144
+ const devices = response.data.reduce((accu, item) => {
145
+ accu[item.id] = item;
146
+ const log = (deploymentDevices[item.id] || {}).log;
147
+ if (log) {
148
+ accu[item.id].log = log;
149
+ }
150
+ return accu;
151
+ }, {});
152
+ const selectedDeviceIds = Object.keys(devices);
153
+ let tasks = [
154
+ dispatch(
155
+ actions.receivedDeploymentDevices({
156
+ id,
157
+ devices,
158
+ selectedDeviceIds,
159
+ totalDeviceCount: Number(response.headers[headerNames.total])
160
+ })
161
+ )
162
+ ];
163
+ const devicesById = getDevicesById(getState());
164
+ // only update those that have changed & lack data
165
+ const lackingData = selectedDeviceIds.reduce((accu, deviceId) => {
166
+ const device = devicesById[deviceId];
167
+ if (!device || !device.identity_data || !device.attributes || Object.keys(device.attributes).length === 0) {
168
+ accu.push(deviceId);
169
+ }
170
+ return accu;
171
+ }, []);
172
+ // get device artifact, inventory and identity details not listed in schedule data
173
+ tasks = lackingData.reduce((accu, deviceId) => [...accu, dispatch(getDeviceById(deviceId)), dispatch(getDeviceAuth(deviceId))], tasks);
174
+ return Promise.all(tasks);
175
+ });
176
+ });
177
+
178
+ const parseDeviceDeployment = ({
179
+ deployment: { id, artifact_name: release, status: deploymentStatus },
180
+ device: { created, deleted, id: deviceId, finished, status, log }
181
+ }) => ({
182
+ id,
183
+ release,
184
+ created,
185
+ deleted,
186
+ deviceId,
187
+ finished,
188
+ status,
189
+ log,
190
+ route: Object.values(DEPLOYMENT_ROUTES).reduce((accu, { key, states }) => {
191
+ if (!accu) {
192
+ return states.includes(deploymentStatus) ? key : accu;
193
+ }
194
+ return accu;
195
+ }, '')
196
+ });
197
+
198
+ export const getDeviceDeployments = createAsyncThunk(`${sliceName}/getDeviceDeployments`, (options, { dispatch }) => {
199
+ const { deviceId, filterSelection = [], page = defaultPage, perPage = defaultPerPage } = options;
200
+ const filters = filterSelection.map(item => `&status=${item}`).join('');
201
+ return GeneralApi.get(`${deploymentsApiUrl}/deployments/devices/${deviceId}?page=${page}&per_page=${perPage}${filters}`)
202
+ .then(({ data, headers }) =>
203
+ Promise.resolve(
204
+ dispatch(
205
+ receivedDevice({
206
+ id: deviceId,
207
+ deviceDeployments: data.map(parseDeviceDeployment),
208
+ deploymentsCount: Number(headers[headerNames.total])
209
+ })
210
+ )
211
+ )
212
+ )
213
+ .catch(err => commonErrorHandler(err, 'There was an error retrieving the device deployment history:', dispatch));
214
+ });
215
+
216
+ export const resetDeviceDeployments = createAsyncThunk(`${sliceName}/resetDeviceDeployments`, (deviceId, { dispatch }) =>
217
+ GeneralApi.delete(`${deploymentsApiUrl}/deployments/devices/${deviceId}/history`)
218
+ .then(() => Promise.resolve(dispatch(getDeviceDeployments({ deviceId }))))
219
+ .catch(err => commonErrorHandler(err, 'There was an error resetting the device deployment history:', dispatch))
220
+ );
221
+
222
+ export const getSingleDeployment = createAsyncThunk(`${sliceName}/getSingleDeployment`, (id, { dispatch, getState }) =>
223
+ GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}`).then(({ data }) => {
224
+ const { deployments } = transformDeployments([data], getState().deployments.byId);
225
+ return Promise.resolve(dispatch(actions.receivedDeployment(deployments[id])));
226
+ })
227
+ );
228
+
229
+ export const getDeviceLog = createAsyncThunk(`${sliceName}/getDeviceLog`, ({ deploymentId, deviceId }, { dispatch }) =>
230
+ GeneralApi.get(`${deploymentsApiUrl}/deployments/${deploymentId}/devices/${deviceId}/log`)
231
+ .catch(e => {
232
+ console.log('no log here', e);
233
+ return Promise.reject();
234
+ })
235
+ .then(({ data: log }) =>
236
+ Promise.all([Promise.resolve(dispatch(actions.receivedDeploymentDeviceLog({ id: deploymentId, deviceId, log }))), Promise.resolve(log)])
237
+ )
238
+ );
239
+
240
+ export const abortDeployment = createAsyncThunk(`${sliceName}/abortDeployment`, (deploymentId, { dispatch, getState }) =>
241
+ GeneralApi.put(`${deploymentsApiUrl}/deployments/${deploymentId}/status`, { status: 'aborted' })
242
+ .then(() => {
243
+ const deploymentsByStatus = getDeploymentsByStatusSelector(getState());
244
+ let status = DEPLOYMENT_STATES.pending;
245
+ let index = deploymentsByStatus.pending.deploymentIds.findIndex(id => id === deploymentId);
246
+ if (index < 0) {
247
+ status = DEPLOYMENT_STATES.inprogress;
248
+ index = deploymentsByStatus.inprogress.deploymentIds.findIndex(id => id === deploymentId);
249
+ }
250
+ const deploymentIds = [...deploymentsByStatus[status].deploymentIds.slice(0, index), ...deploymentsByStatus[status].deploymentIds.slice(index + 1)];
251
+ const deploymentsById = getDeploymentsById(getState());
252
+ const deployments = deploymentIds.reduce((accu, id) => {
253
+ accu[id] = deploymentsById[id];
254
+ return accu;
255
+ }, {});
256
+ const total = Math.max(deploymentsByStatus[status].total - 1, 0);
257
+ return Promise.all([
258
+ dispatch(actions.receivedDeployments(deployments)),
259
+ dispatch(actions.receivedDeploymentsForStatus({ deploymentIds, status, total })),
260
+ dispatch(actions.removedDeployment(deploymentId)),
261
+ dispatch(setSnackbar('The deployment was successfully aborted'))
262
+ ]);
263
+ })
264
+ .catch(err => commonErrorHandler(err, 'There was an error while aborting the deployment:', dispatch))
265
+ );
266
+
267
+ export const updateDeploymentControlMap = createAsyncThunk(`${sliceName}/updateDeploymentControlMap`, ({ deploymentId, updateControlMap }, { dispatch }) =>
268
+ GeneralApi.patch(`${deploymentsApiUrl}/deployments/${deploymentId}`, { update_control_map: updateControlMap })
269
+ .catch(err => commonErrorHandler(err, 'There was an error while updating the deployment status:', dispatch))
270
+ .then(() => Promise.resolve(dispatch(getSingleDeployment(deploymentId))))
271
+ );
272
+
273
+ export const setDeploymentsState = createAsyncThunk(`${sliceName}/setDeploymentsState`, (selection, { dispatch, getState }) => {
274
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
275
+ const { page, perPage, ...selectionState } = selection;
276
+ const currentState = getState().deployments.selectionState;
277
+ let nextState = {
278
+ ...currentState,
279
+ ...selectionState,
280
+ ...Object.keys(DEPLOYMENT_STATES).reduce((accu, item) => {
281
+ accu[item] = {
282
+ ...currentState[item],
283
+ ...selectionState[item]
284
+ };
285
+ return accu;
286
+ }, {}),
287
+ general: {
288
+ ...currentState.general,
289
+ ...selectionState.general
290
+ }
291
+ };
292
+ let tasks = [dispatch(actions.setDeploymentsState(nextState))];
293
+ if (nextState.selectedId && currentState.selectedId !== nextState.selectedId) {
294
+ tasks.push(dispatch(getSingleDeployment(nextState.selectedId)));
295
+ }
296
+ return Promise.all(tasks);
297
+ });
298
+
299
+ const deltaAttributeMappings = [
300
+ { here: 'compressionLevel', there: 'compression_level' },
301
+ { here: 'disableChecksum', there: 'disable_checksum' },
302
+ { here: 'disableDecompression', there: 'disable_external_decompression' },
303
+ { here: 'sourceWindow', there: 'source_window_size' },
304
+ { here: 'inputWindow', there: 'input_window_size' },
305
+ { here: 'duplicatesWindow', there: 'compression_duplicates_window' },
306
+ { here: 'instructionBuffer', there: 'instruction_buffer_size' }
307
+ ];
308
+
309
+ const mapExternalDeltaConfig = (config = {}) =>
310
+ deltaAttributeMappings.reduce((accu, { here, there }) => {
311
+ if (config[there] !== undefined) {
312
+ accu[here] = config[there];
313
+ }
314
+ return accu;
315
+ }, {});
316
+
317
+ export const getDeploymentsConfig = createAsyncThunk(`${sliceName}/getDeploymentsConfig`, (_, { dispatch, getState }) =>
318
+ GeneralApi.get(`${deploymentsApiUrl}/config`).then(({ data }) => {
319
+ const oldConfig = getState().deployments.config;
320
+ const { delta = {} } = data;
321
+ const { binary_delta = {}, binary_delta_limits = {} } = delta;
322
+ const { xdelta_args = {}, timeout: timeoutConfig = oldConfig.binaryDelta.timeout } = binary_delta;
323
+ const { xdelta_args_limits = {}, timeout: timeoutLimit = oldConfig.binaryDeltaLimits.timeout } = binary_delta_limits;
324
+ const config = {
325
+ ...oldConfig,
326
+ hasDelta: Boolean(delta.enabled),
327
+ binaryDelta: {
328
+ ...oldConfig.binaryDelta,
329
+ timeout: timeoutConfig,
330
+ ...mapExternalDeltaConfig(xdelta_args)
331
+ },
332
+ binaryDeltaLimits: {
333
+ ...oldConfig.binaryDeltaLimits,
334
+ timeout: timeoutLimit,
335
+ ...mapExternalDeltaConfig(xdelta_args_limits)
336
+ }
337
+ };
338
+ return Promise.resolve(dispatch(actions.setDeploymentsConfig(config)));
339
+ })
340
+ );
341
+
342
+ // traverse a source object and remove undefined & empty object properties to only return an attribute if there really is content worth sending
343
+ const deepClean = source =>
344
+ Object.entries(source).reduce((accu, [key, value]) => {
345
+ if (value !== undefined) {
346
+ let cleanedValue = typeof value === 'object' ? deepClean(value) : value;
347
+ if (cleanedValue === undefined || (typeof cleanedValue === 'object' && isEmpty(cleanedValue))) {
348
+ return accu;
349
+ }
350
+ accu = { ...(accu ?? {}), [key]: cleanedValue };
351
+ }
352
+ return accu;
353
+ }, undefined);
354
+
355
+ export const saveDeltaDeploymentsConfig = createAsyncThunk(`${sliceName}/saveDeltaDeploymentsConfig`, (config, { dispatch, getState }) => {
356
+ const configChange = {
357
+ timeout: config.timeout,
358
+ xdelta_args: deltaAttributeMappings.reduce((accu, { here, there }) => {
359
+ if (config[here] !== undefined) {
360
+ accu[there] = config[here];
361
+ }
362
+ return accu;
363
+ }, {})
364
+ };
365
+ const result = deepClean(configChange);
366
+ if (!result) {
367
+ return Promise.resolve();
368
+ }
369
+ return GeneralApi.put(`${deploymentsApiUrl}/config/binary_delta`, result)
370
+ .catch(err => commonErrorHandler(err, 'There was a problem storing your delta artifact generation configuration.', dispatch))
371
+ .then(() => {
372
+ const oldConfig = getState().deployments.config;
373
+ const newConfig = {
374
+ ...oldConfig,
375
+ binaryDelta: {
376
+ ...oldConfig.binaryDelta,
377
+ ...config
378
+ }
379
+ };
380
+ return Promise.all([dispatch(actions.setDeploymentsConfig(newConfig)), dispatch(setSnackbar('Settings saved successfully'))]);
381
+ });
382
+ });
@@ -0,0 +1,56 @@
1
+ // Copyright 2019 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
+ import { DEVICE_FILTERING_OPTIONS, apiUrl } from '@northern.tech/store/constants';
15
+
16
+ export const emptyFilter = { key: null, value: '', operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'inventory' };
17
+
18
+ export const deviceAuthV2 = `${apiUrl.v2}/devauth`;
19
+ export const deviceConnect = `${apiUrl.v1}/deviceconnect`;
20
+ export const inventoryApiUrl = `${apiUrl.v1}/inventory`;
21
+ export const inventoryApiUrlV2 = `${apiUrl.v2}/inventory`;
22
+ export const deviceConfig = `${apiUrl.v1}/deviceconfig/configurations/device`;
23
+ export const reportingApiUrl = `${apiUrl.v1}/reporting`;
24
+ export const iotManagerBaseURL = `${apiUrl.v1}/iot-manager`;
25
+
26
+ // see https://github.com/mendersoftware/go-lib-micro/tree/master/ws
27
+ // for the description of proto_header and the consts
28
+ // *Note*: this needs to be aligned with mender-connect and deviceconnect.
29
+ export const DEVICE_MESSAGE_PROTOCOLS = {
30
+ Shell: 1
31
+ };
32
+ export const DEVICE_MESSAGE_TYPES = {
33
+ Delay: 'delay',
34
+ New: 'new',
35
+ Ping: 'ping',
36
+ Pong: 'pong',
37
+ Resize: 'resize',
38
+ Shell: 'shell',
39
+ Stop: 'stop'
40
+ };
41
+
42
+ // we can't include the dismiss state with the rest since this would include dismissed devices in several queries
43
+ export const DEVICE_DISMISSAL_STATE = 'dismiss';
44
+ export const DEVICE_STATES = {
45
+ accepted: 'accepted',
46
+ pending: 'pending',
47
+ preauth: 'preauthorized',
48
+ rejected: 'rejected'
49
+ };
50
+ export const DEVICE_CONNECT_STATES = {
51
+ connected: 'connected',
52
+ disconnected: 'disconnected',
53
+ unknown: 'unknown'
54
+ };
55
+
56
+ export const geoAttributes = ['geo-lat', 'geo-lon'].map(attribute => ({ attribute, scope: 'inventory' }));
@@ -0,0 +1,207 @@
1
+ // Copyright 2023 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
+ import { DEVICE_LIST_DEFAULTS, SORTING_OPTIONS } from '@northern.tech/store/commonConstants';
16
+ import { deepCompare, duplicateFilter } from '@northern.tech/utils/helpers';
17
+ import { createSlice } from '@reduxjs/toolkit';
18
+
19
+ import { DEVICE_STATES } from './constants';
20
+
21
+ export const sliceName = 'devices';
22
+
23
+ export const initialState = {
24
+ byId: {
25
+ // [deviceId]: {
26
+ // ...,
27
+ // twinsByIntegration: { [external.provider.id]: twinData }
28
+ // }
29
+ },
30
+ byStatus: {
31
+ [DEVICE_STATES.accepted]: { deviceIds: [], total: 0 },
32
+ active: { deviceIds: [], total: 0 },
33
+ inactive: { deviceIds: [], total: 0 },
34
+ [DEVICE_STATES.pending]: { deviceIds: [], total: 0 },
35
+ [DEVICE_STATES.preauth]: { deviceIds: [], total: 0 },
36
+ [DEVICE_STATES.rejected]: { deviceIds: [], total: 0 }
37
+ },
38
+ deviceList: {
39
+ deviceIds: [],
40
+ ...DEVICE_LIST_DEFAULTS,
41
+ selectedAttributes: [],
42
+ selectedIssues: [],
43
+ selection: [],
44
+ sort: {
45
+ direction: SORTING_OPTIONS.desc
46
+ // key: null,
47
+ // scope: null
48
+ },
49
+ state: DEVICE_STATES.accepted,
50
+ total: 0
51
+ },
52
+ filters: [
53
+ // { key: 'device_type', value: 'raspberry', operator: '$eq', scope: 'inventory' }
54
+ ],
55
+ filteringAttributes: { identityAttributes: [], inventoryAttributes: [], systemAttributes: [], tagAttributes: [] },
56
+ filteringAttributesLimit: 10,
57
+ filteringAttributesConfig: {
58
+ attributes: {
59
+ // inventory: ['some_attribute']
60
+ },
61
+ count: 0,
62
+ limit: 100
63
+ },
64
+ reports: [
65
+ // { items: [{ key: "someKey", count: 42 }], otherCount: 123, total: <otherCount + itemsCount> }
66
+ ],
67
+ total: 0,
68
+ limit: 0,
69
+ groups: {
70
+ byId: {
71
+ // groupName: { deviceIds: [], total: 0, filters: [] },
72
+ // dynamo: { deviceIds: [], total: 3, filters: [{ a: 1 }] }
73
+ },
74
+ selectedGroup: undefined
75
+ }
76
+ };
77
+
78
+ export const devicesSlice = createSlice({
79
+ name: sliceName,
80
+ initialState,
81
+ reducers: {
82
+ receivedGroups: (state, action) => {
83
+ state.groups.byId = action.payload;
84
+ },
85
+ addToGroup: (state, action) => {
86
+ const { group, deviceIds } = action.payload;
87
+ const maybeExistingGroup = {
88
+ filters: [],
89
+ deviceIds: [],
90
+ ...state.groups.byId[group]
91
+ };
92
+ state.groups.byId[group] = {
93
+ ...maybeExistingGroup,
94
+ deviceIds: [...maybeExistingGroup.deviceIds, ...deviceIds].filter(duplicateFilter),
95
+ total: (maybeExistingGroup.total || 0) + 1
96
+ };
97
+ },
98
+ removeFromGroup: (state, action) => {
99
+ const { group, deviceIds: removedIds } = action.payload;
100
+ const { deviceIds = [], total = 0, ...maybeExistingGroup } = state.groups.byId[group] || {};
101
+ const changedGroup = {
102
+ ...maybeExistingGroup,
103
+ deviceIds: deviceIds.filter(id => !removedIds.includes(id)),
104
+ total: Math.max(total - removedIds.length, 0)
105
+ };
106
+ if (changedGroup.total || changedGroup.deviceIds.length) {
107
+ state.groups.byId[group] = changedGroup;
108
+ return;
109
+ } else if (state.groups.selectedGroup === group) {
110
+ state.groups.selectedGroup = undefined;
111
+ }
112
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
113
+ const { [group]: removal, ...remainingById } = state.groups.byId;
114
+ state.groups.byId = remainingById;
115
+ },
116
+ addGroup: (state, action) => {
117
+ const { groupName, group } = action.payload;
118
+ state.groups.byId[groupName] = {
119
+ ...state.groups.byId[groupName],
120
+ ...group
121
+ };
122
+ },
123
+ selectGroup: (state, { payload: group }) => {
124
+ state.deviceList.deviceIds = state.groups.byId[group] && state.groups.byId[group].deviceIds.length > 0 ? state.groups.byId[group].deviceIds : [];
125
+ state.groups.selectedGroup = group;
126
+ },
127
+ removeGroup: (state, action) => {
128
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
129
+ const { [action.payload]: removal, ...remainingById } = state.groups.byId;
130
+ state.groups.byId = remainingById;
131
+ state.groups.selectedGroup = state.groups.selectedGroup === action.payload ? undefined : state.groups.selectedGroup;
132
+ },
133
+ setDeviceListState: (state, action) => {
134
+ state.deviceList = {
135
+ ...state.deviceList,
136
+ ...action.payload,
137
+ sort: {
138
+ ...state.deviceList.sort,
139
+ ...action.payload.sort
140
+ }
141
+ };
142
+ },
143
+ setFilterAttributes: (state, action) => {
144
+ state.filteringAttributes = action.payload;
145
+ },
146
+ setFilterablesConfig: (state, action) => {
147
+ state.filteringAttributesConfig = action.payload;
148
+ },
149
+ receivedDevices: (state, action) => {
150
+ state.byId = {
151
+ ...state.byId,
152
+ ...action.payload
153
+ };
154
+ },
155
+ setDeviceFilters: (state, action) => {
156
+ if (deepCompare(action.payload, state.filters)) {
157
+ return;
158
+ }
159
+ state.filters = action.payload.filter(filter => filter.key && filter.operator && filter.scope && typeof filter.value !== 'undefined');
160
+ },
161
+ setInactiveDevices: (state, action) => {
162
+ const { activeDeviceTotal, inactiveDeviceTotal } = action.payload;
163
+ state.byStatus.active.total = activeDeviceTotal;
164
+ state.byStatus.inactive.total = inactiveDeviceTotal;
165
+ },
166
+ setDeviceReports: (state, action) => {
167
+ state.reports = action.payload;
168
+ },
169
+ setDevicesByStatus: (state, action) => {
170
+ const { forceUpdate, status, total, deviceIds } = action.payload;
171
+ state.byStatus[status] = total || forceUpdate ? { deviceIds, total } : state.byStatus[status];
172
+ },
173
+ setDevicesCountByStatus: (state, action) => {
174
+ const { count, status } = action.payload;
175
+ state.byStatus[status].total = count;
176
+ },
177
+ setTotalDevices: (state, action) => {
178
+ state.total = action.payload;
179
+ },
180
+ setDeviceLimit: (state, action) => {
181
+ state.limit = action.payload;
182
+ },
183
+ receivedDevice: (state, action) => {
184
+ state.byId[action.payload.id] = {
185
+ ...state.byId[action.payload.id],
186
+ ...action.payload
187
+ };
188
+ },
189
+ maybeUpdateDevicesByStatus: (state, action) => {
190
+ const { deviceId, authId } = action.payload;
191
+ const device = state.byId[deviceId];
192
+ const hasMultipleAuthSets = authId ? device.auth_sets.filter(authset => authset.id !== authId).length > 0 : false;
193
+ if (!hasMultipleAuthSets && Object.values(DEVICE_STATES).includes(device.status)) {
194
+ const deviceIds = state.byStatus[device.status].deviceIds.filter(id => id !== deviceId);
195
+ state.byStatus[device.status] = { deviceIds, total: Math.max(0, state.byStatus[device.status].total - 1) };
196
+ }
197
+ }
198
+ }
199
+ // The following would likely be the way to go with a simpler store setup, but ours seems too entangled for this to provide much benefit
200
+ // import * as actionCreators from './thunks';
201
+ // extraReducers: {
202
+ // [actionCreators.setDeviceListState.fulfilled]: setDeviceListState
203
+ // }
204
+ });
205
+
206
+ export const actions = devicesSlice.actions;
207
+ export default devicesSlice.reducer;