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

mendersoftware / gui / 891984097

pending completion
891984097

Pull #3741

gitlab-ci

mzedel
chore: made use of common truth function across codebase to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3741: MEN-6487 - fix: added more granular check for device troubleshooting feature

4401 of 6401 branches covered (68.75%)

26 of 27 new or added lines in 8 files covered. (96.3%)

1702 existing lines in 165 files now uncovered.

8060 of 9779 relevant lines covered (82.42%)

123.44 hits per line

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

86.72
/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
  ATTRIBUTE_SCOPES,
21
  DEVICE_ISSUE_OPTIONS,
22
  DEVICE_LIST_MAXIMUM_LENGTH,
23
  DEVICE_ONLINE_CUTOFF,
24
  EXTERNAL_PROVIDER,
25
  UNGROUPED_GROUP
26
} from '../constants/deviceConstants';
27
import { rolesByName, twoFAStates, uiPermissionsById } from '../constants/userConstants';
28
import { attributeDuplicateFilter, duplicateFilter, getDemoDeviceAddress as getDemoDeviceAddressHelper } from '../helpers';
29

30
const getAppDocsVersion = state => state.app.docsVersion;
657✔
31
export const getFeatures = state => state.app.features;
3,453✔
32
const getRolesById = state => state.users.rolesById;
688✔
33
const getOrganization = state => state.organization.organization;
2,057✔
34
const getAcceptedDevices = state => state.devices.byStatus.accepted;
190✔
35
const getDevicesById = state => state.devices.byId;
433✔
36
const getGroupsById = state => state.devices.groups.byId;
190✔
37
const getSearchedDevices = state => state.app.searchState.deviceIds;
406✔
38
const getListedDevices = state => state.devices.deviceList.deviceIds;
190✔
39
const getFilteringAttributes = state => state.devices.filteringAttributes;
190✔
40
const getFilteringAttributesFromConfig = state => state.devices.filteringAttributesConfig.attributes;
190✔
41
const getDeviceLimit = state => state.devices.limit;
190✔
42
const getDevicesList = state => Object.values(state.devices.byId);
190✔
43
const getOnboarding = state => state.onboarding;
695✔
44
const getShowHelptips = state => state.users.showHelptips;
693✔
45
const getGlobalSettings = state => state.users.globalSettings;
683✔
46
const getIssueCountsByType = state => state.monitor.issueCounts.byType;
190✔
47
const getReleasesById = state => state.releases.byId;
190✔
48
const getListedReleases = state => state.releases.releasesList.releaseIds;
190✔
49
const getExternalIntegrations = state => state.organization.externalDeviceIntegrations;
190✔
50
const getDeploymentsById = state => state.deployments.byId;
190✔
51
const getDeploymentsByStatus = state => state.deployments.byStatus;
190✔
52

53
export const getCurrentUser = state => state.users.byId[state.users.currentUser] || {};
741✔
54
export const getUserSettings = state => state.users.userSettings;
1,458✔
55

56
export const getHas2FA = createSelector(
190✔
57
  [getCurrentUser],
58
  currentUser => currentUser.hasOwnProperty('tfa_status') && currentUser.tfa_status === twoFAStates.enabled
2!
59
);
60

61
export const getDemoDeviceAddress = createSelector([getDevicesList, getOnboarding], (devices, { approach, demoArtifactPort }) => {
190✔
62
  const demoDeviceAddress = `http://${getDemoDeviceAddressHelper(devices, approach)}`;
2✔
63
  return demoArtifactPort ? `${demoDeviceAddress}:${demoArtifactPort}` : demoDeviceAddress;
2!
64
});
65

66
const listItemMapper = (byId, ids, { defaultObject = {}, cutOffSize = DEVICE_LIST_MAXIMUM_LENGTH }) => {
190✔
67
  return ids.slice(0, cutOffSize).reduce((accu, id) => {
433✔
68
    if (id && byId[id]) {
198!
69
      accu.push({ ...defaultObject, ...byId[id] });
198✔
70
    }
71
    return accu;
198✔
72
  }, []);
73
};
74

75
const listTypeDeviceIdMap = {
190✔
76
  deviceList: getListedDevices,
77
  search: getSearchedDevices
78
};
79
const getDeviceMappingDefaults = () => ({ defaultObject: { auth_sets: [] }, cutOffSize: DEVICE_LIST_MAXIMUM_LENGTH });
408✔
80
export const getMappedDevicesList = createSelector(
190✔
81
  [getDevicesById, (state, listType) => listTypeDeviceIdMap[listType](state), getDeviceMappingDefaults],
408✔
82
  listItemMapper
83
);
84

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

88
export const getLimitMaxed = createSelector([getAcceptedDevices, getDeviceLimit], ({ total: acceptedDevices = 0 }, deviceLimit) =>
190!
89
  Boolean(deviceLimit && deviceLimit <= acceptedDevices)
4✔
90
);
91

92
export const getFilterAttributes = createSelector(
190✔
93
  [getGlobalSettings, getFilteringAttributes],
94
  ({ previousFilters }, { identityAttributes, inventoryAttributes, systemAttributes, tagAttributes }) => {
95
    const deviceNameAttribute = { key: 'name', value: 'Name', scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 1 };
3✔
96
    const deviceIdAttribute = { key: 'id', value: 'Device ID', scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 };
3✔
97
    const checkInAttribute = { key: 'updated_ts', value: 'Last check-in', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
98
    const firstRequestAttribute = { key: 'created_ts', value: 'First request', scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 };
3✔
99
    const attributes = [
3✔
UNCOV
100
      ...previousFilters.map(item => ({
×
101
        ...item,
102
        value: deviceIdAttribute.key === item.key ? deviceIdAttribute.value : item.key,
×
103
        category: 'recently used',
104
        priority: 0
105
      })),
106
      deviceNameAttribute,
107
      deviceIdAttribute,
108
      ...identityAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.identity, category: ATTRIBUTE_SCOPES.identity, priority: 1 })),
3✔
109
      ...inventoryAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.inventory, category: ATTRIBUTE_SCOPES.inventory, priority: 2 })),
3✔
UNCOV
110
      ...tagAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.tags, category: ATTRIBUTE_SCOPES.tags, priority: 3 })),
×
111
      checkInAttribute,
112
      firstRequestAttribute,
UNCOV
113
      ...systemAttributes.map(item => ({ key: item, value: item, scope: ATTRIBUTE_SCOPES.system, category: ATTRIBUTE_SCOPES.system, priority: 4 }))
×
114
    ];
115
    return attributeDuplicateFilter(attributes, 'key');
3✔
116
  }
117
);
118

119
export const getGroups = createSelector([getGroupsById], groupsById => {
190✔
120
  const groupNames = Object.keys(groupsById).sort();
9✔
121
  const groupedGroups = Object.entries(groupsById)
9✔
122
    .sort((a, b) => a[0].localeCompare(b[0]))
24✔
123
    .reduce(
124
      (accu, [groupname, group]) => {
125
        const name = groupname === UNGROUPED_GROUP.id ? UNGROUPED_GROUP.name : groupname;
23✔
126
        const groupItem = { ...group, groupId: name, name: groupname };
23✔
127
        if (group.filters.length > 0) {
23✔
128
          if (groupname !== UNGROUPED_GROUP.id) {
14✔
129
            accu.dynamic.push(groupItem);
9✔
130
          } else {
131
            accu.ungrouped.push(groupItem);
5✔
132
          }
133
        } else {
134
          accu.static.push(groupItem);
9✔
135
        }
136
        return accu;
23✔
137
      },
138
      { dynamic: [], static: [], ungrouped: [] }
139
    );
140
  return { groupNames, ...groupedGroups };
9✔
141
});
142

143
export const getDeviceTwinIntegrations = createSelector([getExternalIntegrations], integrations =>
190✔
144
  integrations.filter(integration => integration.id && EXTERNAL_PROVIDER[integration.provider]?.deviceTwin)
4✔
145
);
146

147
export const getOfflineThresholdSettings = createSelector([getGlobalSettings], ({ offlineThreshold }) => ({
190✔
148
  interval: offlineThreshold?.interval || DEVICE_ONLINE_CUTOFF.interval,
24✔
149
  intervalUnit: offlineThreshold?.intervalUnit || DEVICE_ONLINE_CUTOFF.intervalName
24✔
150
}));
151

152
export const getOnboardingState = createSelector([getOnboarding, getShowHelptips], ({ complete, progress, showTips }, showHelptips) => ({
190✔
153
  complete,
154
  progress,
155
  showHelptips,
156
  showTips
157
}));
158

159
export const getDocsVersion = createSelector([getAppDocsVersion, getFeatures], (appDocsVersion, { isHosted }) => {
190✔
160
  // if hosted, use latest docs version
161
  const docsVersion = appDocsVersion ? `${appDocsVersion}/` : 'development/';
39!
162
  return isHosted ? '' : docsVersion;
39✔
163
});
164

165
export const getIsEnterprise = createSelector(
190✔
166
  [getOrganization, getFeatures],
167
  ({ plan = PLANS.os.value }, { isEnterprise, isHosted }) => isEnterprise || (isHosted && plan === PLANS.enterprise.value)
59✔
168
);
169

170
export const getAttributesList = createSelector(
190✔
171
  [getFilteringAttributes, getFilteringAttributesFromConfig],
172
  ({ identityAttributes = [], inventoryAttributes = [] }, { identity = [], inventory = [] }) =>
14!
173
    [...identityAttributes, ...inventoryAttributes, ...identity, ...inventory].filter(duplicateFilter)
7✔
174
);
175

176
export const getUserRoles = createSelector(
190✔
177
  [getCurrentUser, getRolesById, getIsEnterprise, getFeatures, getOrganization],
178
  (currentUser, rolesById, isEnterprise, { isHosted, hasMultitenancy }, { plan = PLANS.os.value }) => {
290✔
179
    const isAdmin = currentUser.roles?.length
335✔
180
      ? currentUser.roles.some(role => role === rolesByName.admin)
40✔
181
      : !(hasMultitenancy || isEnterprise || (isHosted && plan !== PLANS.os.value));
317!
182
    const uiPermissions = isAdmin
335✔
183
      ? mapUserRolesToUiPermissions([rolesByName.admin], rolesById)
184
      : mapUserRolesToUiPermissions(currentUser.roles || [], rolesById);
568✔
185
    return { isAdmin, uiPermissions };
335✔
186
  }
187
);
188

189
const hasPermission = (thing, permission) => Object.values(thing).some(permissions => permissions.includes(permission));
2,512✔
190

191
export const getUserCapabilities = createSelector([getUserRoles], ({ uiPermissions }) => {
190✔
192
  const canManageReleases = hasPermission(uiPermissions.releases, uiPermissionsById.manage.value);
332✔
193
  const canReadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.read.value);
332✔
194
  const canUploadReleases = canManageReleases || hasPermission(uiPermissions.releases, uiPermissionsById.upload.value);
332✔
195

196
  const canAuditlog = uiPermissions.auditlog.includes(uiPermissionsById.read.value);
332✔
197

198
  const canReadUsers = uiPermissions.userManagement.includes(uiPermissionsById.read.value);
332✔
199
  const canManageUsers = uiPermissions.userManagement.includes(uiPermissionsById.manage.value);
332✔
200

201
  const canReadDevices = hasPermission(uiPermissions.groups, uiPermissionsById.read.value);
332✔
202
  const canWriteDevices = Object.values(uiPermissions.groups).some(
332✔
203
    groupPermissions => groupPermissions.includes(uiPermissionsById.read.value) && groupPermissions.length > 1
48✔
204
  );
205
  const canTroubleshoot = hasPermission(uiPermissions.groups, uiPermissionsById.connect.value);
332✔
206
  const canManageDevices = hasPermission(uiPermissions.groups, uiPermissionsById.manage.value);
332✔
207
  const canConfigure = hasPermission(uiPermissions.groups, uiPermissionsById.configure.value);
332✔
208

209
  const canDeploy = uiPermissions.deployments.includes(uiPermissionsById.deploy.value) || hasPermission(uiPermissions.groups, uiPermissionsById.deploy.value);
332✔
210
  const canReadDeployments = uiPermissions.deployments.includes(uiPermissionsById.read.value);
332✔
211

212
  return {
332✔
213
    canAuditlog,
214
    canConfigure,
215
    canDeploy,
216
    canManageDevices,
217
    canManageReleases,
218
    canManageUsers,
219
    canReadDeployments,
220
    canReadDevices,
221
    canReadReleases,
222
    canReadUsers,
223
    canTroubleshoot,
224
    canUploadReleases,
225
    canWriteDevices,
226
    groupsPermissions: uiPermissions.groups,
227
    releasesPermissions: uiPermissions.releases
228
  };
229
});
230

231
export const getTenantCapabilities = createSelector(
190✔
232
  [getFeatures, getOrganization, getIsEnterprise],
233
  (
234
    {
235
      hasAuditlogs: isAuditlogEnabled,
236
      hasDeviceConfig: isDeviceConfigEnabled,
237
      hasDeviceConnect: isDeviceConnectEnabled,
238
      hasMonitor: isMonitorEnabled,
239
      isHosted
240
    },
241
    { addons = [], plan },
8✔
242
    isEnterprise
243
  ) => {
244
    const canDelta = isEnterprise || plan === PLANS.professional.value;
39✔
245
    const hasAuditlogs = isAuditlogEnabled && (!isHosted || isEnterprise || plan === PLANS.professional.value);
39!
246
    const hasDeviceConfig = isDeviceConfigEnabled && (!isHosted || addons.some(addon => addon.name === 'configure' && Boolean(addon.enabled)));
39!
247
    const hasDeviceConnect = isDeviceConnectEnabled && (!isHosted || addons.some(addon => addon.name === 'troubleshoot' && Boolean(addon.enabled)));
39!
248
    const hasMonitor = isMonitorEnabled && (!isHosted || addons.some(addon => addon.name === 'monitor' && Boolean(addon.enabled)));
39!
249
    return {
39✔
250
      canDelta,
251
      canRetry: canDelta,
252
      canSchedule: canDelta,
253
      hasAuditlogs,
254
      hasDeviceConfig,
255
      hasDeviceConnect,
256
      hasFullFiltering: canDelta,
257
      hasMonitor,
258
      isEnterprise
259
    };
260
  }
261
);
262

263
export const getAvailableIssueOptionsByType = createSelector(
190✔
264
  [getFeatures, getTenantCapabilities, getIssueCountsByType],
265
  ({ hasReporting }, { hasFullFiltering, hasMonitor }, issueCounts) =>
266
    Object.values(DEVICE_ISSUE_OPTIONS).reduce((accu, { isCategory, key, needsFullFiltering, needsMonitor, needsReporting, title }) => {
14✔
267
      if (isCategory || (needsReporting && !hasReporting) || (needsFullFiltering && !hasFullFiltering) || (needsMonitor && !hasMonitor)) {
84!
268
        return accu;
84✔
269
      }
UNCOV
270
      accu[key] = { count: issueCounts[key].filtered, key, title };
×
UNCOV
271
      return accu;
×
272
    }, {})
273
);
274

275
export const getDeviceTypes = createSelector([getAcceptedDevices, getDevicesById], ({ deviceIds = [] }, devicesById) =>
190!
276
  Object.keys(
1✔
277
    deviceIds.slice(0, 200).reduce((accu, item) => {
278
      const { device_type: deviceTypes = [] } = devicesById[item] ? devicesById[item].attributes : {};
2!
279
      accu = deviceTypes.reduce((deviceTypeAccu, deviceType) => {
2✔
280
        if (deviceType.length > 1) {
2!
281
          deviceTypeAccu[deviceType] = deviceTypeAccu[deviceType] ? deviceTypeAccu[deviceType] + 1 : 1;
2!
282
        }
283
        return deviceTypeAccu;
2✔
284
      }, accu);
285
      return accu;
2✔
286
    }, {})
287
  )
288
);
289

290
const getReleaseMappingDefaults = () => ({});
190✔
291
export const getReleasesList = createSelector([getReleasesById, getListedReleases, getReleaseMappingDefaults], listItemMapper);
190✔
292

293
const relevantDeploymentStates = [DEPLOYMENT_STATES.pending, DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.finished];
190✔
294
export const DEPLOYMENT_CUTOFF = 3;
190✔
295
export const getRecentDeployments = createSelector([getDeploymentsById, getDeploymentsByStatus], (deploymentsById, deploymentsByStatus) =>
190✔
296
  Object.entries(deploymentsByStatus).reduce(
24✔
297
    (accu, [state, byStatus]) => {
298
      if (!relevantDeploymentStates.includes(state) || !byStatus.deploymentIds.length) {
96✔
299
        return accu;
42✔
300
      }
301
      accu[state] = byStatus.deploymentIds
54✔
302
        .reduce((accu, id) => {
303
          if (deploymentsById[id]) {
93✔
304
            accu.push(deploymentsById[id]);
87✔
305
          }
306
          return accu;
93✔
307
        }, [])
308
        .slice(0, DEPLOYMENT_CUTOFF);
309
      accu.total += byStatus.total;
54✔
310
      return accu;
54✔
311
    },
312
    { total: 0 }
313
  )
314
);
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