• 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

98.24
/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 = ':';
183✔
21

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

24
const commonFields = {
183✔
25
  ...Object.keys(DEVICE_LIST_DEFAULTS).reduce((accu, key) => ({ ...accu, [key]: { parse: Number, select: defaultSelector, target: key } }), {}),
366✔
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

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

39
export const commonProcessor = searchParams => {
183✔
40
  let params = new URLSearchParams(searchParams);
42✔
41
  const pageState = Object.entries(commonFields).reduce((accu, [key, { parse, select, target }]) => {
42✔
42
    const values = params.getAll(key);
210✔
43
    if (!values.length) {
210✔
44
      return accu;
198✔
45
    }
46
    if (!parse) {
12✔
47
      accu[target] = values;
1✔
48
    } else {
49
      try {
11✔
50
        accu[target] = select(values.map(parse));
11✔
51
      } catch (error) {
52
        console.log('encountered faulty url param, continue...', error);
×
53
      }
54
    }
55
    return accu;
12✔
56
  }, {});
57
  Object.keys(commonFields).map(key => params.delete(key));
210✔
58
  const sort = params.has('sort')
42✔
59
    ? params.getAll('sort').reduce((sortAccu, scopedQuery) => {
60
        const items = scopedQuery.split(SEPARATOR).reverse();
7✔
61
        return ['direction', 'key', 'scope'].reduce((accu, key, index) => {
7✔
62
          if (items[index]) {
21✔
63
            accu[key] = items[index];
11✔
64
          }
65
          return accu;
21✔
66
        }, sortAccu);
67
      }, {})
68
    : undefined;
69
  params.delete('sort');
42✔
70
  return { pageState, params, sort };
42✔
71
};
72

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

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

120
// filters, selectedGroup
121
export const parseDeviceQuery = (searchParams, extraProps = {}) => {
183✔
122
  let queryParams = new URLSearchParams(searchParams);
5✔
123
  const { filteringAttributes = {}, pageState = {} } = extraProps;
5✔
124
  const pageStateExtension = pageState.id?.length === 1 ? { open: true } : {};
5✔
125

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

138
  let groupName = '';
5✔
139
  const groupFilterIndex = scopedFilters.inventory.findIndex(filter => filter.key === 'group' && filter.operator === DEVICE_FILTERING_OPTIONS.$eq.key);
5✔
140
  if (groupFilterIndex > -1) {
5✔
141
    groupName = scopedFilters.inventory[groupFilterIndex].value;
2✔
142
    scopedFilters.inventory.splice(groupFilterIndex, 1);
2✔
143
  }
144

145
  const detailsTab = queryParams.has('tab') ? queryParams.get('tab') : '';
5!
146
  return { detailsTab, filters: Object.values(scopedFilters).flat(), groupName, ...pageStateExtension };
5✔
147
};
148

149
const formatSorting = (sort, { sort: sortDefault }) => {
183✔
150
  if (!sort || deepCompare(sort, sortDefault)) {
74✔
151
    return '';
64✔
152
  }
153
  const sortQuery = ['scope', 'key', 'direction']
10✔
154
    .reduce((accu, key) => {
155
      if (!sort[key]) {
30✔
156
        return accu;
12✔
157
      }
158
      accu.push(sort[key]);
18✔
159
      return accu;
18✔
160
    }, [])
161
    .join(SEPARATOR);
162
  return `sort=${sortQuery}`;
10✔
163
};
164

165
export const formatPageState = ({ selectedId, selectedIssues, page, perPage, sort }, { defaults }) =>
183✔
166
  Object.entries({ page, perPage, id: selectedId, issues: selectedIssues, open: selectedId ? true : undefined })
74✔
167
    .reduce(
168
      (accu, [key, value]) => {
169
        if (Array.isArray(value)) {
370✔
170
          accu.push(...value.map(item => `${key}=${encodeURIComponent(item)}`));
2✔
171
        } else if ((DEVICE_LIST_DEFAULTS[key] != value || !DEVICE_LIST_DEFAULTS.hasOwnProperty(key)) && value) {
369✔
172
          accu.push(`${key}=${encodeURIComponent(value)}`);
8✔
173
        }
174
        return accu;
370✔
175
      },
176
      [formatSorting(sort, defaults)]
177
    )
178
    .filter(i => i)
84✔
179
    .join('&');
180

181
const stripFilterOperator = operator => operator.replaceAll('$', '');
183✔
182

183
const formatFilters = filters => {
183✔
184
  const result = filters
65✔
185
    // group all filters by their scope to get a more organized result
186
    .reduce(
187
      (accu, filter) => {
188
        const { scope = ATTRIBUTE_SCOPES.inventory, operator = '$eq' } = filter;
9✔
189
        accu[scope].add(`${scopes[scope].delimiter}=${filter.key}${SEPARATOR}${stripFilterOperator(operator)}${SEPARATOR}${encodeURIComponent(filter.value)}`);
9✔
190
        return accu;
9✔
191
      },
192
      Object.keys(scopes).reduce((accu, item) => ({ ...accu, [item]: new Set() }), {})
325✔
193
    );
194
  // boil it all down to a single line containing all filters
195
  return Object.values(result)
65✔
196
    .map(filterSet => [...filterSet])
325✔
197
    .flat();
198
};
199

200
export const formatDeviceSearch = ({ pageState, filters, selectedGroup }) => {
183✔
201
  let activeFilters = [...filters];
65✔
202
  if (selectedGroup && selectedGroup !== ALL_DEVICES) {
65✔
203
    const isUngroupedGroup = selectedGroup === UNGROUPED_GROUP.id;
5✔
204
    activeFilters = isUngroupedGroup
5✔
205
      ? activeFilters.filter(
206
          filter => !(filter.key === 'group' && filter.scope === ATTRIBUTE_SCOPES.system && filter.operator === DEVICE_FILTERING_OPTIONS.$nin.key)
1!
207
        )
208
      : activeFilters;
209
    const groupName = isUngroupedGroup ? UNGROUPED_GROUP.name : selectedGroup;
5✔
210
    activeFilters.push({ scope: ATTRIBUTE_SCOPES.inventory, key: 'group', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: groupName });
5✔
211
  }
212
  const formattedFilters = formatFilters(activeFilters).filter(i => i);
65✔
213
  if (pageState.detailsTab && pageState.selectedId) {
65!
214
    formattedFilters.push(`tab=${pageState.detailsTab}`);
×
215
  }
216
  return formattedFilters.join('&');
65✔
217
};
218

219
export const generateDevicePath = ({ pageState }) => {
183✔
220
  const { state: selectedState } = pageState;
2✔
221
  const path = ['/devices'];
2✔
222
  if (![routes.allDevices.key, ''].includes(selectedState)) {
2!
223
    path.push(selectedState);
2✔
224
  }
225
  return path.join('/');
2✔
226
};
227

228
const formatDates = ({ endDate, params, startDate, today, tonight }) => {
183✔
229
  if (endDate && endDate !== tonight) {
21✔
230
    params.set('endDate', endDate.split('T')[0]);
2✔
231
  }
232
  if (startDate && startDate !== today) {
21✔
233
    params.set('startDate', startDate.split('T')[0]);
14✔
234
  }
235
  return params;
21✔
236
};
237

238
const paramReducer = (accu, [key, value]) => {
183✔
239
  if (value) {
42✔
240
    accu.set(key, value);
11✔
241
  }
242
  return accu;
42✔
243
};
244

245
export const formatAuditlogs = ({ pageState }, { today, tonight }) => {
183✔
246
  const { detail, endDate, startDate, type, user } = pageState;
8✔
247
  let params = new URLSearchParams();
8✔
248
  params = Object.entries({ objectId: detail, userId: user ? user.id ?? user : user }).reduce(paramReducer, params);
8✔
249
  if (type) {
8✔
250
    params.set('objectType', type.value ?? type);
7✔
251
  }
252
  params = formatDates({ endDate, params, startDate, today, tonight });
8✔
253
  return params.toString();
8✔
254
};
255

256
const parseDateParams = (params, today, tonight) => {
183✔
257
  let endDate = tonight;
38✔
258
  if (params.get('endDate')) {
38✔
259
    endDate = getISOStringBoundaries(new Date(params.get('endDate'))).end;
2✔
260
  }
261
  let startDate = today;
38✔
262
  if (params.get('startDate')) {
38✔
263
    startDate = getISOStringBoundaries(new Date(params.get('startDate'))).start;
7✔
264
  }
265
  return { endDate, startDate };
38✔
266
};
267

268
export const parseAuditlogsQuery = (params, { today, tonight }) => {
183✔
269
  const type = AUDIT_LOGS_TYPES.find(typeObject => typeObject.value === params.get('objectType'));
19✔
270
  const { endDate, startDate } = parseDateParams(params, today, tonight);
5✔
271
  return {
5✔
272
    detail: params.get('objectId'),
273
    endDate,
274
    startDate,
275
    type,
276
    user: params.get('userId')
277
  };
278
};
279

280
const formatActiveDeployments = (pageState, { defaults }) =>
183✔
281
  [DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.pending]
37✔
282
    .reduce((accu, state) => {
283
      const { page, perPage } = pageState[state] ?? {};
74!
284
      const stateDefaults = defaults[state] ?? {};
74!
285
      const items = Object.entries({ page, perPage })
74✔
286
        .reverse()
287
        .reduce((keyAccu, [key, value]) => {
288
          if ((value && value !== stateDefaults[key]) || keyAccu.length) {
148!
289
            keyAccu.unshift(value || stateDefaults[key]);
148✔
290
          }
291
          return keyAccu;
148✔
292
        }, []);
293
      if (items.length) {
74!
294
        accu.push(`${state}=${items.join(SEPARATOR)}`);
74✔
295
      }
296
      return accu;
74✔
297
    }, [])
298
    .filter(i => i)
74✔
299
    .join('&');
300

301
export const formatDeployments = ({ deploymentObject, pageState }, { defaults, today, tonight }) => {
183✔
302
  const { state: selectedState, showCreationDialog } = pageState.general;
52✔
303
  let params = new URLSearchParams();
52✔
304
  if (showCreationDialog) {
52✔
305
    params.set('open', true);
32✔
306
    if (deploymentObject.release) {
32✔
307
      params.set('release', deploymentObject.release.name);
16✔
308
    }
309
    if (deploymentObject.devices?.length) {
32!
310
      deploymentObject.devices.map(({ id }) => params.append('deviceId', id));
×
311
    }
312
  }
313
  let pageStateQuery;
314
  if (selectedState === DEPLOYMENT_ROUTES.finished.key) {
52✔
315
    const { endDate, search, startDate, type } = pageState[selectedState];
13✔
316
    params = formatDates({ endDate, params, startDate, today, tonight });
13✔
317
    params = Object.entries({ search, type }).reduce(paramReducer, params);
13✔
318
    pageStateQuery = formatPageState(pageState[selectedState], { defaults });
13✔
319
  } else if (selectedState === DEPLOYMENT_ROUTES.scheduled.key) {
39✔
320
    pageStateQuery = formatPageState(pageState[selectedState], { defaults });
2✔
321
  } else {
322
    pageStateQuery = formatActiveDeployments(pageState, { defaults });
37✔
323
  }
324
  return [pageStateQuery, params.toString()].filter(i => i).join('&');
104✔
325
};
326

327
const deploymentsPath = 'deployments/';
183✔
328
const parseDeploymentsPath = path => {
183✔
329
  const parts = path.split(deploymentsPath);
33✔
330
  if (parts.length > 1 && Object.keys(DEPLOYMENT_ROUTES).includes(parts[1])) {
33✔
331
    return parts[1];
23✔
332
  }
333
  return '';
10✔
334
};
335

336
const parseActiveDeployments = params =>
183✔
337
  [DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.pending].reduce((accu, state) => {
23✔
338
    if (!params.has(state)) {
46✔
339
      return accu;
16✔
340
    }
341
    const items = params.get(state).split(SEPARATOR);
30✔
342
    accu[state] = ['page', 'perPage'].reduce((stateAccu, key, index) => (items[index] ? { ...stateAccu, [key]: Number(items[index]) } : stateAccu), {});
60✔
343
    return accu;
30✔
344
  }, {});
345

346
const deploymentFields = {
183✔
347
  deviceId: { attribute: 'devices', parse: id => ({ id }), select: i => i },
1✔
348
  release: { attribute: 'release', parse: String, select: defaultSelector }
349
};
350

351
export const parseDeploymentsQuery = (params, { pageState, location, tonight }) => {
183✔
352
  // for deployments we get a startDate implicitly from a list of retrieved deployments if none is set, thus we're not defaulting to today
353
  const { endDate, startDate } = parseDateParams(params, '', tonight);
33✔
354
  const deploymentObject = Object.entries(deploymentFields).reduce(
33✔
355
    (accu, [key, { attribute, parse, select }]) => (params.has(key) ? { ...accu, [attribute]: select(params.getAll(key).map(parse)) } : accu),
66✔
356
    {}
357
  );
358
  const { state: selectedState, id, open, ...remainingPageState } = pageState;
33✔
359
  const tab = parseDeploymentsPath(location.pathname);
33✔
360
  const deploymentsTab = tab || selectedState || DEPLOYMENT_ROUTES.active.key;
33✔
361

362
  let state = {
33✔
363
    deploymentObject,
364
    general: {
365
      showCreationDialog: Boolean(open && !id),
40✔
366
      showReportDialog: Boolean(open && id),
40✔
367
      state: deploymentsTab
368
    }
369
  };
370
  if (deploymentsTab === DEPLOYMENT_ROUTES.finished.key) {
33✔
371
    const type = DEPLOYMENT_TYPES[params.get('type')] || '';
8✔
372
    const search = params.get('search') || '';
8✔
373
    state[deploymentsTab] = { ...remainingPageState, endDate, search, startDate, type };
8✔
374
  } else if (deploymentsTab === DEPLOYMENT_ROUTES.scheduled.key) {
25✔
375
    state[deploymentsTab] = { ...remainingPageState };
2✔
376
  } else {
377
    state = {
23✔
378
      ...state,
379
      ...parseActiveDeployments(params)
380
    };
381
  }
382
  return state;
33✔
383
};
384

385
export const generateDeploymentsPath = ({ pageState }) => {
183✔
386
  const { state: selectedState = DEPLOYMENT_ROUTES.active.key } = pageState.general;
52!
387
  return `/deployments/${selectedState}`;
52✔
388
};
389

390
const releasesRoot = '/releases';
183✔
391
export const formatReleases = ({ pageState: { searchTerm, selectedTags = [], tab, type } }) =>
183!
392
  Object.entries({ name: searchTerm, tab, type })
40✔
393
    .reduce(
394
      (accu, [key, value]) => (value ? [...accu, `${key}=${value}`] : accu),
120✔
395
      selectedTags.map(tag => `tag=${tag}`)
4✔
396
    )
397
    .join('&');
398

399
export const generateReleasesPath = ({ pageState: { selectedRelease } }) => `${releasesRoot}${selectedRelease ? `/${selectedRelease}` : ''}`;
183✔
400

401
export const parseReleasesQuery = (queryParams, extraProps) => {
183✔
402
  const name = queryParams.has('name') ? queryParams.get('name') : '';
10!
403
  const tab = queryParams.has('tab') ? queryParams.get('tab') : undefined;
10✔
404
  const tags = queryParams.has('tag') ? queryParams.getAll('tag') : [];
10✔
405
  const type = queryParams.has('type') ? queryParams.get('type') : '';
10!
406
  let selectedRelease = extraProps.location.pathname.substring(releasesRoot.length + 1);
10✔
407
  if (!selectedRelease && extraProps.pageState.id?.length) {
10!
408
    selectedRelease = extraProps.pageState.id[0];
×
409
  }
410
  return { searchTerm: name, selectedRelease, tab, tags, type };
10✔
411
};
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