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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

97.21
/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;
1,240✔
35
export const getFeatures = state => state.app.features;
17,575✔
36
export const getTooltipsById = state => state.users.tooltips.byId;
2,341✔
37
export const getRolesById = state => state.users.rolesById;
1,555✔
38
export const getOrganization = state => state.organization.organization;
6,274✔
39
export const getAcceptedDevices = state => state.devices.byStatus.accepted;
6,366✔
40
export const getCurrentSession = state => state.users.currentSession;
2,874✔
41
const getDevicesByStatus = state => state.devices.byStatus;
1,285✔
42
export const getDevicesById = state => state.devices.byId;
24,653✔
43
export const getDeviceReports = state => state.devices.reports;
3,151✔
44
export const getGroupsById = state => state.devices.groups.byId;
2,708✔
45
const getSelectedGroup = state => state.devices.groups.selectedGroup;
183✔
46
const getSearchedDevices = state => state.app.searchState.deviceIds;
962✔
47
const getListedDevices = state => state.devices.deviceList.deviceIds;
183✔
48
const getFilteringAttributes = state => state.devices.filteringAttributes;
1,100✔
49
export const getDeviceFilters = state => state.devices.filters || [];
184!
50
const getFilteringAttributesFromConfig = state => state.devices.filteringAttributesConfig.attributes;
1,067✔
51
export const getSortedFilteringAttributes = createSelector([getFilteringAttributes], filteringAttributes => ({
183✔
52
  ...filteringAttributes,
53
  identityAttributes: [...filteringAttributes.identityAttributes, 'id']
54
}));
55
export const getDeviceLimit = state => state.devices.limit;
1,795✔
56
const getDevicesList = state => Object.values(state.devices.byId);
183✔
57
const getOnboarding = state => state.onboarding;
1,419✔
58
export const getGlobalSettings = state => state.users.globalSettings;
3,367✔
59
const getIssueCountsByType = state => state.monitor.issueCounts.byType;
1,074✔
60
const getSelectedReleaseId = state => state.releases.selectedRelease;
183✔
61
export const getReleasesById = state => state.releases.byId;
1,472✔
62
export const getReleaseTags = state => state.releases.tags;
256✔
63
export const getReleaseListState = state => state.releases.releasesList;
843✔
64
const getListedReleases = state => state.releases.releasesList.releaseIds;
183✔
65
export const getUpdateTypes = state => state.releases.updateTypes;
183✔
66
export const getExternalIntegrations = state => state.organization.externalDeviceIntegrations;
183✔
67
const getDeploymentsById = state => state.deployments.byId;
2,259✔
68
export const getDeploymentsByStatus = state => state.deployments.byStatus;
1,811✔
69
const getSelectedDeploymentDeviceIds = state => state.deployments.selectedDeviceIds;
183✔
70
export const getDeploymentsSelectionState = state => state.deployments.selectionState;
1,963✔
71
export const getFullVersionInformation = state => state.app.versionInformation;
995✔
72
const getCurrentUserId = state => state.users.currentUser;
2,660✔
73
const getUsersById = state => state.users.byId;
1,593✔
74
export const getCurrentUser = createSelector([getUsersById, getCurrentUserId], (usersById, userId) => usersById[userId] ?? {});
183✔
75
export const getUserSettings = state => state.users.userSettings;
9,654✔
76

77
export const getVersionInformation = createSelector([getFullVersionInformation, getFeatures], ({ Integration, ...remainder }, { isHosted }) =>
183✔
78
  isHosted && Integration !== 'next' ? remainder : { ...remainder, Integration }
9!
79
);
80
export const getIsPreview = createSelector([getFullVersionInformation], ({ Integration }) => versionCompare(Integration, 'next') > -1);
183✔
81

82
export const getShowHelptips = createSelector([getTooltipsById], tooltips =>
183✔
83
  Object.values(tooltips).reduce((accu, { readState }) => accu || readState === READ_STATES.unread, false)
2!
84
);
85

86
export const getMappedDeploymentSelection = createSelector(
183✔
87
  [getDeploymentsSelectionState, (_, deploymentsState) => deploymentsState, getDeploymentsById],
1,169✔
88
  (selectionState, deploymentsState, deploymentsById) => {
89
    const { selection = [] } = selectionState[deploymentsState] ?? {};
1,138!
90
    return selection.reduce((accu, id) => {
1,138✔
91
      if (deploymentsById[id]) {
2,080!
92
        accu.push(deploymentsById[id]);
2,080✔
93
      }
94
      return accu;
2,080✔
95
    }, []);
96
  }
97
);
98

99
export const getDeploymentRelease = createSelector(
183✔
100
  [getDeploymentsById, getDeploymentsSelectionState, getReleasesById],
101
  (deploymentsById, { selectedId }, releasesById) => {
102
    const deployment = deploymentsById[selectedId] || {};
5✔
103
    return deployment.artifact_name && releasesById[deployment.artifact_name] ? releasesById[deployment.artifact_name] : { device_types_compatible: [] };
5✔
104
  }
105
);
106

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

119
export const getHas2FA = createSelector(
183✔
120
  [getCurrentUser],
121
  currentUser => currentUser.hasOwnProperty('tfa_status') && currentUser.tfa_status === twoFAStates.enabled
5✔
122
);
123

124
export const getDemoDeviceAddress = createSelector([getDevicesList, getOnboarding], (devices, { approach, demoArtifactPort }) => {
183✔
125
  const demoDeviceAddress = `http://${getDemoDeviceAddressHelper(devices, approach)}`;
3✔
126
  return demoArtifactPort ? `${demoDeviceAddress}:${demoArtifactPort}` : demoDeviceAddress;
3!
127
});
128

129
export const getDeviceReportsForUser = createSelector(
183✔
130
  [getUserSettings, getCurrentUserId, getGlobalSettings, getDevicesById],
131
  ({ reports }, currentUserId, globalSettings, devicesById) => {
132
    return reports || globalSettings[`${currentUserId}-reports`] || (Object.keys(devicesById).length ? defaultReports : []);
26✔
133
  }
134
);
135

136
const listItemMapper = (byId, ids, { defaultObject = {}, cutOffSize = DEVICE_LIST_MAXIMUM_LENGTH }) => {
183✔
137
  return ids.slice(0, cutOffSize).reduce((accu, id) => {
1,027✔
138
    if (id && byId[id]) {
308!
139
      accu.push({ ...defaultObject, ...byId[id] });
308✔
140
    }
141
    return accu;
308✔
142
  }, []);
143
};
144

145
const listTypeDeviceIdMap = {
183✔
146
  deviceList: getListedDevices,
147
  search: getSearchedDevices
148
};
149
const getDeviceMappingDefaults = () => ({ defaultObject: { auth_sets: [] }, cutOffSize: DEVICE_LIST_MAXIMUM_LENGTH });
989✔
150
export const getMappedDevicesList = createSelector(
183✔
151
  [getDevicesById, (state, listType) => listTypeDeviceIdMap[listType](state), getDeviceMappingDefaults],
989✔
152
  listItemMapper
153
);
154

155
export const getDeviceCountsByStatus = createSelector([getDevicesByStatus], byStatus =>
183✔
156
  Object.values(DEVICE_STATES).reduce((accu, state) => {
407✔
157
    accu[state] = byStatus[state].total || 0;
1,628✔
158
    return accu;
1,628✔
159
  }, {})
160
);
161

162
export const getDeviceById = createSelector([getDevicesById, (_, deviceId) => deviceId], (devicesById, deviceId = '') => devicesById[deviceId] ?? {});
1,450✔
163

164
export const getDeviceConfigDeployment = createSelector([getDeviceById, getDeploymentsById], (device, deploymentsById) => {
183✔
165
  const { config = {} } = device;
7✔
166
  const { deployment_id: configDeploymentId } = config;
7✔
167
  const deviceConfigDeployment = deploymentsById[configDeploymentId] || {};
7✔
168
  return { device, deviceConfigDeployment };
7✔
169
});
170

171
export const getSelectedGroupInfo = createSelector(
183✔
172
  [getAcceptedDevices, getGroupsById, getSelectedGroup],
173
  ({ total: acceptedDeviceTotal }, groupsById, selectedGroup) => {
174
    let groupCount = acceptedDeviceTotal;
7✔
175
    let groupFilters = [];
7✔
176
    if (selectedGroup && groupsById[selectedGroup]) {
7✔
177
      groupCount = groupsById[selectedGroup].total;
2✔
178
      groupFilters = groupsById[selectedGroup].filters.map(filter => ({ ...filter, isGroupFilter: true })) || [];
2!
179
    }
180
    return { groupCount, selectedGroup, groupFilters };
7✔
181
  }
182
);
183

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

187
export const getLimitMaxed = createSelector([getAcceptedDevices, getDeviceLimit], ({ total: acceptedDevices = 0 }, deviceLimit) =>
183!
188
  Boolean(deviceLimit && deviceLimit <= acceptedDevices)
7✔
189
);
190

191
export const getFilterAttributes = createSelector(
183✔
192
  [getGlobalSettings, getFilteringAttributes],
193
  ({ previousFilters }, { identityAttributes, inventoryAttributes, systemAttributes, tagAttributes }) => {
194
    const deviceNameAttribute = { key: 'name', value: 'Name', scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 1 };
3✔
195
    const deviceIdAttribute = { key: 'id', value: 'Device ID', scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 };
3✔
196
    const checkInAttribute = { key: 'check_in_time', value: 'Latest activity', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
197
    const updateAttribute = { ...checkInAttribute, key: 'updated_ts', value: 'Last inventory update' };
3✔
198
    const firstRequestAttribute = { key: 'created_ts', value: 'First request', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
199
    const attributes = [
3✔
200
      ...previousFilters.map(item => ({
×
201
        ...item,
202
        value: deviceIdAttribute.key === item.key ? deviceIdAttribute.value : item.key,
×
203
        category: 'recently used',
204
        priority: 0
205
      })),
206
      deviceNameAttribute,
207
      deviceIdAttribute,
208
      ...identityAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 })),
3✔
209
      ...inventoryAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.inventory, category: ATTRIBUTE_SCOPES.inventory, priority: 2 })),
3✔
210
      ...tagAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 3 })),
×
211
      checkInAttribute,
212
      updateAttribute,
213
      firstRequestAttribute,
214
      ...systemAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 }))
×
215
    ];
216
    return attributeDuplicateFilter(attributes, 'key');
3✔
217
  }
218
);
219

220
const getFilteringAttributesLimit = state => state.devices.filteringAttributesLimit;
183✔
221

222
export const getDeviceIdentityAttributes = createSelector(
183✔
223
  [getFilteringAttributes, getFilteringAttributesLimit],
224
  ({ identityAttributes }, filteringAttributesLimit) => {
225
    // limit the selection of the available attribute to AVAILABLE_ATTRIBUTE_LIMIT
226
    const attributes = identityAttributes.slice(0, filteringAttributesLimit);
2✔
227
    return attributes.reduce(
2✔
228
      (accu, value) => {
229
        accu.push({ value, label: value, scope: 'identity' });
3✔
230
        return accu;
3✔
231
      },
232
      [
233
        { value: 'name', label: 'Name', scope: 'tags' },
234
        { value: 'id', label: 'Device ID', scope: 'identity' }
235
      ]
236
    );
237
  }
238
);
239

240
// eslint-disable-next-line no-unused-vars
241
export const getGroupsByIdWithoutUngrouped = createSelector([getGroupsById], ({ [UNGROUPED_GROUP.id]: ungrouped, ...groups }) => groups);
183✔
242

243
export const getGroups = createSelector([getGroupsById], groupsById => {
183✔
244
  const groupNames = Object.keys(groupsById).sort();
11✔
245
  const groupedGroups = Object.entries(groupsById)
11✔
246
    .sort((a, b) => a[0].localeCompare(b[0]))
35✔
247
    .reduce(
248
      (accu, [groupname, group]) => {
249
        const name = groupname === UNGROUPED_GROUP.id ? UNGROUPED_GROUP.name : groupname;
30✔
250
        const groupItem = { ...group, groupId: name, name: groupname };
30✔
251
        if (group.filters.length > 0) {
30✔
252
          if (groupname !== UNGROUPED_GROUP.id) {
19✔
253
            accu.dynamic.push(groupItem);
11✔
254
          } else {
255
            accu.ungrouped.push(groupItem);
8✔
256
          }
257
        } else {
258
          accu.static.push(groupItem);
11✔
259
        }
260
        return accu;
30✔
261
      },
262
      { dynamic: [], static: [], ungrouped: [] }
263
    );
264
  return { groupNames, ...groupedGroups };
11✔
265
});
266

267
export const getDeviceTwinIntegrations = createSelector([getExternalIntegrations], integrations =>
183✔
268
  integrations.filter(integration => integration.id && EXTERNAL_PROVIDER[integration.provider]?.deviceTwin)
4✔
269
);
270

271
export const getOfflineThresholdSettings = createSelector([getGlobalSettings], ({ offlineThreshold }) => ({
183✔
272
  interval: offlineThreshold?.interval || DEVICE_ONLINE_CUTOFF.interval,
18✔
273
  intervalUnit: offlineThreshold?.intervalUnit || DEVICE_ONLINE_CUTOFF.intervalName
18✔
274
}));
275

276
export const getOnboardingState = createSelector([getOnboarding, getUserSettings], ({ complete, progress, showTips, ...remainder }, { onboarding = {} }) => ({
183!
277
  ...remainder,
278
  ...onboarding,
279
  complete: onboarding.complete || complete,
126✔
280
  progress:
281
    Object.keys(onboardingSteps).findIndex(step => step === progress) > Object.keys(onboardingSteps).findIndex(step => step === onboarding.progress)
1,027✔
282
      ? progress
283
      : onboarding.progress,
284
  showTips: !onboarding.showTips ? onboarding.showTips : showTips
64!
285
}));
286

287
export const getTooltipsState = createSelector([getTooltipsById, getUserSettings], (byId, { tooltips = {} }) =>
183✔
288
  Object.entries(byId).reduce(
44✔
289
    (accu, [id, value]) => {
290
      accu[id] = { ...accu[id], ...value };
×
291
      return accu;
×
292
    },
293
    { ...tooltips }
294
  )
295
);
296

297
export const getDocsVersion = createSelector([getAppDocsVersion, getFeatures], (appDocsVersion, { isHosted }) => {
183✔
298
  // if hosted, use latest docs version
299
  const docsVersion = appDocsVersion ? `${appDocsVersion}/` : 'development/';
37!
300
  return isHosted ? '' : docsVersion;
37✔
301
});
302

303
export const getIsEnterprise = createSelector(
183✔
304
  [getOrganization, getFeatures],
305
  ({ plan = PLANS.os.id }, { isEnterprise, isHosted }) => isEnterprise || (isHosted && plan === PLANS.enterprise.id)
74✔
306
);
307

308
export const getAttributesList = createSelector(
183✔
309
  [getFilteringAttributes, getFilteringAttributesFromConfig],
310
  ({ identityAttributes = [], inventoryAttributes = [] }, { identity = [], inventory = [] }) =>
34!
311
    [...identityAttributes, ...inventoryAttributes, ...identity, ...inventory].filter(duplicateFilter)
17✔
312
);
313

314
export const getRolesList = createSelector([getRolesById], rolesById => Object.entries(rolesById).map(([id, role]) => ({ id, ...role })));
183✔
315

316
export const getUserRoles = createSelector(
183✔
317
  [getCurrentUser, getRolesById, getIsEnterprise, getFeatures, getOrganization],
318
  (currentUser, rolesById, isEnterprise, { isHosted, hasMultitenancy }, { plan = PLANS.os.id }) => {
3✔
319
    const isAdmin = currentUser.roles?.length
62✔
320
      ? currentUser.roles.some(role => role === rolesByName.admin)
60✔
321
      : !(hasMultitenancy || isEnterprise || (isHosted && plan !== PLANS.os.id));
6!
322
    const uiPermissions = isAdmin
62!
323
      ? mapUserRolesToUiPermissions([rolesByName.admin], rolesById)
324
      : mapUserRolesToUiPermissions(currentUser.roles || [], rolesById);
×
325
    return { isAdmin, uiPermissions };
62✔
326
  }
327
);
328

329
const hasPermission = (thing, permission) => Object.values(thing).some(permissions => permissions.includes(permission));
275✔
330

331
export const getUserCapabilities = createSelector([getUserRoles], ({ uiPermissions }) => {
183✔
332
  const canManageReleases = hasPermission(uiPermissions.releases, uiPermissionsById.manage.value);
55✔
333
  const canReadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.read.value);
55!
334
  const canUploadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.upload.value);
55!
335

336
  const canAuditlog = uiPermissions.auditlog.includes(uiPermissionsById.read.value);
55✔
337

338
  const canReadUsers = uiPermissions.userManagement.includes(uiPermissionsById.read.value);
55✔
339
  const canManageUsers = uiPermissions.userManagement.includes(uiPermissionsById.manage.value);
55✔
340

341
  const canReadDevices = hasPermission(uiPermissions.groups, uiPermissionsById.read.value);
55✔
342
  const canWriteDevices = Object.values(uiPermissions.groups).some(
55✔
343
    groupPermissions => groupPermissions.includes(uiPermissionsById.read.value) && groupPermissions.length > 1
55✔
344
  );
345
  const canTroubleshoot = hasPermission(uiPermissions.groups, uiPermissionsById.connect.value);
55✔
346
  const canManageDevices = hasPermission(uiPermissions.groups, uiPermissionsById.manage.value);
55✔
347
  const canConfigure = hasPermission(uiPermissions.groups, uiPermissionsById.configure.value);
55✔
348

349
  const canDeploy = uiPermissions.deployments.includes(uiPermissionsById.deploy.value) || hasPermission(uiPermissions.groups, uiPermissionsById.deploy.value);
55!
350
  const canReadDeployments = uiPermissions.deployments.includes(uiPermissionsById.read.value);
55✔
351

352
  return {
55✔
353
    canAuditlog,
354
    canConfigure,
355
    canDeploy,
356
    canManageDevices,
357
    canManageReleases,
358
    canManageUsers,
359
    canReadDeployments,
360
    canReadDevices,
361
    canReadReleases,
362
    canReadUsers,
363
    canTroubleshoot,
364
    canUploadReleases,
365
    canWriteDevices,
366
    groupsPermissions: uiPermissions.groups,
367
    releasesPermissions: uiPermissions.releases
368
  };
369
});
370

371
export const getTenantCapabilities = createSelector(
183✔
372
  [getFeatures, getOrganization, getIsEnterprise],
373
  (
374
    {
375
      hasAddons,
376
      hasAuditlogs: isAuditlogEnabled,
377
      hasDeviceConfig: isDeviceConfigEnabled,
378
      hasDeviceConnect: isDeviceConnectEnabled,
379
      hasMonitor: isMonitorEnabled,
380
      isHosted
381
    },
382
    { addons = [], plan = PLANS.os.id },
4✔
383
    isEnterprise
384
  ) => {
385
    const canDelta = isEnterprise || plan === PLANS.professional.id;
42✔
386
    const hasAuditlogs = isAuditlogEnabled && (!isHosted || isEnterprise || plan === PLANS.professional.id);
42!
387
    const hasDeviceConfig = hasAddons || (isDeviceConfigEnabled && (!isHosted || addons.some(addon => addon.name === 'configure' && Boolean(addon.enabled))));
42!
388
    const hasDeviceConnect =
389
      hasAddons || (isDeviceConnectEnabled && (!isHosted || addons.some(addon => addon.name === 'troubleshoot' && Boolean(addon.enabled))));
42!
390
    const hasMonitor = hasAddons || (isMonitorEnabled && (!isHosted || addons.some(addon => addon.name === 'monitor' && Boolean(addon.enabled))));
42!
391
    return {
42✔
392
      canDelta,
393
      canRetry: canDelta,
394
      canSchedule: canDelta,
395
      hasAuditlogs,
396
      hasDeviceConfig,
397
      hasDeviceConnect,
398
      hasFullFiltering: canDelta,
399
      hasMonitor,
400
      isEnterprise,
401
      plan
402
    };
403
  }
404
);
405

406
export const getAvailableIssueOptionsByType = createSelector(
183✔
407
  [getFeatures, getTenantCapabilities, getIssueCountsByType],
408
  ({ hasReporting }, { hasFullFiltering, hasMonitor }, issueCounts) =>
409
    Object.values(DEVICE_ISSUE_OPTIONS).reduce((accu, { isCategory, key, needsFullFiltering, needsMonitor, needsReporting, title }) => {
12✔
410
      if (isCategory || (needsReporting && !hasReporting) || (needsFullFiltering && !hasFullFiltering) || (needsMonitor && !hasMonitor)) {
72✔
411
        return accu;
66✔
412
      }
413
      accu[key] = { count: issueCounts[key].filtered, key, title };
6✔
414
      return accu;
6✔
415
    }, {})
416
);
417

418
export const getDeviceTypes = createSelector([getAcceptedDevices, getDevicesById], ({ deviceIds = [] }, devicesById) =>
183!
419
  Object.keys(
1✔
420
    deviceIds.slice(0, 200).reduce((accu, item) => {
421
      const { device_type: deviceTypes = [] } = devicesById[item] ? devicesById[item].attributes : {};
2!
422
      accu = deviceTypes.reduce((deviceTypeAccu, deviceType) => {
2✔
423
        if (deviceType.length > 1) {
2!
424
          deviceTypeAccu[deviceType] = deviceTypeAccu[deviceType] ? deviceTypeAccu[deviceType] + 1 : 1;
2!
425
        }
426
        return deviceTypeAccu;
2✔
427
      }, accu);
428
      return accu;
2✔
429
    }, {})
430
  )
431
);
432

433
export const getGroupNames = createSelector([getGroupsById, getUserRoles, (_, options = {}) => options], (groupsById, { uiPermissions }, { staticOnly }) => {
1,297✔
434
  // eslint-disable-next-line no-unused-vars
435
  const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = groupsById;
1,297✔
436
  if (staticOnly) {
1,297!
437
    return Object.keys(uiPermissions.groups).sort();
×
438
  }
439
  return Object.keys(
1,297✔
440
    Object.entries(groups).reduce((accu, [groupName, group]) => {
441
      if (group.filter || uiPermissions.groups[ALL_DEVICES]) {
2,592!
442
        accu[groupName] = group;
2,592✔
443
      }
444
      return accu;
2,592✔
445
    }, uiPermissions.groups)
446
  ).sort();
447
});
448

449
const getReleaseMappingDefaults = () => ({});
183✔
450
export const getReleasesList = createSelector([getReleasesById, getListedReleases, getReleaseMappingDefaults], listItemMapper);
183✔
451

452
export const getReleaseTagsById = createSelector([getReleaseTags], releaseTags => releaseTags.reduce((accu, key) => ({ ...accu, [key]: key }), {}));
183✔
453
export const getHasReleases = createSelector(
183✔
454
  [getReleaseListState, getReleasesById],
455
  ({ searchTotal, total }, byId) => !!(Object.keys(byId).length || total || searchTotal)
32!
456
);
457

458
export const getSelectedRelease = createSelector([getReleasesById, getSelectedReleaseId], (byId, id) => byId[id] ?? {});
183✔
459

460
const relevantDeploymentStates = [DEPLOYMENT_STATES.pending, DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.finished];
183✔
461
export const DEPLOYMENT_CUTOFF = 3;
183✔
462
export const getRecentDeployments = createSelector([getDeploymentsById, getDeploymentsByStatus], (deploymentsById, deploymentsByStatus) =>
183✔
463
  Object.entries(deploymentsByStatus).reduce(
404✔
464
    (accu, [state, byStatus]) => {
465
      if (!relevantDeploymentStates.includes(state) || !byStatus.deploymentIds.length) {
1,616✔
466
        return accu;
407✔
467
      }
468
      accu[state] = byStatus.deploymentIds
1,209✔
469
        .reduce((accu, id) => {
470
          if (deploymentsById[id]) {
2,343✔
471
            accu.push(deploymentsById[id]);
2,337✔
472
          }
473
          return accu;
2,343✔
474
        }, [])
475
        .slice(0, DEPLOYMENT_CUTOFF);
476
      accu.total += byStatus.total;
1,209✔
477
      return accu;
1,209✔
478
    },
479
    { total: 0 }
480
  )
481
);
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