• 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

92.25
/src/js/helpers.js
1
// Copyright 2017 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 React from 'react';
15

16
import pluralize from 'pluralize';
17
import Cookies from 'universal-cookie';
18

19
import { DARK_MODE, SORTING_OPTIONS } from './constants/appConstants';
20
import {
21
  DEPLOYMENT_STATES,
22
  defaultStats,
23
  deploymentDisplayStates,
24
  deploymentStatesToSubstates,
25
  deploymentStatesToSubstatesWithSkipped
26
} from './constants/deploymentConstants';
27
import { ATTRIBUTE_SCOPES, DEVICE_FILTERING_OPTIONS } from './constants/deviceConstants';
28

29
const isEncoded = uri => {
184✔
30
  uri = uri || '';
8✔
31
  return uri !== decodeURIComponent(uri);
8✔
32
};
33

34
export const fullyDecodeURI = uri => {
184✔
35
  while (isEncoded(uri)) {
6✔
36
    uri = decodeURIComponent(uri);
2✔
37
  }
38
  return uri;
6✔
39
};
40

41
export const groupDeploymentDevicesStats = deployment => {
184✔
42
  const deviceStatCollector = (deploymentStates, devices) =>
1✔
43
    Object.values(devices).reduce((accu, device) => (deploymentStates.includes(device.status) ? accu + 1 : accu), 0);
50✔
44

45
  const inprogress = deviceStatCollector(deploymentStatesToSubstates.inprogress, deployment.devices);
1✔
46
  const pending = deviceStatCollector(deploymentStatesToSubstates.pending, deployment.devices);
1✔
47
  const successes = deviceStatCollector(deploymentStatesToSubstates.successes, deployment.devices);
1✔
48
  const failures = deviceStatCollector(deploymentStatesToSubstates.failures, deployment.devices);
1✔
49
  const paused = deviceStatCollector(deploymentStatesToSubstates.paused, deployment.devices);
1✔
50
  return { inprogress, paused, pending, successes, failures };
1✔
51
};
52

53
export const statCollector = (items, statistics) => items.reduce((accu, property) => accu + Number(statistics[property] || 0), 0);
7,729✔
54
export const groupDeploymentStats = (deployment, withSkipped) => {
184✔
55
  const { statistics = {} } = deployment;
589✔
56
  const { status = {} } = statistics;
589✔
57
  const stats = { ...defaultStats, ...status };
589✔
58
  let groupStates = deploymentStatesToSubstates;
589✔
59
  let result = {};
589✔
60
  if (withSkipped) {
589✔
61
    groupStates = deploymentStatesToSubstatesWithSkipped;
255✔
62
    result.skipped = statCollector(groupStates.skipped, stats);
255✔
63
  }
64
  result = {
589✔
65
    ...result,
66
    // don't include 'pending' as inprogress, as all remaining devices will be pending - we don't discriminate based on phase membership
67
    inprogress: statCollector(groupStates.inprogress, stats),
68
    pending: (deployment.max_devices ? deployment.max_devices - deployment.device_count : 0) + statCollector(groupStates.pending, stats),
589✔
69
    successes: statCollector(groupStates.successes, stats),
70
    failures: statCollector(groupStates.failures, stats),
71
    paused: statCollector(groupStates.paused, stats)
72
  };
73
  return result;
589✔
74
};
75

76
export const getDeploymentState = deployment => {
184✔
77
  const { status: deploymentStatus = DEPLOYMENT_STATES.pending } = deployment;
317✔
78
  const { inprogress: currentProgressCount, paused } = groupDeploymentStats(deployment);
317✔
79

80
  let status = deploymentDisplayStates[deploymentStatus];
317✔
81
  if (deploymentStatus === DEPLOYMENT_STATES.pending && currentProgressCount === 0) {
317✔
82
    status = 'queued';
153✔
83
  } else if (paused > 0) {
164!
UNCOV
84
    status = deploymentDisplayStates.paused;
×
85
  }
86
  return status;
317✔
87
};
88

89
export const isEmpty = obj => {
184✔
90
  for (const _ in obj) {
462✔
91
    return false;
203✔
92
  }
93
  return true;
259✔
94
};
95

96
export const extractErrorMessage = (err, fallback = '') =>
184✔
97
  err.response?.data?.error?.message || err.response?.data?.error || err.error || err.message || fallback;
9!
98

99
export const preformatWithRequestID = (res, failMsg) => {
184✔
100
  // ellipsis line
101
  if (failMsg.length > 100) failMsg = `${failMsg.substring(0, 220)}...`;
11✔
102

103
  try {
11✔
104
    if (res?.data && Object.keys(res.data).includes('request_id')) {
11✔
105
      let shortRequestUUID = res.data['request_id'].substring(0, 8);
2✔
106
      return `${failMsg} [Request ID: ${shortRequestUUID}]`;
2✔
107
    }
108
  } catch (e) {
UNCOV
109
    console.log('failed to extract request id:', e);
×
110
  }
111
  return failMsg;
9✔
112
};
113

114
export const versionCompare = (v1, v2) => {
184✔
115
  const partsV1 = `${v1}`.split('.');
19✔
116
  const partsV2 = `${v2}`.split('.');
19✔
117
  for (let index = 0; index < partsV1.length; index++) {
19✔
118
    const numberV1 = partsV1[index];
24✔
119
    const numberV2 = partsV2[index];
24✔
120
    if (numberV1 > numberV2) {
24✔
121
      return 1;
14✔
122
    }
123
    if (numberV2 > numberV1) {
10✔
124
      return -1;
4✔
125
    }
126
  }
127
  return 0;
1✔
128
};
129

130
/*
131
 *
132
 * Deep compare
133
 *
134
 */
135
// eslint-disable-next-line sonarjs/cognitive-complexity
136
export function deepCompare() {
137
  var i, l, leftChain, rightChain;
138

139
  // eslint-disable-next-line sonarjs/cognitive-complexity
140
  function compare2Objects(x, y) {
141
    var p;
142

143
    // remember that NaN === NaN returns false
144
    // and isNaN(undefined) returns true
145
    if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
695!
UNCOV
146
      return true;
×
147
    }
148

149
    // Compare primitives and functions.
150
    // Check if both arguments link to the same object.
151
    // Especially useful on the step where we compare prototypes
152
    if (x === y) {
695✔
153
      return true;
343✔
154
    }
155

156
    // Works in case when functions are created in constructor.
157
    // Comparing dates is a common scenario. Another built-ins?
158
    // We can even handle functions passed across iframes
159
    if (
352✔
160
      (typeof x === 'function' && typeof y === 'function') ||
1,752!
161
      (x instanceof Date && y instanceof Date) ||
162
      (x instanceof RegExp && y instanceof RegExp) ||
163
      (x instanceof String && y instanceof String) ||
164
      (x instanceof Number && y instanceof Number)
165
    ) {
166
      return x.toString() === y.toString();
3✔
167
    }
168

169
    // At last checking prototypes as good as we can
170
    if (!(x instanceof Object && y instanceof Object)) {
349✔
171
      return false;
4✔
172
    }
173

174
    if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
345!
UNCOV
175
      return false;
×
176
    }
177

178
    if (x.constructor !== y.constructor) {
345!
UNCOV
179
      return false;
×
180
    }
181

182
    if (x.prototype !== y.prototype) {
345!
UNCOV
183
      return false;
×
184
    }
185

186
    // Check for infinitive linking loops
187
    if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
345!
UNCOV
188
      return false;
×
189
    }
190

191
    // Quick checking of one object being a subset of another.
192
    // todo: cache the structure of arguments[0] for performance
193
    for (p in y) {
345✔
194
      if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || typeof y[p] !== typeof x[p]) {
1,617✔
195
        return false;
72✔
196
      }
197
    }
198

199
    for (p in x) {
273✔
200
      if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || typeof y[p] !== typeof x[p]) {
1,110✔
201
        return false;
8✔
202
      }
203

204
      switch (typeof x[p]) {
1,102✔
205
        case 'object':
206
        case 'function':
207
          leftChain.push(x);
422✔
208
          rightChain.push(y);
422✔
209

210
          if (!compare2Objects(x[p], y[p])) {
422✔
211
            return false;
12✔
212
          }
213

214
          leftChain.pop();
410✔
215
          rightChain.pop();
410✔
216
          break;
410✔
217

218
        default:
219
          if (x[p] !== y[p]) {
680✔
220
            return false;
16✔
221
          }
222
          break;
664✔
223
      }
224
    }
225

226
    return true;
237✔
227
  }
228

229
  if (arguments.length < 1) {
271!
UNCOV
230
    return true; //Die silently? Don't know how to handle such case, please help...
×
231
    // throw "Need two or more arguments to compare";
232
  }
233

234
  for (i = 1, l = arguments.length; i < l; i++) {
271✔
235
    leftChain = []; //Todo: this can be cached
273✔
236
    rightChain = [];
273✔
237

238
    if (!compare2Objects(arguments[0], arguments[i])) {
273✔
239
      return false;
100✔
240
    }
241
  }
242

243
  return true;
171✔
244
}
245

246
export const stringToBoolean = content => {
184✔
247
  if (!content) {
297✔
248
    return false;
267✔
249
  }
250
  const string = content + '';
30✔
251
  switch (string.trim().toLowerCase()) {
30✔
252
    case 'true':
253
    case 'yes':
254
    case '1':
255
      return true;
26✔
256
    case 'false':
257
    case 'no':
258
    case '0':
259
    case null:
260
      return false;
3✔
261
    default:
262
      return Boolean(string);
1✔
263
  }
264
};
265

266
export const toggle = current => !current;
184✔
267

268
export const formatTime = date => {
184✔
269
  if (date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date)) {
57✔
270
    return date.toISOString().slice(0, -1);
1✔
271
  } else if (date) {
56✔
272
    return date.replace(' ', 'T').replace(/ /g, '').replace('UTC', '');
46✔
273
  }
274
};
275

276
export const customSort = (direction, field) => (a, b) => {
1,017✔
277
  if (typeof a[field] === 'string') {
248,122✔
278
    const result = a[field].localeCompare(b[field], { sensitivity: 'case' });
248,056✔
279
    return direction ? result * -1 : result;
248,056✔
280
  }
281
  if (a[field] > b[field]) return direction ? -1 : 1;
66!
282
  if (a[field] < b[field]) return direction ? 1 : -1;
56!
283
  return 0;
19✔
284
};
285

286
export const duplicateFilter = (item, index, array) => array.indexOf(item) == index;
5,562✔
287

288
export const attributeDuplicateFilter = (filterableArray, attributeName = 'key') =>
184!
289
  filterableArray.filter(
5✔
290
    (item, index, array) => array.findIndex(filter => filter[attributeName] === item[attributeName] && filter.scope === item.scope) == index
266✔
291
  );
292

293
export const unionizeStrings = (someStrings, someOtherStrings) => {
184✔
294
  const startingPoint = new Set(someStrings.filter(item => item.length));
24✔
295
  const uniqueStrings = someOtherStrings.length
24✔
296
    ? someOtherStrings.reduce((accu, item) => {
297
        if (item.trim().length) {
33✔
298
          accu.add(item.trim());
14✔
299
        }
300
        return accu;
33✔
301
      }, startingPoint)
302
    : startingPoint;
303
  return [...uniqueStrings];
24✔
304
};
305

306
export const generateDeploymentGroupDetails = (filter, groupName) =>
184✔
307
  filter && filter.terms?.length
3✔
308
    ? `${groupName} (${filter.terms
309
        .map(filter => `${filter.attribute || filter.key} ${DEVICE_FILTERING_OPTIONS[filter.type || filter.operator].shortform} ${filter.value}`)
4✔
310
        .join(', ')})`
311
    : groupName;
312

313
export const mapDeviceAttributes = (attributes = []) =>
184✔
314
  attributes.reduce(
279✔
315
    (accu, attribute) => {
316
      if (!(attribute.value && attribute.name) && attribute.scope === ATTRIBUTE_SCOPES.inventory) {
3,195!
UNCOV
317
        return accu;
×
318
      }
319
      accu[attribute.scope || ATTRIBUTE_SCOPES.inventory] = {
3,195✔
320
        ...accu[attribute.scope || ATTRIBUTE_SCOPES.inventory],
3,201✔
321
        [attribute.name]: attribute.value
322
      };
323
      if (attribute.name === 'device_type' && attribute.scope === ATTRIBUTE_SCOPES.inventory) {
3,195✔
324
        accu.inventory.device_type = [].concat(attribute.value);
154✔
325
      }
326
      return accu;
3,195✔
327
    },
328
    { inventory: { device_type: [], artifact_name: '' }, identity: {}, monitor: {}, system: {}, tags: {} }
329
  );
330

331
export const getFormattedSize = bytes => {
184✔
332
  const suffixes = ['Bytes', 'KB', 'MB', 'GB'];
125✔
333
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
125✔
334
  if (!bytes) {
125✔
335
    return '0 Bytes';
13✔
336
  }
337
  return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${suffixes[i]}`;
112✔
338
};
339

340
export const FileSize = React.forwardRef(({ fileSize, style }, ref) => (
184✔
341
  <div ref={ref} style={style}>
116✔
342
    {getFormattedSize(fileSize)}
343
  </div>
344
));
345
FileSize.displayName = 'FileSize';
184✔
346

347
const collectAddressesFrom = devices =>
184✔
348
  devices.reduce((collector, { attributes = {} }) => {
22!
349
    const ips = Object.entries(attributes).reduce((accu, [name, value]) => {
63✔
350
      if (name.startsWith('ipv4')) {
263✔
351
        if (Array.isArray(value)) {
43!
UNCOV
352
          const texts = value.map(text => text.slice(0, text.indexOf('/')));
×
UNCOV
353
          accu.push(...texts);
×
354
        } else {
355
          const text = value.slice(0, value.indexOf('/'));
43✔
356
          accu.push(text);
43✔
357
        }
358
      }
359
      return accu;
263✔
360
    }, []);
361
    collector.push(...ips);
63✔
362
    return collector;
63✔
363
  }, []);
364

365
export const getDemoDeviceAddress = (devices, onboardingApproach) => {
184✔
366
  const defaultVitualizedIp = '10.0.2.15';
22✔
367
  const addresses = collectAddressesFrom(devices);
22✔
368
  const address = addresses.reduce((accu, item) => {
22✔
369
    if (accu && item === defaultVitualizedIp) {
43!
UNCOV
370
      return accu;
×
371
    }
372
    return item;
43✔
373
  }, null);
374
  if (!address || (onboardingApproach === 'virtual' && (navigator.appVersion.indexOf('Win') != -1 || navigator.appVersion.indexOf('Mac') != -1))) {
22✔
375
    return 'localhost';
1✔
376
  }
377
  return address;
21✔
378
};
379

380
export const detectOsIdentifier = () => {
184✔
381
  if (navigator.appVersion.indexOf('Win') != -1) return 'Windows';
3!
382
  if (navigator.appVersion.indexOf('Mac') != -1) return 'MacOs';
3✔
383
  if (navigator.appVersion.indexOf('X11') != -1) return 'Unix';
2!
384
  return 'Linux';
2✔
385
};
386

387
export const getRemainderPercent = phases => {
184✔
388
  // remove final phase size if set
389
  phases[phases.length - 1].batch_size = null;
51✔
390
  // use this to get remaining percent of final phase so we don't set a hard number
391
  return phases.reduce((accu, phase) => (phase.batch_size ? accu - phase.batch_size : accu), 100);
140✔
392
};
393

394
export const validatePhases = (phases, deploymentDeviceCount, hasFilter) => {
184✔
395
  if (!phases?.length) {
22✔
396
    return true;
6✔
397
  }
398
  const remainder = getRemainderPercent(phases);
16✔
399
  return phases.reduce((accu, phase) => {
16✔
400
    if (!accu) {
48✔
401
      return accu;
4✔
402
    }
403
    const deviceCount = Math.floor((deploymentDeviceCount / 100) * (phase.batch_size || remainder));
44✔
404
    return deviceCount >= 1 || hasFilter;
44✔
405
  }, true);
406
};
407

408
export const getPhaseDeviceCount = (numberDevices = 1, batchSize, remainder, isLastPhase) =>
184✔
409
  isLastPhase ? Math.ceil((numberDevices / 100) * (batchSize || remainder)) : Math.floor((numberDevices / 100) * (batchSize || remainder));
84✔
410

411
export const startTimeSort = (a, b) => (b.created > a.created) - (b.created < a.created);
486✔
412

413
export const standardizePhases = phases =>
184✔
414
  phases.map((phase, index) => {
3✔
415
    let standardizedPhase = { batch_size: phase.batch_size, start_ts: index };
8✔
416
    if (phase.delay) {
8✔
417
      standardizedPhase.delay = phase.delay;
5✔
418
      standardizedPhase.delayUnit = phase.delayUnit || 'hours';
5✔
419
    }
420
    if (index === 0) {
8✔
421
      // delete the start timestamp from a deployment pattern, to default to starting without delay
422
      delete standardizedPhase.start_ts;
3✔
423
    }
424
    return standardizedPhase;
8✔
425
  });
426

427
const getInstallScriptArgs = ({ isHosted, isPreRelease }) => {
184✔
428
  let installScriptArgs = '--demo';
11✔
429
  installScriptArgs = isPreRelease ? `${installScriptArgs} -c experimental` : installScriptArgs;
11✔
430
  installScriptArgs = isHosted ? `${installScriptArgs} --commercial --jwt-token $JWT_TOKEN` : installScriptArgs;
11✔
431
  return installScriptArgs;
11✔
432
};
433

434
const getSetupArgs = ({ deviceType = 'generic-armv6', ipAddress, isDemoMode, tenantToken, isOnboarding }) => {
184✔
435
  let menderSetupArgs = `--quiet --device-type "${deviceType}"`;
11✔
436
  menderSetupArgs = tenantToken ? `${menderSetupArgs} --tenant-token $TENANT_TOKEN` : menderSetupArgs;
11✔
437
  // in production we use polling intervals from the client examples: https://github.com/mendersoftware/mender/blob/master/examples/mender.conf.production
438
  menderSetupArgs = isDemoMode || isOnboarding ? `${menderSetupArgs} --demo` : `${menderSetupArgs} --retry-poll 300 --update-poll 1800 --inventory-poll 28800`;
11✔
439
  if (isDemoMode) {
11✔
440
    // Demo installation, either OS os Enterprise. Install demo cert and add IP to /etc/hosts
441
    menderSetupArgs = `${menderSetupArgs}${ipAddress ? ` --server-ip ${ipAddress}` : ''}`;
6!
442
  } else {
443
    // Production installation, either OS, HM, or Enterprise
444
    menderSetupArgs = `${menderSetupArgs} --server-url https://${window.location.hostname} --server-cert=""`;
5✔
445
  }
446
  return menderSetupArgs;
11✔
447
};
448

449
const installComponents = '--force-mender-client4';
184✔
450

451
export const getDebConfigurationCode = props => {
184✔
452
  const { tenantToken, token, isPreRelease } = props;
11✔
453
  const envVars = tenantToken ? `JWT_TOKEN="${token}"\nTENANT_TOKEN="${tenantToken}"\n` : '';
11✔
454
  const installScriptArgs = getInstallScriptArgs(props);
11✔
455
  const scriptUrl = isPreRelease ? 'https://get.mender.io/staging' : 'https://get.mender.io';
11✔
456
  const menderSetupArgs = getSetupArgs(props);
11✔
457
  return `${envVars}wget -O- ${scriptUrl} | sudo bash -s -- ${installScriptArgs} ${installComponents} -- ${menderSetupArgs}`;
11✔
458
};
459

460
export const getSnackbarMessage = (skipped, done) => {
184✔
461
  pluralize.addIrregularRule('its', 'their');
1✔
462
  const skipText = skipped
1!
463
    ? `${skipped} ${pluralize('devices', skipped)} ${pluralize('have', skipped)} more than one pending authset. Expand ${pluralize(
464
        'this',
465
        skipped
466
      )} ${pluralize('device', skipped)} to individually adjust ${pluralize('their', skipped)} authorization status. `
467
    : '';
468
  const doneText = done ? `${done} ${pluralize('device', done)} ${pluralize('was', done)} updated successfully. ` : '';
1!
469
  return `${doneText}${skipText}`;
1✔
470
};
471

472
export const extractSoftware = (attributes = {}) => {
184✔
473
  const softwareKeys = Object.keys(attributes).reduce((accu, item) => {
40✔
474
    if (item.endsWith('.version')) {
100✔
475
      accu.push(item.substring(0, item.lastIndexOf('.')));
51✔
476
    }
477
    return accu;
100✔
478
  }, []);
479
  return Object.entries(attributes).reduce(
40✔
480
    (accu, item) => {
481
      if (softwareKeys.some(key => item[0].startsWith(key))) {
210✔
482
        accu.software.push(item);
66✔
483
      } else {
484
        accu.nonSoftware.push(item);
34✔
485
      }
486
      return accu;
100✔
487
    },
488
    { software: [], nonSoftware: [] }
489
  );
490
};
491

492
export const extractSoftwareItem = (artifactProvides = {}) => {
184✔
493
  const { software } = extractSoftware(artifactProvides);
12✔
494
  return (
12✔
495
    software
496
      .reduce((accu, item) => {
497
        const infoItems = item[0].split('.');
10✔
498
        if (infoItems[infoItems.length - 1] !== 'version') {
10!
UNCOV
499
          return accu;
×
500
        }
501
        accu.push({ key: infoItems[0], name: infoItems.slice(1, infoItems.length - 1).join('.'), version: item[1], nestingLevel: infoItems.length });
10✔
502
        return accu;
10✔
503
      }, [])
504
      // we assume the smaller the nesting level in the software name, the closer the software is to the rootfs/ the higher the chances we show the rootfs
505
      // sort based on this assumption & then only return the first item (can't use index access, since there might not be any software item at all)
UNCOV
506
      .sort((a, b) => a.nestingLevel - b.nestingLevel)
×
507
      .reduce((accu, item) => accu ?? item, undefined)
10✔
508
  );
509
};
510

511
const cookies = new Cookies();
184✔
512

513
export const createDownload = (target, filename, token) => {
184✔
514
  let link = document.createElement('a');
1✔
515
  link.setAttribute('href', target);
1✔
516
  link.setAttribute('download', filename);
1✔
517
  link.style.display = 'none';
1✔
518
  document.body.appendChild(link);
1✔
519
  cookies.set('JWT', token, { path: '/', secure: true, sameSite: 'strict', maxAge: 5 });
1✔
520
  link.click();
1✔
521
  document.body.removeChild(link);
1✔
522
};
523

524
export const createFileDownload = (content, filename, token) => createDownload('data:text/plain;charset=utf-8,' + encodeURIComponent(content), filename, token);
184✔
525

526
export const getISOStringBoundaries = currentDate => {
184✔
527
  const date = [currentDate.getUTCFullYear(), `0${currentDate.getUTCMonth() + 1}`.slice(-2), `0${currentDate.getUTCDate()}`.slice(-2)].join('-');
171✔
528
  return { start: `${date}T00:00:00.000`, end: `${date}T23:59:59.999` };
171✔
529
};
530

531
export const isDarkMode = mode => mode === DARK_MODE;
184✔
532

533
export const combineSortCriteria = (currentCriteria = [], newCriteria = []) =>
184!
534
  newCriteria.reduce(
40✔
535
    (accu, sort) => {
536
      const existingSortIndex = accu.findIndex(({ scope, key }) => scope === sort.scope && key === sort.key);
12!
537
      if (existingSortIndex > -1) {
12!
NEW
538
        accu.splice(existingSortIndex, 1);
×
539
      }
540
      if (sort.disabled) {
12!
541
        return accu;
12✔
542
      }
NEW
543
      accu.push(sort);
×
NEW
544
      return accu;
×
545
    },
NEW
546
    [...currentCriteria.filter(({ disabled }) => !disabled)]
×
547
  );
548

549
export const sortCriteriaToSortOptions = criteria =>
184✔
550
  criteria.reduce((accu, sort) => {
24✔
NEW
551
    const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol, scope: sortScope } = sort;
×
NEW
552
    if (sortCol) {
×
NEW
553
      accu.push({ attribute: sortCol, order: sortDown, scope: sortScope });
×
554
    }
NEW
555
    return accu;
×
556
  }, []);
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