• 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

98.32
/src/js/utils/locationutils.js
1
// Copyright 2022 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 { routes } from '../components/devices/base-devices';
15
import { DEPLOYMENT_ROUTES, DEPLOYMENT_STATES, DEPLOYMENT_TYPES } from '../constants/deploymentConstants';
16
import { ALL_DEVICES, ATTRIBUTE_SCOPES, DEVICE_FILTERING_OPTIONS, DEVICE_LIST_DEFAULTS, UNGROUPED_GROUP, emptyFilter } from '../constants/deviceConstants';
17
import { AUDIT_LOGS_TYPES } from '../constants/organizationConstants';
18
import { deepCompare, getISOStringBoundaries } from '../helpers';
19

20
const SEPARATOR = ':';
184✔
21

22
const defaultSelector = result => result[0];
184✔
23

24
const commonFields = {
184✔
25
  ...Object.keys(DEVICE_LIST_DEFAULTS).reduce((accu, key) => ({ ...accu, [key]: { parse: Number, select: defaultSelector, target: key } }), {}),
368✔
26
  id: { parse: String, select: i => i, target: 'id' },
2✔
27
  issues: { parse: undefined, select: defaultSelector, target: 'selectedIssues' },
28
  open: { parse: Boolean, select: defaultSelector, target: 'open' }
29
};
30
const sortingFields = ['scope', 'key', 'direction'];
184✔
31
const parsingSortingFields = [...sortingFields].reverse();
184✔
32

33
const scopes = {
184✔
34
  identity: { delimiter: 'identity', filters: [] },
35
  inventory: { delimiter: 'inventory', filters: [] },
36
  monitor: { delimiter: 'monitor', filters: [] },
37
  system: { delimiter: 'system', filters: [] },
38
  tags: { delimiter: 'tags', filters: [] }
39
};
40

41
export const commonProcessor = searchParams => {
184✔
42
  let params = new URLSearchParams(searchParams);
37✔
43
  const pageState = Object.entries(commonFields).reduce((accu, [key, { parse, select, target }]) => {
37✔
44
    const values = params.getAll(key);
185✔
45
    if (!values.length) {
185✔
46
      return accu;
173✔
47
    }
48
    if (!parse) {
12✔
49
      accu[target] = values;
1✔
50
    } else {
51
      try {
11✔
52
        accu[target] = select(values.map(parse));
11✔
53
      } catch (error) {
UNCOV
54
        console.log('encountered faulty url param, continue...', error);
×
55
      }
56
    }
57
    return accu;
12✔
58
  }, {});
59
  Object.keys(commonFields).map(key => params.delete(key));
185✔
60
  const sort = params.has('sort')
37✔
61
    ? params.getAll('sort').reduce((sortAccu, scopedQuery) => {
62
        // reverse the items to ensure the optional scope is only considered if it is present
63
        const items = scopedQuery.split(SEPARATOR).reverse();
8✔
64
        const parsedSortItem = parsingSortingFields.reduce((accu, key, index) => {
8✔
65
          if (items[index]) {
24✔
66
            accu[key] = items[index];
16✔
67
          }
68
          return accu;
24✔
69
        }, {});
70
        sortAccu.push(parsedSortItem);
8✔
71
        return sortAccu;
8✔
72
      }, [])
73
    : [];
74
  params.delete('sort');
37✔
75
  return { pageState, params, sort };
37✔
76
};
77

78
const legacyDeviceQueryParse = (searchParams, filteringAttributes) => {
184✔
79
  let params = new URLSearchParams(searchParams);
1✔
80
  const result = Object.keys(scopes).reduce((accu, scope) => ({ ...accu, [scope]: [] }), {});
5✔
81
  if (params.get('group')) {
1!
82
    result.inventory.push({ ...emptyFilter, key: 'group', scope: 'inventory', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: params.get('group') });
1✔
83
    params.delete('group');
1✔
84
  }
85
  const filters = [...params.keys()].reduce(
1✔
86
    (accu, key) =>
87
      params.getAll(key).reduce((innerAccu, value) => {
3✔
88
        const scope =
89
          Object.entries(filteringAttributes).reduce((foundScope, [currentScope, attributes]) => {
3✔
90
            if (foundScope) {
6✔
91
              return foundScope;
1✔
92
            }
93
            return attributes.includes(key) ? currentScope.substring(0, currentScope.indexOf('Attributes')) : foundScope;
5✔
94
          }, undefined) ?? ATTRIBUTE_SCOPES.inventory;
95
        innerAccu[scope].push({ ...emptyFilter, scope, key, operator: DEVICE_FILTERING_OPTIONS.$eq.key, value });
3✔
96
        return innerAccu;
3✔
97
      }, accu),
98
    result
99
  );
100
  [...params.keys()].map(key => params.delete(key));
3✔
101
  return { filters, params };
1✔
102
};
103

104
const scopedFilterParse = searchParams => {
184✔
105
  let params = new URLSearchParams(searchParams);
4✔
106
  const filters = Object.keys(scopes).reduce(
4✔
107
    (accu, scope) => {
108
      accu[scope] = [];
20✔
109
      if (!params.has(scope)) {
20✔
110
        return accu;
16✔
111
      }
112
      accu[scope] = params.getAll(scope).map(scopedQuery => {
4✔
113
        const items = scopedQuery.split(SEPARATOR);
5✔
114
        // URLSearchParams will automatically decode any URI encoding present in the query string, thus we have to also handle queries with a SEPARATOR separately
115
        return { ...emptyFilter, scope, key: items[0], operator: `$${items[1]}`, value: items.slice(2).join(SEPARATOR) };
5✔
116
      });
117
      return accu;
4✔
118
    },
119
    { ...scopes }
120
  );
121
  Object.keys(scopes).map(scope => params.delete(scope));
20✔
122
  return { filters, params };
4✔
123
};
124

125
// filters, selectedGroup
126
export const parseDeviceQuery = (searchParams, extraProps = {}) => {
184✔
127
  let queryParams = new URLSearchParams(searchParams);
5✔
128
  const { filteringAttributes = {}, pageState = {} } = extraProps;
5✔
129
  const pageStateExtension = pageState.id?.length === 1 ? { open: true } : {};
5✔
130

131
  let scopedFilters;
132
  const refersOldStyleAttributes = Object.values(filteringAttributes).some(scopeValues => scopeValues.some(scopedValue => queryParams.get(scopedValue)));
5✔
133
  if ((refersOldStyleAttributes && !Object.keys(scopes).some(scope => queryParams.get(scope))) || queryParams.get('group')) {
5✔
134
    const { filters, params } = legacyDeviceQueryParse(queryParams, filteringAttributes);
1✔
135
    scopedFilters = filters;
1✔
136
    queryParams = params;
1✔
137
  } else {
138
    const { filters, params } = scopedFilterParse(queryParams);
4✔
139
    scopedFilters = filters;
4✔
140
    queryParams = params;
4✔
141
  }
142

143
  let groupName = '';
5✔
144
  const groupFilterIndex = scopedFilters.inventory.findIndex(filter => filter.key === 'group' && filter.operator === DEVICE_FILTERING_OPTIONS.$eq.key);
5✔
145
  if (groupFilterIndex > -1) {
5✔
146
    groupName = scopedFilters.inventory[groupFilterIndex].value;
3✔
147
    scopedFilters.inventory.splice(groupFilterIndex, 1);
3✔
148
  }
149

150
  const detailsTab = queryParams.has('tab') ? queryParams.get('tab') : '';
5!
151
  return { detailsTab, filters: Object.values(scopedFilters).flat(), groupName, ...pageStateExtension };
5✔
152
};
153

154
const formatSorting = (sort, { sort: sortDefault = [] }) => {
184✔
155
  if (!sort || deepCompare(sort, sortDefault)) {
67✔
156
    return '';
57✔
157
  }
158
  let sorter = sort;
10✔
159
  if (!Array.isArray(sort)) {
10✔
160
    sorter = [sort];
9✔
161
  }
162
  const queries = sorter.reduce((accu, sortOption) => {
10✔
163
    const sortQuery = sortingFields
12✔
164
      .reduce((fieldsAccu, key) => {
165
        if (!sortOption[key]) {
36✔
166
          return fieldsAccu;
11✔
167
        }
168
        fieldsAccu.push(sortOption[key]);
25✔
169
        return fieldsAccu;
25✔
170
      }, [])
171
      .join(SEPARATOR);
172
    accu.push(`sort=${sortQuery}`);
12✔
173
    return accu;
12✔
174
  }, []);
175
  return queries.join('&');
10✔
176
};
177

178
export const formatPageState = ({ selectedId, selectedIssues, page, perPage, sort }, { defaults = {} }) =>
184!
179
  Object.entries({ page, perPage, id: selectedId, issues: selectedIssues, open: selectedId ? true : undefined })
67✔
180
    .reduce(
181
      (accu, [key, value]) => {
182
        if (Array.isArray(value)) {
335✔
183
          accu.push(...value.map(item => `${key}=${encodeURIComponent(item)}`));
2✔
184
        } else if ((DEVICE_LIST_DEFAULTS[key] != value || !DEVICE_LIST_DEFAULTS.hasOwnProperty(key)) && value) {
334✔
185
          accu.push(`${key}=${encodeURIComponent(value)}`);
12✔
186
        }
187
        return accu;
335✔
188
      },
189
      [formatSorting(sort, defaults)]
190
    )
191
    .filter(i => i)
81✔
192
    .join('&');
193

194
const stripFilterOperator = operator => operator.replaceAll('$', '');
184✔
195

196
const formatFilters = filters => {
184✔
197
  const result = filters
44✔
198
    // group all filters by their scope to get a more organized result
199
    .reduce(
200
      (accu, filter) => {
201
        const { scope = ATTRIBUTE_SCOPES.inventory, operator = '$eq' } = filter;
9✔
202
        accu[scope].add(`${scopes[scope].delimiter}=${filter.key}${SEPARATOR}${stripFilterOperator(operator)}${SEPARATOR}${encodeURIComponent(filter.value)}`);
9✔
203
        return accu;
9✔
204
      },
205
      Object.keys(scopes).reduce((accu, item) => ({ ...accu, [item]: new Set() }), {})
220✔
206
    );
207
  // boil it all down to a single line containing all filters
208
  return Object.values(result)
44✔
209
    .map(filterSet => [...filterSet])
220✔
210
    .flat();
211
};
212

213
export const formatDeviceSearch = ({ pageState, filters, selectedGroup }) => {
184✔
214
  let activeFilters = [...filters];
44✔
215
  if (selectedGroup && selectedGroup !== ALL_DEVICES) {
44✔
216
    const isUngroupedGroup = selectedGroup === UNGROUPED_GROUP.id;
5✔
217
    activeFilters = isUngroupedGroup
5✔
218
      ? activeFilters.filter(
219
          filter => !(filter.key === 'group' && filter.scope === ATTRIBUTE_SCOPES.system && filter.operator === DEVICE_FILTERING_OPTIONS.$nin.key)
1!
220
        )
221
      : activeFilters;
222
    const groupName = isUngroupedGroup ? UNGROUPED_GROUP.name : selectedGroup;
5✔
223
    activeFilters.push({ scope: ATTRIBUTE_SCOPES.inventory, key: 'group', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: groupName });
5✔
224
  }
225
  const formattedFilters = formatFilters(activeFilters).filter(i => i);
44✔
226
  if (pageState.detailsTab && pageState.selectedId) {
44!
UNCOV
227
    formattedFilters.push(`tab=${pageState.detailsTab}`);
×
228
  }
229
  return formattedFilters.join('&');
44✔
230
};
231

232
export const generateDevicePath = ({ pageState }) => {
184✔
233
  const { state: selectedState } = pageState;
2✔
234
  const path = ['/devices'];
2✔
235
  if (![routes.allDevices.key, ''].includes(selectedState)) {
2!
236
    path.push(selectedState);
2✔
237
  }
238
  return path.join('/');
2✔
239
};
240

241
const formatDates = ({ endDate, params, startDate, today, tonight }) => {
184✔
242
  if (endDate && endDate !== tonight) {
22✔
243
    params.set('endDate', endDate.split('T')[0]);
2✔
244
  }
245
  if (startDate && startDate !== today) {
22✔
246
    params.set('startDate', startDate.split('T')[0]);
17✔
247
  }
248
  return params;
22✔
249
};
250

251
const paramReducer = (accu, [key, value]) => {
184✔
252
  if (value) {
44✔
253
    accu.set(key, value);
17✔
254
  }
255
  return accu;
44✔
256
};
257

258
export const formatAuditlogs = ({ pageState }, { today, tonight }) => {
184✔
259
  const { detail, endDate, startDate, type, user } = pageState;
13✔
260
  let params = new URLSearchParams();
13✔
261
  params = Object.entries({ objectId: detail, userId: user ? user.id ?? user : user }).reduce(paramReducer, params);
13✔
262
  if (type) {
13!
263
    params.set('objectType', type.value ?? type);
13✔
264
  }
265
  params = formatDates({ endDate, params, startDate, today, tonight });
13✔
266
  return params.toString();
13✔
267
};
268

269
const parseDateParams = (params, today, tonight) => {
184✔
270
  let endDate = tonight;
34✔
271
  if (params.get('endDate')) {
34✔
272
    endDate = getISOStringBoundaries(new Date(params.get('endDate'))).end;
2✔
273
  }
274
  let startDate = today;
34✔
275
  if (params.get('startDate')) {
34✔
276
    startDate = getISOStringBoundaries(new Date(params.get('startDate'))).start;
5✔
277
  }
278
  return { endDate, startDate };
34✔
279
};
280

281
export const parseAuditlogsQuery = (params, { today, tonight }) => {
184✔
282
  const type = AUDIT_LOGS_TYPES.find(typeObject => typeObject.value === params.get('objectType'));
15✔
283
  const { endDate, startDate } = parseDateParams(params, today, tonight);
4✔
284
  return {
4✔
285
    detail: params.get('objectId'),
286
    endDate,
287
    startDate,
288
    type,
289
    user: params.get('userId')
290
  };
291
};
292

293
const formatActiveDeployments = (pageState, { defaults }) =>
184✔
294
  [DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.pending]
37✔
295
    .reduce((accu, state) => {
296
      const { page, perPage } = pageState[state] ?? {};
74!
297
      const stateDefaults = defaults[state] ?? {};
74!
298
      const items = Object.entries({ page, perPage })
74✔
299
        .reverse()
300
        .reduce((keyAccu, [key, value]) => {
301
          if ((value && value !== stateDefaults[key]) || keyAccu.length) {
148!
302
            keyAccu.unshift(value || stateDefaults[key]);
148✔
303
          }
304
          return keyAccu;
148✔
305
        }, []);
306
      if (items.length) {
74!
307
        accu.push(`${state}=${items.join(SEPARATOR)}`);
74✔
308
      }
309
      return accu;
74✔
310
    }, [])
311
    .filter(i => i)
74✔
312
    .join('&');
313

314
export const formatDeployments = ({ deploymentObject, pageState }, { defaults = {}, today, tonight }) => {
184!
315
  const { state: selectedState, showCreationDialog } = pageState.general;
48✔
316
  let params = new URLSearchParams();
48✔
317
  if (showCreationDialog) {
48✔
318
    params.set('open', true);
25✔
319
    if (deploymentObject.release) {
25✔
320
      params.set('release', deploymentObject.release.name);
6✔
321
    }
322
    if (deploymentObject.devices?.length) {
25!
UNCOV
323
      deploymentObject.devices.map(({ id }) => params.append('deviceId', id));
×
324
    }
325
  }
326
  let pageStateQuery;
327
  if (selectedState === DEPLOYMENT_ROUTES.finished.key) {
48✔
328
    const { endDate, search, startDate, type } = pageState[selectedState];
9✔
329
    params = formatDates({ endDate, params, startDate, today, tonight });
9✔
330
    params = Object.entries({ search, type }).reduce(paramReducer, params);
9✔
331
    pageStateQuery = formatPageState(pageState[selectedState], { defaults });
9✔
332
  } else if (selectedState === DEPLOYMENT_ROUTES.scheduled.key) {
39✔
333
    pageStateQuery = formatPageState(pageState[selectedState], { defaults });
2✔
334
  } else {
335
    pageStateQuery = formatActiveDeployments(pageState, { defaults });
37✔
336
  }
337
  return [pageStateQuery, params.toString()].filter(i => i).join('&');
96✔
338
};
339

340
const deploymentsPath = 'deployments/';
184✔
341
const parseDeploymentsPath = path => {
184✔
342
  const parts = path.split(deploymentsPath);
30✔
343
  if (parts.length > 1 && Object.keys(DEPLOYMENT_ROUTES).includes(parts[1])) {
30✔
344
    return parts[1];
21✔
345
  }
346
  return '';
9✔
347
};
348

349
const parseActiveDeployments = params =>
184✔
350
  [DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.pending].reduce((accu, state) => {
21✔
351
    if (!params.has(state)) {
42✔
352
      return accu;
14✔
353
    }
354
    const items = params.get(state).split(SEPARATOR);
28✔
355
    accu[state] = ['page', 'perPage'].reduce((stateAccu, key, index) => (items[index] ? { ...stateAccu, [key]: Number(items[index]) } : stateAccu), {});
56✔
356
    return accu;
28✔
357
  }, {});
358

359
const deploymentFields = {
184✔
360
  deviceId: { attribute: 'devices', parse: id => ({ id }), select: i => i },
1✔
361
  release: { attribute: 'release', parse: String, select: defaultSelector }
362
};
363

364
export const parseDeploymentsQuery = (params, { pageState, location, tonight }) => {
184✔
365
  // for deployments we get a startDate implicitly from a list of retrieved deployments if none is set, thus we're not defaulting to today
366
  const { endDate, startDate } = parseDateParams(params, '', tonight);
30✔
367
  const deploymentObject = Object.entries(deploymentFields).reduce(
30✔
368
    (accu, [key, { attribute, parse, select }]) => (params.has(key) ? { ...accu, [attribute]: select(params.getAll(key).map(parse)) } : accu),
60✔
369
    {}
370
  );
371
  const { state: selectedState, id, open, ...remainingPageState } = pageState;
30✔
372
  const tab = parseDeploymentsPath(location.pathname);
30✔
373
  const deploymentsTab = tab || selectedState || DEPLOYMENT_ROUTES.active.key;
30✔
374

375
  let state = {
30✔
376
    deploymentObject,
377
    general: {
378
      showCreationDialog: Boolean(open && !id),
37✔
379
      showReportDialog: Boolean(open && id),
37✔
380
      state: deploymentsTab
381
    }
382
  };
383
  if (deploymentsTab === DEPLOYMENT_ROUTES.finished.key) {
30✔
384
    const type = DEPLOYMENT_TYPES[params.get('type')] || '';
7✔
385
    const search = params.get('search') || '';
7✔
386
    state[deploymentsTab] = { ...remainingPageState, endDate, search, startDate, type };
7✔
387
  } else if (deploymentsTab === DEPLOYMENT_ROUTES.scheduled.key) {
23✔
388
    state[deploymentsTab] = { ...remainingPageState };
2✔
389
  } else {
390
    state = {
21✔
391
      ...state,
392
      ...parseActiveDeployments(params)
393
    };
394
  }
395
  return state;
30✔
396
};
397

398
export const generateDeploymentsPath = ({ pageState }) => {
184✔
399
  const { state: selectedState = DEPLOYMENT_ROUTES.active.key } = pageState.general;
48!
400
  return `/deployments/${selectedState}`;
48✔
401
};
402

403
const releasesRoot = '/releases';
184✔
404
export const formatReleases = ({ pageState: { searchTerm, selectedTags = [], tab, type } }) =>
184!
405
  Object.entries({ name: searchTerm, tab, type })
61✔
406
    .reduce(
407
      (accu, [key, value]) => (value ? [...accu, `${key}=${value}`] : accu),
183✔
408
      selectedTags.map(tag => `tag=${tag}`)
4✔
409
    )
410
    .join('&');
411

412
export const generateReleasesPath = ({ pageState: { selectedRelease } }) =>
184✔
413
  `${releasesRoot}${selectedRelease ? `/${encodeURIComponent(selectedRelease)}` : ''}`;
9✔
414

415
export const parseReleasesQuery = (queryParams, extraProps) => {
184✔
416
  const name = queryParams.has('name') ? queryParams.get('name') : '';
9✔
417
  const tab = queryParams.has('tab') ? queryParams.get('tab') : undefined;
9✔
418
  const tags = queryParams.has('tag') ? queryParams.getAll('tag') : [];
9✔
419
  const type = queryParams.has('type') ? queryParams.get('type') : '';
9!
420
  let selectedRelease = decodeURIComponent(extraProps.location.pathname.substring(releasesRoot.length + 1));
9✔
421
  if (!selectedRelease && extraProps.pageState.id?.length) {
9!
UNCOV
422
    selectedRelease = extraProps.pageState.id[0];
×
423
  }
424
  return { searchTerm: name, selectedRelease, tab, tags, type };
9✔
425
};
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