• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / gui / 908425489

pending completion
908425489

Pull #3799

gitlab-ci

mzedel
chore: aligned loader usage in devices list with deployment devices list

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3799: MEN-6553

4406 of 6423 branches covered (68.6%)

18 of 19 new or added lines in 3 files covered. (94.74%)

1777 existing lines in 167 files now uncovered.

8329 of 10123 relevant lines covered (82.28%)

144.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.42
/src/js/selectors/index.js
1
// Copyright 2020 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 { createSelector } from '@reduxjs/toolkit';
15

16
import { mapUserRolesToUiPermissions } from '../actions/userActions';
17
import { PLANS } from '../constants/appConstants';
18
import { DEPLOYMENT_STATES } from '../constants/deploymentConstants';
19
import {
20
  ALL_DEVICES,
21
  ATTRIBUTE_SCOPES,
22
  DEVICE_ISSUE_OPTIONS,
23
  DEVICE_LIST_MAXIMUM_LENGTH,
24
  DEVICE_ONLINE_CUTOFF,
25
  DEVICE_STATES,
26
  EXTERNAL_PROVIDER,
27
  UNGROUPED_GROUP
28
} from '../constants/deviceConstants';
29
import { rolesByName, twoFAStates, uiPermissionsById } from '../constants/userConstants';
30
import { attributeDuplicateFilter, duplicateFilter, getDemoDeviceAddress as getDemoDeviceAddressHelper, versionCompare } from '../helpers';
31

32
const getAppDocsVersion = state => state.app.docsVersion;
706✔
33
export const getFeatures = state => state.app.features;
7,435✔
34
export const getRolesById = state => state.users.rolesById;
735✔
35
export const getOrganization = state => state.organization.organization;
3,414✔
36
export const getAcceptedDevices = state => state.devices.byStatus.accepted;
3,203✔
37
const getDevicesByStatus = state => state.devices.byStatus;
672✔
38
export const getDevicesById = state => state.devices.byId;
2,732✔
39
const getGroupsById = state => state.devices.groups.byId;
690✔
40
const getSelectedGroup = state => state.devices.groups.selectedGroup;
190✔
41
const getSearchedDevices = state => state.app.searchState.deviceIds;
436✔
42
const getListedDevices = state => state.devices.deviceList.deviceIds;
190✔
43
const getFilteringAttributes = state => state.devices.filteringAttributes;
445✔
44
export const getDeviceFilters = state => state.devices.filters || [];
190!
45
const getFilteringAttributesFromConfig = state => state.devices.filteringAttributesConfig.attributes;
441✔
46
export const getDeviceLimit = state => state.devices.limit;
1,217✔
47
const getDevicesList = state => Object.values(state.devices.byId);
190✔
48
const getOnboarding = state => state.onboarding;
734✔
49
export const getShowHelptips = state => state.users.showHelptips;
3,273✔
50
export const getGlobalSettings = state => state.users.globalSettings;
1,501✔
51
const getIssueCountsByType = state => state.monitor.issueCounts.byType;
443✔
52
export const getReleasesById = state => state.releases.byId;
1,558✔
53
const getListedReleases = state => state.releases.releasesList.releaseIds;
190✔
54
export const getExternalIntegrations = state => state.organization.externalDeviceIntegrations;
190✔
55
const getDeploymentsById = state => state.deployments.byId;
440✔
56
const getDeploymentsByStatus = state => state.deployments.byStatus;
440✔
57
export const getVersionInformation = state => state.app.versionInformation;
1,177✔
58

59
export const getCurrentUser = state => state.users.byId[state.users.currentUser] || {};
1,892✔
60
export const getUserSettings = state => state.users.userSettings;
4,159✔
61
export const getIsPreview = createSelector([getVersionInformation], ({ Integration }) => versionCompare(Integration, 'next') > -1);
190✔
62

63
export const getHas2FA = createSelector(
190✔
64
  [getCurrentUser],
65
  currentUser => currentUser.hasOwnProperty('tfa_status') && currentUser.tfa_status === twoFAStates.enabled
2!
66
);
67

68
export const getDemoDeviceAddress = createSelector([getDevicesList, getOnboarding], (devices, { approach, demoArtifactPort }) => {
190✔
69
  const demoDeviceAddress = `http://${getDemoDeviceAddressHelper(devices, approach)}`;
2✔
70
  return demoArtifactPort ? `${demoDeviceAddress}:${demoArtifactPort}` : demoDeviceAddress;
2!
71
});
72

73
const listItemMapper = (byId, ids, { defaultObject = {}, cutOffSize = DEVICE_LIST_MAXIMUM_LENGTH }) => {
190✔
74
  return ids.slice(0, cutOffSize).reduce((accu, id) => {
470✔
75
    if (id && byId[id]) {
206!
76
      accu.push({ ...defaultObject, ...byId[id] });
206✔
77
    }
78
    return accu;
206✔
79
  }, []);
80
};
81

82
const listTypeDeviceIdMap = {
190✔
83
  deviceList: getListedDevices,
84
  search: getSearchedDevices
85
};
86
const getDeviceMappingDefaults = () => ({ defaultObject: { auth_sets: [] }, cutOffSize: DEVICE_LIST_MAXIMUM_LENGTH });
439✔
87
export const getMappedDevicesList = createSelector(
190✔
88
  [getDevicesById, (state, listType) => listTypeDeviceIdMap[listType](state), getDeviceMappingDefaults],
439✔
89
  listItemMapper
90
);
91

92
export const getDeviceCountsByStatus = createSelector([getDevicesByStatus], byStatus =>
190✔
93
  Object.values(DEVICE_STATES).reduce((accu, state) => {
34✔
94
    accu[state] = byStatus[state].total || 0;
136✔
95
    return accu;
136✔
96
  }, {})
97
);
98

99
export const getSelectedGroupInfo = createSelector(
190✔
100
  [getAcceptedDevices, getGroupsById, getSelectedGroup],
101
  ({ total: acceptedDeviceTotal }, groupsById, selectedGroup) => {
102
    let groupCount = acceptedDeviceTotal;
6✔
103
    let groupFilters = [];
6✔
104
    if (selectedGroup && groupsById[selectedGroup]) {
6✔
105
      groupCount = groupsById[selectedGroup].total;
2✔
106
      groupFilters = groupsById[selectedGroup].filters || [];
2!
107
    }
108
    return { groupCount, selectedGroup, groupFilters };
6✔
109
  }
110
);
111

112
const defaultIdAttribute = Object.freeze({ attribute: 'id', scope: ATTRIBUTE_SCOPES.identity });
190✔
113
export const getIdAttribute = createSelector([getGlobalSettings], ({ id_attribute = { ...defaultIdAttribute } }) => id_attribute);
190✔
114

115
export const getLimitMaxed = createSelector([getAcceptedDevices, getDeviceLimit], ({ total: acceptedDevices = 0 }, deviceLimit) =>
190!
116
  Boolean(deviceLimit && deviceLimit <= acceptedDevices)
6✔
117
);
118

119
export const getFilterAttributes = createSelector(
190✔
120
  [getGlobalSettings, getFilteringAttributes],
121
  ({ previousFilters }, { identityAttributes, inventoryAttributes, systemAttributes, tagAttributes }) => {
122
    const deviceNameAttribute = { key: 'name', value: 'Name', scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 1 };
3✔
123
    const deviceIdAttribute = { key: 'id', value: 'Device ID', scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 };
3✔
124
    const checkInAttribute = { key: 'updated_ts', value: 'Last check-in', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
125
    const firstRequestAttribute = { key: 'created_ts', value: 'First request', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
126
    const attributes = [
3✔
UNCOV
127
      ...previousFilters.map(item => ({
×
128
        ...item,
129
        value: deviceIdAttribute.key === item.key ? deviceIdAttribute.value : item.key,
×
130
        category: 'recently used',
131
        priority: 0
132
      })),
133
      deviceNameAttribute,
134
      deviceIdAttribute,
135
      ...identityAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 })),
3✔
136
      ...inventoryAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.inventory, category: ATTRIBUTE_SCOPES.inventory, priority: 2 })),
3✔
UNCOV
137
      ...tagAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 3 })),
×
138
      checkInAttribute,
139
      firstRequestAttribute,
UNCOV
140
      ...systemAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 }))
×
141
    ];
142
    return attributeDuplicateFilter(attributes, 'key');
3✔
143
  }
144
);
145

146
export const getGroups = createSelector([getGroupsById], groupsById => {
190✔
147
  const groupNames = Object.keys(groupsById).sort();
12✔
148
  const groupedGroups = Object.entries(groupsById)
12✔
149
    .sort((a, b) => a[0].localeCompare(b[0]))
31✔
150
    .reduce(
151
      (accu, [groupname, group]) => {
152
        const name = groupname === UNGROUPED_GROUP.id ? UNGROUPED_GROUP.name : groupname;
31✔
153
        const groupItem = { ...group, groupId: name, name: groupname };
31✔
154
        if (group.filters.length > 0) {
31✔
155
          if (groupname !== UNGROUPED_GROUP.id) {
19✔
156
            accu.dynamic.push(groupItem);
12✔
157
          } else {
158
            accu.ungrouped.push(groupItem);
7✔
159
          }
160
        } else {
161
          accu.static.push(groupItem);
12✔
162
        }
163
        return accu;
31✔
164
      },
165
      { dynamic: [], static: [], ungrouped: [] }
166
    );
167
  return { groupNames, ...groupedGroups };
12✔
168
});
169

170
export const getDeviceTwinIntegrations = createSelector([getExternalIntegrations], integrations =>
190✔
171
  integrations.filter(integration => integration.id && EXTERNAL_PROVIDER[integration.provider]?.deviceTwin)
4✔
172
);
173

174
export const getOfflineThresholdSettings = createSelector([getGlobalSettings], ({ offlineThreshold }) => ({
190✔
175
  interval: offlineThreshold?.interval || DEVICE_ONLINE_CUTOFF.interval,
24✔
176
  intervalUnit: offlineThreshold?.intervalUnit || DEVICE_ONLINE_CUTOFF.intervalName
24✔
177
}));
178

179
export const getOnboardingState = createSelector([getOnboarding, getShowHelptips], ({ complete, progress, showTips, ...remainder }, showHelptips) => ({
190✔
180
  ...remainder,
181
  complete,
182
  progress,
183
  showHelptips,
184
  showTips
185
}));
186

187
export const getDocsVersion = createSelector([getAppDocsVersion, getFeatures], (appDocsVersion, { isHosted }) => {
190✔
188
  // if hosted, use latest docs version
189
  const docsVersion = appDocsVersion ? `${appDocsVersion}/` : 'development/';
41!
190
  return isHosted ? '' : docsVersion;
41✔
191
});
192

193
export const getIsEnterprise = createSelector(
190✔
194
  [getOrganization, getFeatures],
195
  ({ plan = PLANS.os.value }, { isEnterprise, isHosted }) => isEnterprise || (isHosted && plan === PLANS.enterprise.value)
69✔
196
);
197

198
export const getAttributesList = createSelector(
190✔
199
  [getFilteringAttributes, getFilteringAttributesFromConfig],
200
  ({ identityAttributes = [], inventoryAttributes = [] }, { identity = [], inventory = [] }) =>
22!
201
    [...identityAttributes, ...inventoryAttributes, ...identity, ...inventory].filter(duplicateFilter)
11✔
202
);
203

204
export const getUserRoles = createSelector(
190✔
205
  [getCurrentUser, getRolesById, getIsEnterprise, getFeatures, getOrganization],
206
  (currentUser, rolesById, isEnterprise, { isHosted, hasMultitenancy }, { plan = PLANS.os.value }) => {
318✔
207
    const isAdmin = currentUser.roles?.length
378✔
208
      ? currentUser.roles.some(role => role === rolesByName.admin)
47✔
209
      : !(hasMultitenancy || isEnterprise || (isHosted && plan !== PLANS.os.value));
375!
210
    const uiPermissions = isAdmin
378✔
211
      ? mapUserRolesToUiPermissions([rolesByName.admin], rolesById)
212
      : mapUserRolesToUiPermissions(currentUser.roles || [], rolesById);
618✔
213
    return { isAdmin, uiPermissions };
378✔
214
  }
215
);
216

217
const hasPermission = (thing, permission) => Object.values(thing).some(permissions => permissions.includes(permission));
2,792✔
218

219
export const getUserCapabilities = createSelector([getUserRoles], ({ uiPermissions }) => {
190✔
220
  const canManageReleases = hasPermission(uiPermissions.releases, uiPermissionsById.manage.value);
373✔
221
  const canReadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.read.value);
373✔
222
  const canUploadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.upload.value);
373✔
223

224
  const canAuditlog = uiPermissions.auditlog.includes(uiPermissionsById.read.value);
373✔
225

226
  const canReadUsers = uiPermissions.userManagement.includes(uiPermissionsById.read.value);
373✔
227
  const canManageUsers = uiPermissions.userManagement.includes(uiPermissionsById.manage.value);
373✔
228

229
  const canReadDevices = hasPermission(uiPermissions.groups, uiPermissionsById.read.value);
373✔
230
  const canWriteDevices = Object.values(uiPermissions.groups).some(
373✔
231
    groupPermissions => groupPermissions.includes(uiPermissionsById.read.value) && groupPermissions.length > 1
64✔
232
  );
233
  const canTroubleshoot = hasPermission(uiPermissions.groups, uiPermissionsById.connect.value);
373✔
234
  const canManageDevices = hasPermission(uiPermissions.groups, uiPermissionsById.manage.value);
373✔
235
  const canConfigure = hasPermission(uiPermissions.groups, uiPermissionsById.configure.value);
373✔
236

237
  const canDeploy = uiPermissions.deployments.includes(uiPermissionsById.deploy.value) || hasPermission(uiPermissions.groups, uiPermissionsById.deploy.value);
373✔
238
  const canReadDeployments = uiPermissions.deployments.includes(uiPermissionsById.read.value);
373✔
239

240
  return {
373✔
241
    canAuditlog,
242
    canConfigure,
243
    canDeploy,
244
    canManageDevices,
245
    canManageReleases,
246
    canManageUsers,
247
    canReadDeployments,
248
    canReadDevices,
249
    canReadReleases,
250
    canReadUsers,
251
    canTroubleshoot,
252
    canUploadReleases,
253
    canWriteDevices,
254
    groupsPermissions: uiPermissions.groups,
255
    releasesPermissions: uiPermissions.releases
256
  };
257
});
258

259
export const getTenantCapabilities = createSelector(
190✔
260
  [getFeatures, getOrganization, getIsEnterprise],
261
  (
262
    {
263
      hasAddons,
264
      hasAuditlogs: isAuditlogEnabled,
265
      hasDeviceConfig: isDeviceConfigEnabled,
266
      hasDeviceConnect: isDeviceConnectEnabled,
267
      hasMonitor: isMonitorEnabled,
268
      isHosted
269
    },
270
    { addons = [], plan },
8✔
271
    isEnterprise
272
  ) => {
273
    const canDelta = isEnterprise || plan === PLANS.professional.value;
39✔
274
    const hasAuditlogs = isAuditlogEnabled && (!isHosted || isEnterprise || plan === PLANS.professional.value);
39!
275
    const hasDeviceConfig = hasAddons || (isDeviceConfigEnabled && (!isHosted || addons.some(addon => addon.name === 'configure' && Boolean(addon.enabled))));
39!
276
    const hasDeviceConnect =
277
      hasAddons || (isDeviceConnectEnabled && (!isHosted || addons.some(addon => addon.name === 'troubleshoot' && Boolean(addon.enabled))));
39!
278
    const hasMonitor = hasAddons || (isMonitorEnabled && (!isHosted || addons.some(addon => addon.name === 'monitor' && Boolean(addon.enabled))));
39!
279
    return {
39✔
280
      canDelta,
281
      canRetry: canDelta,
282
      canSchedule: canDelta,
283
      hasAuditlogs,
284
      hasDeviceConfig,
285
      hasDeviceConnect,
286
      hasFullFiltering: canDelta,
287
      hasMonitor,
288
      isEnterprise
289
    };
290
  }
291
);
292

293
export const getAvailableIssueOptionsByType = createSelector(
190✔
294
  [getFeatures, getTenantCapabilities, getIssueCountsByType],
295
  ({ hasReporting }, { hasFullFiltering, hasMonitor }, issueCounts) =>
296
    Object.values(DEVICE_ISSUE_OPTIONS).reduce((accu, { isCategory, key, needsFullFiltering, needsMonitor, needsReporting, title }) => {
18✔
297
      if (isCategory || (needsReporting && !hasReporting) || (needsFullFiltering && !hasFullFiltering) || (needsMonitor && !hasMonitor)) {
108✔
298
        return accu;
106✔
299
      }
300
      accu[key] = { count: issueCounts[key].filtered, key, title };
2✔
301
      return accu;
2✔
302
    }, {})
303
);
304

305
export const getDeviceTypes = createSelector([getAcceptedDevices, getDevicesById], ({ deviceIds = [] }, devicesById) =>
190!
306
  Object.keys(
1✔
307
    deviceIds.slice(0, 200).reduce((accu, item) => {
308
      const { device_type: deviceTypes = [] } = devicesById[item] ? devicesById[item].attributes : {};
2!
309
      accu = deviceTypes.reduce((deviceTypeAccu, deviceType) => {
2✔
310
        if (deviceType.length > 1) {
2!
311
          deviceTypeAccu[deviceType] = deviceTypeAccu[deviceType] ? deviceTypeAccu[deviceType] + 1 : 1;
2!
312
        }
313
        return deviceTypeAccu;
2✔
314
      }, accu);
315
      return accu;
2✔
316
    }, {})
317
  )
318
);
319

320
export const getGroupNames = createSelector([getGroupsById, getUserRoles, (_, options = {}) => options], (groupsById, { uiPermissions }, { staticOnly }) => {
670✔
321
  // eslint-disable-next-line no-unused-vars
322
  const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = groupsById;
670✔
323
  if (staticOnly) {
670!
UNCOV
324
    return Object.keys(uiPermissions.groups).sort();
×
325
  }
326
  return Object.keys(
670✔
327
    Object.entries(groups).reduce((accu, [groupName, group]) => {
328
      if (group.filterId || uiPermissions.groups[ALL_DEVICES]) {
1,278✔
329
        accu[groupName] = group;
700✔
330
      }
331
      return accu;
1,278✔
332
    }, uiPermissions.groups)
333
  ).sort();
334
});
335

336
const getReleaseMappingDefaults = () => ({});
190✔
337
export const getReleasesList = createSelector([getReleasesById, getListedReleases, getReleaseMappingDefaults], listItemMapper);
190✔
338

339
const relevantDeploymentStates = [DEPLOYMENT_STATES.pending, DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.finished];
190✔
340
export const DEPLOYMENT_CUTOFF = 3;
190✔
341
export const getRecentDeployments = createSelector([getDeploymentsById, getDeploymentsByStatus], (deploymentsById, deploymentsByStatus) =>
190✔
342
  Object.entries(deploymentsByStatus).reduce(
204✔
343
    (accu, [state, byStatus]) => {
344
      if (!relevantDeploymentStates.includes(state) || !byStatus.deploymentIds.length) {
816✔
345
        return accu;
231✔
346
      }
347
      accu[state] = byStatus.deploymentIds
585✔
348
        .reduce((accu, id) => {
349
          if (deploymentsById[id]) {
1,155✔
350
            accu.push(deploymentsById[id]);
1,149✔
351
          }
352
          return accu;
1,155✔
353
        }, [])
354
        .slice(0, DEPLOYMENT_CUTOFF);
355
      accu.total += byStatus.total;
585✔
356
      return accu;
585✔
357
    },
358
    { total: 0 }
359
  )
360
);
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc