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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

97.39
/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 { defaultReports } from '../actions/deviceActions';
17
import { mapUserRolesToUiPermissions } from '../actions/userActions';
18
import { PLANS } from '../constants/appConstants';
19
import { DEPLOYMENT_STATES } from '../constants/deploymentConstants';
20
import {
21
  ALL_DEVICES,
22
  ATTRIBUTE_SCOPES,
23
  DEVICE_ISSUE_OPTIONS,
24
  DEVICE_LIST_MAXIMUM_LENGTH,
25
  DEVICE_ONLINE_CUTOFF,
26
  DEVICE_STATES,
27
  EXTERNAL_PROVIDER,
28
  UNGROUPED_GROUP
29
} from '../constants/deviceConstants';
30
import { READ_STATES, rolesByName, twoFAStates, uiPermissionsById } from '../constants/userConstants';
31
import { attributeDuplicateFilter, duplicateFilter, getDemoDeviceAddress as getDemoDeviceAddressHelper, versionCompare } from '../helpers';
32
import { onboardingSteps } from '../utils/onboardingmanager';
33

34
const getAppDocsVersion = state => state.app.docsVersion;
2,386✔
35
export const getFeatures = state => state.app.features;
27,227✔
36
export const getTooltipsById = state => state.users.tooltips.byId;
4,659✔
37
export const getRolesById = state => state.users.rolesById;
2,755✔
38
export const getOrganization = state => state.organization.organization;
12,575✔
39
export const getAcceptedDevices = state => state.devices.byStatus.accepted;
7,139✔
40
export const getCurrentSession = state => state.users.currentSession;
4,478✔
41
const getDevicesByStatus = state => state.devices.byStatus;
2,386✔
42
export const getDevicesById = state => state.devices.byId;
15,436✔
43
export const getDeviceReports = state => state.devices.reports;
2,255✔
44
export const getGroupsById = state => state.devices.groups.byId;
2,751✔
45
const getSelectedGroup = state => state.devices.groups.selectedGroup;
184✔
46
const getSearchedDevices = state => state.app.searchState.deviceIds;
2,122✔
47
const getListedDevices = state => state.devices.deviceList.deviceIds;
184✔
48
const getFilteringAttributes = state => state.devices.filteringAttributes;
2,302✔
49
export const getDeviceFilters = state => state.devices.filters || [];
305!
50
const getFilteringAttributesFromConfig = state => state.devices.filteringAttributesConfig.attributes;
2,216✔
51
export const getSortedFilteringAttributes = createSelector([getFilteringAttributes], filteringAttributes => ({
184✔
52
  ...filteringAttributes,
53
  identityAttributes: [...filteringAttributes.identityAttributes, 'id']
54
}));
55
export const getDeviceLimit = state => state.devices.limit;
2,291✔
56
const getDevicesList = state => Object.values(state.devices.byId);
184✔
57
const getOnboarding = state => state.onboarding;
2,596✔
58
export const getGlobalSettings = state => state.users.globalSettings;
5,374✔
59
const getIssueCountsByType = state => state.monitor.issueCounts.byType;
2,248✔
60
const getSelectedReleaseId = state => state.releases.selectedRelease;
184✔
61
export const getReleasesById = state => state.releases.byId;
982✔
62
export const getReleaseTags = state => state.releases.tags;
330✔
63
export const getReleaseListState = state => state.releases.releasesList;
577✔
64
const getListedReleases = state => state.releases.releasesList.releaseIds;
184✔
65
export const getUpdateTypes = state => state.releases.updateTypes;
184✔
66
export const getExternalIntegrations = state => state.organization.externalDeviceIntegrations;
184✔
67
const getDeploymentsById = state => state.deployments.byId;
2,719✔
68
export const getDeploymentsByStatus = state => state.deployments.byStatus;
2,587✔
69
const getSelectedDeploymentDeviceIds = state => state.deployments.selectedDeviceIds;
184✔
70
export const getDeploymentsSelectionState = state => state.deployments.selectionState;
857✔
71
export const getFullVersionInformation = state => state.app.versionInformation;
2,180✔
72
export const getAuditLog = state => state.organization.auditlog.events;
184✔
73
export const getAuditLogSelectionState = state => state.organization.auditlog.selectionState;
184✔
74
const getCurrentUserId = state => state.users.currentUser;
4,986✔
75
const getUsersById = state => state.users.byId;
2,770✔
76
export const getCurrentUser = createSelector([getUsersById, getCurrentUserId], (usersById, userId) => usersById[userId] ?? {});
184✔
77
export const getUserSettings = state => state.users.userSettings;
14,171✔
78
export const getSsoConfig = ({ organization: { ssoConfigs = [] } }) => ssoConfigs[0];
184!
79

80
export const getVersionInformation = createSelector([getFullVersionInformation, getFeatures], ({ Integration, ...remainder }, { isHosted }) =>
184✔
81
  isHosted && Integration !== 'next' ? remainder : { ...remainder, Integration }
18!
82
);
83
export const getIsPreview = createSelector([getFullVersionInformation], ({ Integration }) => versionCompare(Integration, 'next') > -1);
184✔
84

85
export const getShowHelptips = createSelector([getTooltipsById], tooltips =>
184✔
86
  Object.values(tooltips).reduce((accu, { readState }) => accu || readState === READ_STATES.unread, false)
4!
87
);
88

89
export const getMappedDeploymentSelection = createSelector(
184✔
90
  [getDeploymentsSelectionState, (_, deploymentsState) => deploymentsState, getDeploymentsById],
453✔
91
  (selectionState, deploymentsState, deploymentsById) => {
92
    const { selection = [] } = selectionState[deploymentsState] ?? {};
240!
93
    return selection.reduce((accu, id) => {
240✔
94
      if (deploymentsById[id]) {
399!
95
        accu.push(deploymentsById[id]);
399✔
96
      }
97
      return accu;
399✔
98
    }, []);
99
  }
100
);
101

102
export const getDeploymentRelease = createSelector(
184✔
103
  [getDeploymentsById, getDeploymentsSelectionState, getReleasesById],
104
  (deploymentsById, { selectedId }, releasesById) => {
105
    const deployment = deploymentsById[selectedId] || {};
3✔
106
    return deployment.artifact_name && releasesById[deployment.artifact_name] ? releasesById[deployment.artifact_name] : { device_types_compatible: [] };
3✔
107
  }
108
);
109

110
export const getSelectedDeploymentData = createSelector(
184✔
111
  [getDeploymentsById, getDeploymentsSelectionState, getDevicesById, getSelectedDeploymentDeviceIds],
112
  (deploymentsById, { selectedId }, devicesById, selectedDeviceIds) => {
113
    const deployment = deploymentsById[selectedId] ?? {};
3✔
114
    const { devices = {} } = deployment;
3✔
115
    return {
3✔
116
      deployment,
117
      selectedDevices: selectedDeviceIds.map(deviceId => ({ ...devicesById[deviceId], ...devices[deviceId] }))
1✔
118
    };
119
  }
120
);
121

122
export const getHas2FA = createSelector(
184✔
123
  [getCurrentUser],
124
  currentUser => currentUser.hasOwnProperty('tfa_status') && currentUser.tfa_status === twoFAStates.enabled
5✔
125
);
126

127
export const getDemoDeviceAddress = createSelector([getDevicesList, getOnboarding], (devices, { approach, demoArtifactPort }) => {
184✔
128
  const demoDeviceAddress = `http://${getDemoDeviceAddressHelper(devices, approach)}`;
5✔
129
  return demoArtifactPort ? `${demoDeviceAddress}:${demoArtifactPort}` : demoDeviceAddress;
5!
130
});
131

132
export const getDeviceReportsForUser = createSelector(
184✔
133
  [getUserSettings, getCurrentUserId, getGlobalSettings, getDevicesById],
134
  ({ reports }, currentUserId, globalSettings, devicesById) => {
135
    return reports || globalSettings[`${currentUserId}-reports`] || (Object.keys(devicesById).length ? defaultReports : []);
79✔
136
  }
137
);
138

139
const listItemMapper = (byId, ids, { defaultObject = {}, cutOffSize = DEVICE_LIST_MAXIMUM_LENGTH }) => {
184✔
140
  return ids.slice(0, cutOffSize).reduce((accu, id) => {
2,259✔
141
    if (id && byId[id]) {
1,315!
142
      accu.push({ ...defaultObject, ...byId[id] });
1,315✔
143
    }
144
    return accu;
1,315✔
145
  }, []);
146
};
147

148
const listTypeDeviceIdMap = {
184✔
149
  deviceList: getListedDevices,
150
  search: getSearchedDevices
151
};
152
const getDeviceMappingDefaults = () => ({ defaultObject: { auth_sets: [] }, cutOffSize: DEVICE_LIST_MAXIMUM_LENGTH });
2,184✔
153
export const getMappedDevicesList = createSelector(
184✔
154
  [getDevicesById, (state, listType) => listTypeDeviceIdMap[listType](state), getDeviceMappingDefaults],
2,184✔
155
  listItemMapper
156
);
157

158
export const getDeviceCountsByStatus = createSelector([getDevicesByStatus], byStatus =>
184✔
159
  Object.values(DEVICE_STATES).reduce((accu, state) => {
804✔
160
    accu[state] = byStatus[state].total || 0;
3,216✔
161
    return accu;
3,216✔
162
  }, {})
163
);
164

165
export const getDeviceById = createSelector([getDevicesById, (_, deviceId) => deviceId], (devicesById, deviceId = '') => devicesById[deviceId] ?? {});
2,712✔
166

167
export const getAuditLogEntry = createSelector([getAuditLog, getAuditLogSelectionState], (events, { selectedId }) => {
184✔
168
  if (!selectedId) {
14✔
169
    return;
9✔
170
  }
171
  const [eventAction, eventTime] = atob(selectedId).split('|');
5✔
172
  return events.find(item => item.action === eventAction && item.time === eventTime);
15✔
173
});
174

175
export const getAuditlogDevice = createSelector([getAuditLogEntry, getDevicesById], (auditlogEvent, devicesById) => {
184✔
176
  let auditlogDevice = {};
7✔
177
  if (auditlogEvent) {
7!
178
    const { object = {} } = auditlogEvent;
7!
179
    const { device = {}, id, type } = object;
7✔
180
    auditlogDevice = type === 'device' ? { id, ...device } : auditlogDevice;
7!
181
  }
182
  return { ...auditlogDevice, ...devicesById[auditlogDevice.id] };
7✔
183
});
184

185
export const getDeviceConfigDeployment = createSelector([getDeviceById, getDeploymentsById], (device, deploymentsById) => {
184✔
186
  const { config = {} } = device;
12✔
187
  const { deployment_id: configDeploymentId } = config;
12✔
188
  const deviceConfigDeployment = deploymentsById[configDeploymentId] || {};
12✔
189
  return { device, deviceConfigDeployment };
12✔
190
});
191

192
export const getSelectedGroupInfo = createSelector(
184✔
193
  [getAcceptedDevices, getGroupsById, getSelectedGroup],
194
  ({ total: acceptedDeviceTotal }, groupsById, selectedGroup) => {
195
    let groupCount = acceptedDeviceTotal;
11✔
196
    let groupFilters = [];
11✔
197
    if (selectedGroup && groupsById[selectedGroup]) {
11✔
198
      groupCount = groupsById[selectedGroup].total;
4✔
199
      groupFilters = groupsById[selectedGroup].filters || [];
4!
200
    }
201
    return { groupCount, selectedGroup, groupFilters };
11✔
202
  }
203
);
204

205
const defaultIdAttribute = Object.freeze({ attribute: 'id', scope: ATTRIBUTE_SCOPES.identity });
184✔
206
export const getIdAttribute = createSelector([getGlobalSettings], ({ id_attribute = { ...defaultIdAttribute } }) => id_attribute);
184✔
207

208
export const getLimitMaxed = createSelector([getAcceptedDevices, getDeviceLimit], ({ total: acceptedDevices = 0 }, deviceLimit) =>
184!
209
  Boolean(deviceLimit && deviceLimit <= acceptedDevices)
11✔
210
);
211

212
export const getFilterAttributes = createSelector(
184✔
213
  [getGlobalSettings, getFilteringAttributes],
214
  ({ previousFilters }, { identityAttributes, inventoryAttributes, systemAttributes, tagAttributes }) => {
215
    const deviceNameAttribute = { key: 'name', value: 'Name', scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 1 };
3✔
216
    const deviceIdAttribute = { key: 'id', value: 'Device ID', scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 };
3✔
217
    const checkInAttribute = { key: 'check_in_time', value: 'Latest activity', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
218
    const updateAttribute = { ...checkInAttribute, key: 'updated_ts', value: 'Last inventory update' };
3✔
219
    const firstRequestAttribute = { key: 'created_ts', value: 'First request', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
220
    const attributes = [
3✔
UNCOV
221
      ...previousFilters.map(item => ({
×
222
        ...item,
223
        value: deviceIdAttribute.key === item.key ? deviceIdAttribute.value : item.key,
×
224
        category: 'recently used',
225
        priority: 0
226
      })),
227
      deviceNameAttribute,
228
      deviceIdAttribute,
229
      ...identityAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 })),
3✔
230
      ...inventoryAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.inventory, category: ATTRIBUTE_SCOPES.inventory, priority: 2 })),
3✔
UNCOV
231
      ...tagAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 3 })),
×
232
      checkInAttribute,
233
      updateAttribute,
234
      firstRequestAttribute,
UNCOV
235
      ...systemAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 }))
×
236
    ];
237
    return attributeDuplicateFilter(attributes, 'key');
3✔
238
  }
239
);
240

241
const getFilteringAttributesLimit = state => state.devices.filteringAttributesLimit;
184✔
242

243
export const getDeviceIdentityAttributes = createSelector(
184✔
244
  [getFilteringAttributes, getFilteringAttributesLimit],
245
  ({ identityAttributes }, filteringAttributesLimit) => {
246
    // limit the selection of the available attribute to AVAILABLE_ATTRIBUTE_LIMIT
247
    const attributes = identityAttributes.slice(0, filteringAttributesLimit);
2✔
248
    return attributes.reduce(
2✔
249
      (accu, value) => {
250
        accu.push({ value, label: value, scope: 'identity' });
3✔
251
        return accu;
3✔
252
      },
253
      [
254
        { value: 'name', label: 'Name', scope: 'tags' },
255
        { value: 'id', label: 'Device ID', scope: 'identity' }
256
      ]
257
    );
258
  }
259
);
260

261
// eslint-disable-next-line no-unused-vars
262
export const getGroupsByIdWithoutUngrouped = createSelector([getGroupsById], ({ [UNGROUPED_GROUP.id]: ungrouped, ...groups }) => groups);
184✔
263

264
export const getGroups = createSelector([getGroupsById], groupsById => {
184✔
265
  const groupNames = Object.keys(groupsById).sort();
16✔
266
  const groupedGroups = Object.entries(groupsById)
16✔
267
    .sort((a, b) => a[0].localeCompare(b[0]))
48✔
268
    .reduce(
269
      (accu, [groupname, group]) => {
270
        const name = groupname === UNGROUPED_GROUP.id ? UNGROUPED_GROUP.name : groupname;
42✔
271
        const groupItem = { ...group, groupId: name, name: groupname };
42✔
272
        if (group.filters.length > 0) {
42✔
273
          if (groupname !== UNGROUPED_GROUP.id) {
27✔
274
            accu.dynamic.push(groupItem);
15✔
275
          } else {
276
            accu.ungrouped.push(groupItem);
12✔
277
          }
278
        } else {
279
          accu.static.push(groupItem);
15✔
280
        }
281
        return accu;
42✔
282
      },
283
      { dynamic: [], static: [], ungrouped: [] }
284
    );
285
  return { groupNames, ...groupedGroups };
16✔
286
});
287

288
export const getDeviceTwinIntegrations = createSelector([getExternalIntegrations], integrations =>
184✔
289
  integrations.filter(integration => integration.id && EXTERNAL_PROVIDER[integration.provider]?.deviceTwin)
4✔
290
);
291

292
export const getOfflineThresholdSettings = createSelector([getGlobalSettings], ({ offlineThreshold }) => ({
184✔
293
  interval: offlineThreshold?.interval || DEVICE_ONLINE_CUTOFF.interval,
35✔
294
  intervalUnit: offlineThreshold?.intervalUnit || DEVICE_ONLINE_CUTOFF.intervalName
35✔
295
}));
296

297
export const getOnboardingState = createSelector([getOnboarding, getUserSettings], ({ complete, progress, showTips, ...remainder }, { onboarding = {} }) => ({
184!
298
  ...remainder,
299
  ...onboarding,
300
  complete: onboarding.complete || complete,
203✔
301
  progress:
302
    Object.keys(onboardingSteps).findIndex(step => step === progress) > Object.keys(onboardingSteps).findIndex(step => step === onboarding.progress)
1,658✔
303
      ? progress
304
      : onboarding.progress,
305
  showTips: !onboarding.showTips ? onboarding.showTips : showTips
103✔
306
}));
307

308
export const getTooltipsState = createSelector([getTooltipsById, getUserSettings], (byId, { tooltips = {} }) =>
184✔
309
  Object.entries(byId).reduce(
64✔
310
    (accu, [id, value]) => {
UNCOV
311
      accu[id] = { ...accu[id], ...value };
×
UNCOV
312
      return accu;
×
313
    },
314
    { ...tooltips }
315
  )
316
);
317

318
export const getDocsVersion = createSelector([getAppDocsVersion, getFeatures], (appDocsVersion, { isHosted }) => {
184✔
319
  // if hosted, use latest docs version
320
  const docsVersion = appDocsVersion ? `${appDocsVersion}/` : 'development/';
42!
321
  return isHosted ? '' : docsVersion;
42✔
322
});
323

324
export const getIsEnterprise = createSelector(
184✔
325
  [getOrganization, getFeatures],
326
  ({ plan = PLANS.os.id }, { isEnterprise, isHosted }) => isEnterprise || (isHosted && plan === PLANS.enterprise.id)
90✔
327
);
328

329
export const getAttributesList = createSelector(
184✔
330
  [getFilteringAttributes, getFilteringAttributesFromConfig],
331
  ({ identityAttributes = [], inventoryAttributes = [] }, { identity = [], inventory = [] }) =>
30!
332
    [...identityAttributes, ...inventoryAttributes, ...identity, ...inventory].filter(duplicateFilter)
15✔
333
);
334

335
export const getRolesList = createSelector([getRolesById], rolesById => Object.entries(rolesById).map(([id, role]) => ({ id, ...role })));
184✔
336

337
export const getUserRoles = createSelector(
184✔
338
  [getCurrentUser, getRolesById, getIsEnterprise, getFeatures, getOrganization],
339
  (currentUser, rolesById, isEnterprise, { isHosted, hasMultitenancy }, { plan = PLANS.os.id }) => {
7✔
340
    const isAdmin = currentUser.roles?.length
73✔
341
      ? currentUser.roles.some(role => role === rolesByName.admin)
70✔
342
      : !(hasMultitenancy || isEnterprise || (isHosted && plan !== PLANS.os.id));
5!
343
    const uiPermissions = isAdmin
73✔
344
      ? mapUserRolesToUiPermissions([rolesByName.admin], rolesById)
345
      : mapUserRolesToUiPermissions(currentUser.roles || [], rolesById);
4✔
346
    return { isAdmin, uiPermissions };
73✔
347
  }
348
);
349

350
const hasPermission = (thing, permission) => Object.values(thing).some(permissions => permissions.includes(permission));
351✔
351

352
export const getUserCapabilities = createSelector([getUserRoles], ({ uiPermissions }) => {
184✔
353
  const canManageReleases = hasPermission(uiPermissions.releases, uiPermissionsById.manage.value);
69✔
354
  const canReadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.read.value);
69✔
355
  const canUploadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.upload.value);
69✔
356

357
  const canAuditlog = uiPermissions.auditlog.includes(uiPermissionsById.read.value);
69✔
358

359
  const canReadUsers = uiPermissions.userManagement.includes(uiPermissionsById.read.value);
69✔
360
  const canManageUsers = uiPermissions.userManagement.includes(uiPermissionsById.manage.value);
69✔
361

362
  const canReadDevices = hasPermission(uiPermissions.groups, uiPermissionsById.read.value);
69✔
363
  const canWriteDevices = Object.values(uiPermissions.groups).some(
69✔
364
    groupPermissions => groupPermissions.includes(uiPermissionsById.read.value) && groupPermissions.length > 1
67✔
365
  );
366
  const canTroubleshoot = hasPermission(uiPermissions.groups, uiPermissionsById.connect.value);
69✔
367
  const canManageDevices = hasPermission(uiPermissions.groups, uiPermissionsById.manage.value);
69✔
368
  const canConfigure = hasPermission(uiPermissions.groups, uiPermissionsById.configure.value);
69✔
369

370
  const canDeploy = uiPermissions.deployments.includes(uiPermissionsById.deploy.value) || hasPermission(uiPermissions.groups, uiPermissionsById.deploy.value);
69✔
371
  const canReadDeployments = uiPermissions.deployments.includes(uiPermissionsById.read.value);
69✔
372

373
  return {
69✔
374
    canAuditlog,
375
    canConfigure,
376
    canDeploy,
377
    canManageDevices,
378
    canManageReleases,
379
    canManageUsers,
380
    canReadDeployments,
381
    canReadDevices,
382
    canReadReleases,
383
    canReadUsers,
384
    canTroubleshoot,
385
    canUploadReleases,
386
    canWriteDevices,
387
    groupsPermissions: uiPermissions.groups,
388
    releasesPermissions: uiPermissions.releases
389
  };
390
});
391

392
export const getTenantCapabilities = createSelector(
184✔
393
  [getFeatures, getOrganization, getIsEnterprise],
394
  (
395
    { hasAuditlogs: isAuditlogEnabled, hasDeviceConfig: isDeviceConfigEnabled, hasDeviceConnect: isDeviceConnectEnabled, hasMonitor: isMonitorEnabled },
396
    { addons = [], plan = PLANS.os.id },
10✔
397
    isEnterprise
398
  ) => {
399
    const canDelta = isEnterprise || plan === PLANS.professional.id;
50✔
400
    const hasAuditlogs = isAuditlogEnabled && isEnterprise;
50✔
401
    const hasDeviceConfig = isDeviceConfigEnabled && addons.some(addon => addon.name === 'configure' && Boolean(addon.enabled));
50✔
402
    const hasDeviceConnect = isDeviceConnectEnabled && (!isEnterprise || addons.some(addon => addon.name === 'troubleshoot' && Boolean(addon.enabled)));
50✔
403
    const hasMonitor = isMonitorEnabled && addons.some(addon => addon.name === 'monitor' && Boolean(addon.enabled));
50✔
404
    return {
50✔
405
      canDelta,
406
      canRetry: canDelta,
407
      canSchedule: canDelta,
408
      hasAuditlogs,
409
      hasDeviceConfig,
410
      hasDeviceConnect,
411
      hasFullFiltering: canDelta,
412
      hasMonitor,
413
      isEnterprise,
414
      plan
415
    };
416
  }
417
);
418

419
export const getAvailableIssueOptionsByType = createSelector(
184✔
420
  [getFeatures, getTenantCapabilities, getIssueCountsByType],
421
  ({ hasReporting }, { hasFullFiltering, hasMonitor }, issueCounts) =>
422
    Object.values(DEVICE_ISSUE_OPTIONS).reduce((accu, { isCategory, key, needsFullFiltering, needsMonitor, needsReporting, title }) => {
18✔
423
      if (isCategory || (needsReporting && !hasReporting) || (needsFullFiltering && !hasFullFiltering) || (needsMonitor && !hasMonitor)) {
108✔
424
        return accu;
102✔
425
      }
426
      accu[key] = { count: issueCounts[key].filtered, key, title };
6✔
427
      return accu;
6✔
428
    }, {})
429
);
430

431
export const getDeviceTypes = createSelector([getAcceptedDevices, getDevicesById], ({ deviceIds = [] }, devicesById) =>
184!
432
  Object.keys(
1✔
433
    deviceIds.slice(0, 200).reduce((accu, item) => {
434
      const { device_type: deviceTypes = [] } = devicesById[item] ? devicesById[item].attributes : {};
2!
435
      accu = deviceTypes.reduce((deviceTypeAccu, deviceType) => {
2✔
436
        if (deviceType.length > 1) {
2!
437
          deviceTypeAccu[deviceType] = deviceTypeAccu[deviceType] ? deviceTypeAccu[deviceType] + 1 : 1;
2!
438
        }
439
        return deviceTypeAccu;
2✔
440
      }, accu);
441
      return accu;
2✔
442
    }, {})
443
  )
444
);
445

446
export const getGroupNames = createSelector([getGroupsById, getUserRoles, (_, options = {}) => options], (groupsById, { uiPermissions }, { staticOnly }) => {
184✔
447
  // eslint-disable-next-line no-unused-vars
448
  const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = groupsById;
156✔
449
  if (staticOnly) {
156!
UNCOV
450
    return Object.keys(uiPermissions.groups).sort();
×
451
  }
452
  return Object.keys(
156✔
453
    Object.entries(groups).reduce((accu, [groupName, group]) => {
454
      if (group.filter || uiPermissions.groups[ALL_DEVICES]) {
312!
455
        accu[groupName] = group;
312✔
456
      }
457
      return accu;
312✔
458
    }, uiPermissions.groups)
459
  ).sort();
460
});
461

462
const getReleaseMappingDefaults = () => ({});
184✔
463
export const getReleasesList = createSelector([getReleasesById, getListedReleases, getReleaseMappingDefaults], listItemMapper);
184✔
464

465
export const getReleaseTagsById = createSelector([getReleaseTags], releaseTags => releaseTags.reduce((accu, key) => ({ ...accu, [key]: key }), {}));
184✔
466
export const getHasReleases = createSelector(
184✔
467
  [getReleaseListState, getReleasesById],
468
  ({ searchTotal, total }, byId) => !!(Object.keys(byId).length || total || searchTotal)
82!
469
);
470

471
export const getSelectedRelease = createSelector([getReleasesById, getSelectedReleaseId], (byId, id) => byId[id] ?? {});
184✔
472

473
const relevantDeploymentStates = [DEPLOYMENT_STATES.pending, DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.finished];
184✔
474
export const DEPLOYMENT_CUTOFF = 3;
184✔
475
export const getRecentDeployments = createSelector([getDeploymentsById, getDeploymentsByStatus], (deploymentsById, deploymentsByStatus) =>
184✔
476
  Object.entries(deploymentsByStatus).reduce(
794✔
477
    (accu, [state, byStatus]) => {
478
      if (!relevantDeploymentStates.includes(state) || !byStatus.deploymentIds.length) {
3,176✔
479
        return accu;
806✔
480
      }
481
      accu[state] = byStatus.deploymentIds
2,370✔
482
        .reduce((accu, id) => {
483
          if (deploymentsById[id]) {
4,692✔
484
            accu.push(deploymentsById[id]);
4,689✔
485
          }
486
          return accu;
4,692✔
487
        }, [])
488
        .slice(0, DEPLOYMENT_CUTOFF);
489
      accu.total += byStatus.total;
2,370✔
490
      return accu;
2,370✔
491
    },
492
    { total: 0 }
493
  )
494
);
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