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

mendersoftware / gui / 913068613

pending completion
913068613

Pull #3803

gitlab-ci

web-flow
Merge pull request #3801 from mzedel/men-6383

MEN-6383 - device check in time
Pull Request #3803: staging alignment

4418 of 6435 branches covered (68.66%)

178 of 246 new or added lines in 27 files covered. (72.36%)

8352 of 10138 relevant lines covered (82.38%)

160.95 hits per line

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

88.46
/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;
862✔
33
export const getFeatures = state => state.app.features;
9,766✔
34
export const getRolesById = state => state.users.rolesById;
893✔
35
export const getOrganization = state => state.organization.organization;
4,392✔
36
export const getAcceptedDevices = state => state.devices.byStatus.accepted;
3,853✔
37
const getDevicesByStatus = state => state.devices.byStatus;
826✔
38
export const getDevicesById = state => state.devices.byId;
2,853✔
39
const getGroupsById = state => state.devices.groups.byId;
842✔
40
const getSelectedGroup = state => state.devices.groups.selectedGroup;
190✔
41
const getSearchedDevices = state => state.app.searchState.deviceIds;
597✔
42
const getListedDevices = state => state.devices.deviceList.deviceIds;
190✔
43
const getFilteringAttributes = state => state.devices.filteringAttributes;
606✔
44
export const getDeviceFilters = state => state.devices.filters || [];
190!
45
const getFilteringAttributesFromConfig = state => state.devices.filteringAttributesConfig.attributes;
602✔
46
export const getDeviceLimit = state => state.devices.limit;
1,726✔
47
const getDevicesList = state => Object.values(state.devices.byId);
190✔
48
const getOnboarding = state => state.onboarding;
889✔
49
export const getShowHelptips = state => state.users.showHelptips;
4,447✔
50
export const getGlobalSettings = state => state.users.globalSettings;
1,643✔
51
const getIssueCountsByType = state => state.monitor.issueCounts.byType;
604✔
52
export const getReleasesById = state => state.releases.byId;
1,531✔
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;
601✔
56
const getDeploymentsByStatus = state => state.deployments.byStatus;
601✔
57
export const getVersionInformation = state => state.app.versionInformation;
1,688✔
58

59
export const getCurrentUser = state => state.users.byId[state.users.currentUser] || {};
2,378✔
60
export const getUserSettings = state => state.users.userSettings;
5,716✔
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) => {
630✔
75
    if (id && byId[id]) {
224!
76
      accu.push({ ...defaultObject, ...byId[id] });
224✔
77
    }
78
    return accu;
224✔
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 });
600✔
87
export const getMappedDevicesList = createSelector(
190✔
88
  [getDevicesById, (state, listType) => listTypeDeviceIdMap[listType](state), getDeviceMappingDefaults],
600✔
89
  listItemMapper
90
);
91

92
export const getDeviceCountsByStatus = createSelector([getDevicesByStatus], byStatus =>
190✔
93
  Object.values(DEVICE_STATES).reduce((accu, state) => {
210✔
94
    accu[state] = byStatus[state].total || 0;
840✔
95
    return accu;
840✔
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: 'check_in_time', value: 'Latest activity', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
125
    const updateAttribute = { ...checkInAttribute, key: 'updated_ts', value: 'Last inventory update' };
3✔
126
    const firstRequestAttribute = { key: 'created_ts', value: 'First request', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
127
    const attributes = [
3✔
128
      ...previousFilters.map(item => ({
×
129
        ...item,
130
        value: deviceIdAttribute.key === item.key ? deviceIdAttribute.value : item.key,
×
131
        category: 'recently used',
132
        priority: 0
133
      })),
134
      deviceNameAttribute,
135
      deviceIdAttribute,
136
      ...identityAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 })),
3✔
137
      ...inventoryAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.inventory, category: ATTRIBUTE_SCOPES.inventory, priority: 2 })),
3✔
138
      ...tagAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 3 })),
×
139
      checkInAttribute,
140
      updateAttribute,
141
      firstRequestAttribute,
142
      ...systemAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 }))
×
143
    ];
144
    return attributeDuplicateFilter(attributes, 'key');
3✔
145
  }
146
);
147

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

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

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

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

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

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

200
export const getAttributesList = createSelector(
190✔
201
  [getFilteringAttributes, getFilteringAttributesFromConfig],
202
  ({ identityAttributes = [], inventoryAttributes = [] }, { identity = [], inventory = [] }) =>
18!
203
    [...identityAttributes, ...inventoryAttributes, ...identity, ...inventory].filter(duplicateFilter)
9✔
204
);
205

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

219
const hasPermission = (thing, permission) => Object.values(thing).some(permissions => permissions.includes(permission));
4,143✔
220

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

226
  const canAuditlog = uiPermissions.auditlog.includes(uiPermissionsById.read.value);
540✔
227

228
  const canReadUsers = uiPermissions.userManagement.includes(uiPermissionsById.read.value);
540✔
229
  const canManageUsers = uiPermissions.userManagement.includes(uiPermissionsById.manage.value);
540✔
230

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

239
  const canDeploy = uiPermissions.deployments.includes(uiPermissionsById.deploy.value) || hasPermission(uiPermissions.groups, uiPermissionsById.deploy.value);
540✔
240
  const canReadDeployments = uiPermissions.deployments.includes(uiPermissionsById.read.value);
540✔
241

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

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

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

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

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

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

341
const relevantDeploymentStates = [DEPLOYMENT_STATES.pending, DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.finished];
190✔
342
export const DEPLOYMENT_CUTOFF = 3;
190✔
343
export const getRecentDeployments = createSelector([getDeploymentsById, getDeploymentsByStatus], (deploymentsById, deploymentsByStatus) =>
190✔
344
  Object.entries(deploymentsByStatus).reduce(
202✔
345
    (accu, [state, byStatus]) => {
346
      if (!relevantDeploymentStates.includes(state) || !byStatus.deploymentIds.length) {
808✔
347
        return accu;
226✔
348
      }
349
      accu[state] = byStatus.deploymentIds
582✔
350
        .reduce((accu, id) => {
351
          if (deploymentsById[id]) {
1,152✔
352
            accu.push(deploymentsById[id]);
1,149✔
353
          }
354
          return accu;
1,152✔
355
        }, [])
356
        .slice(0, DEPLOYMENT_CUTOFF);
357
      accu.total += byStatus.total;
582✔
358
      return accu;
582✔
359
    },
360
    { total: 0 }
361
  )
362
);
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