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

mendersoftware / gui / 1057188406

01 Nov 2023 04:24AM UTC coverage: 82.824% (-17.1%) from 99.964%
1057188406

Pull #4134

gitlab-ci

web-flow
chore: Bump uuid from 9.0.0 to 9.0.1

Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4134: chore: Bump uuid from 9.0.0 to 9.0.1

4349 of 6284 branches covered (0.0%)

8313 of 10037 relevant lines covered (82.82%)

200.97 hits per line

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

94.81
/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 jwtDecode from 'jwt-decode';
17
import pluralize from 'pluralize';
18

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

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

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

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

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

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

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

81
  let status = deploymentDisplayStates[deploymentStatus];
1,966✔
82
  if (deploymentStatus === DEPLOYMENT_STATES.pending && currentProgressCount === 0) {
1,966✔
83
    status = 'queued';
979✔
84
  } else if (paused > 0) {
987!
85
    status = deploymentDisplayStates.paused;
×
86
  }
87
  return status;
1,966✔
88
};
89

90
export const decodeSessionToken = token => {
184✔
91
  try {
5✔
92
    var decoded = jwtDecode(token);
5✔
93
    return decoded.sub;
3✔
94
  } catch (err) {
95
    //console.log(err);
96
    return;
2✔
97
  }
98
};
99

100
export const isEmpty = obj => {
184✔
101
  for (const _ in obj) {
392✔
102
    return false;
181✔
103
  }
104
  return true;
211✔
105
};
106

107
export const extractErrorMessage = (err, fallback = '') =>
184✔
108
  err.response?.data?.error?.message || err.response?.data?.error || err.error || err.message || fallback;
12!
109

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

114
  try {
11✔
115
    if (res?.data && Object.keys(res.data).includes('request_id')) {
11✔
116
      let shortRequestUUID = res.data['request_id'].substring(0, 8);
2✔
117
      return `${failMsg} [Request ID: ${shortRequestUUID}]`;
2✔
118
    }
119
  } catch (e) {
120
    console.log('failed to extract request id:', e);
×
121
  }
122
  return failMsg;
9✔
123
};
124

125
export const versionCompare = (v1, v2) => {
184✔
126
  const partsV1 = `${v1}`.split('.');
19✔
127
  const partsV2 = `${v2}`.split('.');
19✔
128
  for (let index = 0; index < partsV1.length; index++) {
19✔
129
    const numberV1 = partsV1[index];
24✔
130
    const numberV2 = partsV2[index];
24✔
131
    if (numberV1 > numberV2) {
24✔
132
      return 1;
13✔
133
    }
134
    if (numberV2 > numberV1) {
11✔
135
      return -1;
5✔
136
    }
137
  }
138
  return 0;
1✔
139
};
140

141
/*
142
 *
143
 * Deep compare
144
 *
145
 */
146
// eslint-disable-next-line sonarjs/cognitive-complexity
147
export function deepCompare() {
148
  var i, l, leftChain, rightChain;
149

150
  function compare2Objects(x, y) {
151
    var p;
152

153
    // remember that NaN === NaN returns false
154
    // and isNaN(undefined) returns true
155
    if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
468!
156
      return true;
×
157
    }
158

159
    // Compare primitives and functions.
160
    // Check if both arguments link to the same object.
161
    // Especially useful on the step where we compare prototypes
162
    if (x === y) {
468✔
163
      return true;
225✔
164
    }
165

166
    // Works in case when functions are created in constructor.
167
    // Comparing dates is a common scenario. Another built-ins?
168
    // We can even handle functions passed across iframes
169
    if (
243✔
170
      (typeof x === 'function' && typeof y === 'function') ||
1,213!
171
      (x instanceof Date && y instanceof Date) ||
172
      (x instanceof RegExp && y instanceof RegExp) ||
173
      (x instanceof String && y instanceof String) ||
174
      (x instanceof Number && y instanceof Number)
175
    ) {
176
      return x.toString() === y.toString();
1✔
177
    }
178

179
    // At last checking prototypes as good as we can
180
    if (!(x instanceof Object && y instanceof Object)) {
242✔
181
      return false;
17✔
182
    }
183

184
    if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
225!
185
      return false;
×
186
    }
187

188
    if (x.constructor !== y.constructor) {
225!
189
      return false;
×
190
    }
191

192
    if (x.prototype !== y.prototype) {
225!
193
      return false;
×
194
    }
195

196
    // Check for infinitive linking loops
197
    if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
225!
198
      return false;
×
199
    }
200

201
    // Quick checking of one object being a subset of another.
202
    // todo: cache the structure of arguments[0] for performance
203
    for (p in y) {
225✔
204
      if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || typeof y[p] !== typeof x[p]) {
1,042✔
205
        return false;
55✔
206
      }
207
    }
208

209
    for (p in x) {
170✔
210
      if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || typeof y[p] !== typeof x[p]) {
680✔
211
        return false;
9✔
212
      }
213

214
      switch (typeof x[p]) {
671✔
215
        case 'object':
216
        case 'function':
217
          leftChain.push(x);
279✔
218
          rightChain.push(y);
279✔
219

220
          if (!compare2Objects(x[p], y[p])) {
279✔
221
            return false;
12✔
222
          }
223

224
          leftChain.pop();
267✔
225
          rightChain.pop();
267✔
226
          break;
267✔
227

228
        default:
229
          if (x[p] !== y[p]) {
392✔
230
            return false;
7✔
231
          }
232
          break;
385✔
233
      }
234
    }
235

236
    return true;
142✔
237
  }
238

239
  if (arguments.length < 1) {
187!
240
    return true; //Die silently? Don't know how to handle such case, please help...
×
241
    // throw "Need two or more arguments to compare";
242
  }
243

244
  for (i = 1, l = arguments.length; i < l; i++) {
187✔
245
    leftChain = []; //Todo: this can be cached
189✔
246
    rightChain = [];
189✔
247

248
    if (!compare2Objects(arguments[0], arguments[i])) {
189✔
249
      return false;
88✔
250
    }
251
  }
252

253
  return true;
99✔
254
}
255

256
export const stringToBoolean = content => {
184✔
257
  if (!content) {
235✔
258
    return false;
212✔
259
  }
260
  const string = content + '';
23✔
261
  switch (string.trim().toLowerCase()) {
23✔
262
    case 'true':
263
    case 'yes':
264
    case '1':
265
      return true;
19✔
266
    case 'false':
267
    case 'no':
268
    case '0':
269
    case null:
270
      return false;
3✔
271
    default:
272
      return Boolean(string);
1✔
273
  }
274
};
275

276
export const toggle = current => !current;
184✔
277

278
export const formatTime = date => {
184✔
279
  if (date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date)) {
75✔
280
    return date.toISOString().slice(0, -1);
1✔
281
  } else if (date) {
74✔
282
    return date.replace(' ', 'T').replace(/ /g, '').replace('UTC', '');
64✔
283
  }
284
};
285

286
export const customSort = (direction, field) => (a, b) => {
713✔
287
  if (typeof a[field] === 'string') {
192,527✔
288
    const result = a[field].localeCompare(b[field], { sensitivity: 'case' });
192,416✔
289
    return direction ? result * -1 : result;
192,416✔
290
  }
291
  if (a[field] > b[field]) return direction ? -1 : 1;
111!
292
  if (a[field] < b[field]) return direction ? 1 : -1;
91!
293
  return 0;
17✔
294
};
295

296
export const duplicateFilter = (item, index, array) => array.indexOf(item) == index;
3,964✔
297

298
export const attributeDuplicateFilter = (filterableArray, attributeName = 'key') =>
184!
299
  filterableArray.filter(
5✔
300
    (item, index, array) => array.findIndex(filter => filter[attributeName] === item[attributeName] && filter.scope === item.scope) == index
266✔
301
  );
302

303
export const unionizeStrings = (someStrings, someOtherStrings) => {
184✔
304
  const startingPoint = new Set(someStrings.filter(item => item.length));
24✔
305
  const uniqueStrings = someOtherStrings.length
24✔
306
    ? someOtherStrings.reduce((accu, item) => {
307
        if (item.trim().length) {
33✔
308
          accu.add(item.trim());
14✔
309
        }
310
        return accu;
33✔
311
      }, startingPoint)
312
    : startingPoint;
313
  return [...uniqueStrings];
24✔
314
};
315

316
export const generateDeploymentGroupDetails = (filter, groupName) =>
184✔
317
  filter && filter.terms?.length
3✔
318
    ? `${groupName} (${filter.terms
319
        .map(filter => `${filter.attribute || filter.key} ${DEVICE_FILTERING_OPTIONS[filter.type || filter.operator].shortform} ${filter.value}`)
4✔
320
        .join(', ')})`
321
    : groupName;
322

323
export const mapDeviceAttributes = (attributes = []) =>
184✔
324
  attributes.reduce(
150✔
325
    (accu, attribute) => {
326
      if (!(attribute.value && attribute.name) && attribute.scope === ATTRIBUTE_SCOPES.inventory) {
1,678!
327
        return accu;
×
328
      }
329
      accu[attribute.scope || ATTRIBUTE_SCOPES.inventory] = {
1,678✔
330
        ...accu[attribute.scope || ATTRIBUTE_SCOPES.inventory],
1,684✔
331
        [attribute.name]: attribute.value
332
      };
333
      if (attribute.name === 'device_type' && attribute.scope === ATTRIBUTE_SCOPES.inventory) {
1,678✔
334
        accu.inventory.device_type = [].concat(attribute.value);
81✔
335
      }
336
      return accu;
1,678✔
337
    },
338
    { inventory: { device_type: [], artifact_name: '' }, identity: {}, monitor: {}, system: {}, tags: {} }
339
  );
340

341
export const getFormattedSize = bytes => {
184✔
342
  const suffixes = ['Bytes', 'KB', 'MB', 'GB'];
143✔
343
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
143✔
344
  if (!bytes) {
143✔
345
    return '0 Bytes';
31✔
346
  }
347
  return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${suffixes[i]}`;
112✔
348
};
349

350
export const FileSize = React.forwardRef(({ fileSize, style }, ref) => (
184✔
351
  <div ref={ref} style={style}>
134✔
352
    {getFormattedSize(fileSize)}
353
  </div>
354
));
355
FileSize.displayName = 'FileSize';
184✔
356

357
const collectAddressesFrom = devices =>
184✔
358
  devices.reduce((collector, { attributes = {} }) => {
15!
359
    const ips = Object.entries(attributes).reduce((accu, [name, value]) => {
39✔
360
      if (name.startsWith('ipv4')) {
132✔
361
        if (Array.isArray(value)) {
27!
362
          const texts = value.map(text => text.slice(0, text.indexOf('/')));
×
363
          accu.push(...texts);
×
364
        } else {
365
          const text = value.slice(0, value.indexOf('/'));
27✔
366
          accu.push(text);
27✔
367
        }
368
      }
369
      return accu;
132✔
370
    }, []);
371
    collector.push(...ips);
39✔
372
    return collector;
39✔
373
  }, []);
374

375
export const getDemoDeviceAddress = (devices, onboardingApproach) => {
184✔
376
  const defaultVitualizedIp = '10.0.2.15';
15✔
377
  const addresses = collectAddressesFrom(devices);
15✔
378
  const address = addresses.reduce((accu, item) => {
15✔
379
    if (accu && item === defaultVitualizedIp) {
27!
380
      return accu;
×
381
    }
382
    return item;
27✔
383
  }, null);
384
  if (!address || (onboardingApproach === 'virtual' && (navigator.appVersion.indexOf('Win') != -1 || navigator.appVersion.indexOf('Mac') != -1))) {
15✔
385
    return 'localhost';
1✔
386
  }
387
  return address;
14✔
388
};
389

390
export const detectOsIdentifier = () => {
184✔
391
  if (navigator.appVersion.indexOf('Win') != -1) return 'Windows';
3!
392
  if (navigator.appVersion.indexOf('Mac') != -1) return 'MacOs';
3✔
393
  if (navigator.appVersion.indexOf('X11') != -1) return 'Unix';
2!
394
  return 'Linux';
2✔
395
};
396

397
export const getRemainderPercent = phases => {
184✔
398
  // remove final phase size if set
399
  phases[phases.length - 1].batch_size = null;
75✔
400
  // use this to get remaining percent of final phase so we don't set a hard number
401
  return phases.reduce((accu, phase) => (phase.batch_size ? accu - phase.batch_size : accu), 100);
203✔
402
};
403

404
export const validatePhases = (phases, deploymentDeviceCount, hasFilter) => {
184✔
405
  if (!phases?.length) {
37✔
406
    return true;
12✔
407
  }
408
  const remainder = getRemainderPercent(phases);
25✔
409
  return phases.reduce((accu, phase) => {
25✔
410
    if (!accu) {
66✔
411
      return accu;
5✔
412
    }
413
    const deviceCount = Math.floor((deploymentDeviceCount / 100) * (phase.batch_size || remainder));
61✔
414
    return deviceCount >= 1 || hasFilter;
61✔
415
  }, true);
416
};
417

418
export const getPhaseDeviceCount = (numberDevices = 1, batchSize, remainder, isLastPhase) =>
184✔
419
  isLastPhase ? Math.ceil((numberDevices / 100) * (batchSize || remainder)) : Math.floor((numberDevices / 100) * (batchSize || remainder));
129✔
420

421
export const startTimeSort = (a, b) => (b.created > a.created) - (b.created < a.created);
282✔
422

423
export const standardizePhases = phases =>
184✔
424
  phases.map((phase, index) => {
3✔
425
    let standardizedPhase = { batch_size: phase.batch_size, start_ts: index };
8✔
426
    if (phase.delay) {
8✔
427
      standardizedPhase.delay = phase.delay;
5✔
428
      standardizedPhase.delayUnit = phase.delayUnit || 'hours';
5✔
429
    }
430
    if (index === 0) {
8✔
431
      // delete the start timestamp from a deployment pattern, to default to starting without delay
432
      delete standardizedPhase.start_ts;
3✔
433
    }
434
    return standardizedPhase;
8✔
435
  });
436

437
const getInstallScriptArgs = ({ isHosted, isPreRelease }) => {
184✔
438
  let installScriptArgs = '--demo';
11✔
439
  installScriptArgs = isPreRelease ? `${installScriptArgs} -c experimental` : installScriptArgs;
11✔
440
  installScriptArgs = isHosted ? `${installScriptArgs} --commercial --jwt-token $JWT_TOKEN` : installScriptArgs;
11✔
441
  return installScriptArgs;
11✔
442
};
443

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

459
export const getDebConfigurationCode = props => {
184✔
460
  const { tenantToken, isPreRelease } = props;
11✔
461
  const envVars = tenantToken ? `JWT_TOKEN="${getToken()}"\nTENANT_TOKEN="${tenantToken}"\n` : '';
11✔
462
  const installScriptArgs = getInstallScriptArgs(props);
11✔
463
  const scriptUrl = isPreRelease ? 'https://get.mender.io/staging' : 'https://get.mender.io';
11✔
464
  const menderSetupArgs = getSetupArgs(props);
11✔
465
  return `${envVars}wget -O- ${scriptUrl} | sudo bash -s -- ${installScriptArgs} -- ${menderSetupArgs}`;
11✔
466
};
467

468
export const getSnackbarMessage = (skipped, done) => {
184✔
469
  pluralize.addIrregularRule('its', 'their');
1✔
470
  const skipText = skipped
1!
471
    ? `${skipped} ${pluralize('devices', skipped)} ${pluralize('have', skipped)} more than one pending authset. Expand ${pluralize(
472
        'this',
473
        skipped
474
      )} ${pluralize('device', skipped)} to individually adjust ${pluralize('their', skipped)} authorization status. `
475
    : '';
476
  const doneText = done ? `${done} ${pluralize('device', done)} ${pluralize('was', done)} updated successfully. ` : '';
1!
477
  return `${doneText}${skipText}`;
1✔
478
};
479

480
export const extractSoftware = (attributes = {}) => {
184✔
481
  const softwareKeys = Object.keys(attributes).reduce((accu, item) => {
62✔
482
    if (item.endsWith('.version')) {
162✔
483
      accu.push(item.substring(0, item.lastIndexOf('.')));
73✔
484
    }
485
    return accu;
162✔
486
  }, []);
487
  return Object.entries(attributes).reduce(
62✔
488
    (accu, item) => {
489
      if (softwareKeys.some(key => item[0].startsWith(key))) {
272✔
490
        accu.software.push(item);
88✔
491
      } else {
492
        accu.nonSoftware.push(item);
74✔
493
      }
494
      return accu;
162✔
495
    },
496
    { software: [], nonSoftware: [] }
497
  );
498
};
499

500
export const extractSoftwareItem = (artifactProvides = {}) => {
184✔
501
  const { software } = extractSoftware(artifactProvides);
31✔
502
  return (
31✔
503
    software
504
      .reduce((accu, item) => {
505
        const infoItems = item[0].split('.');
29✔
506
        if (infoItems[infoItems.length - 1] !== 'version') {
29!
507
          return accu;
×
508
        }
509
        accu.push({ key: infoItems[0], name: infoItems.slice(1, infoItems.length - 1).join('.'), version: item[1], nestingLevel: infoItems.length });
29✔
510
        return accu;
29✔
511
      }, [])
512
      // 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
513
      // 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)
514
      .sort((a, b) => a.nestingLevel - b.nestingLevel)
×
515
      .reduce((accu, item) => accu ?? item, undefined)
29✔
516
  );
517
};
518

519
export const createDownload = (target, filename) => {
184✔
520
  let link = document.createElement('a');
1✔
521
  link.setAttribute('href', target);
1✔
522
  link.setAttribute('download', filename);
1✔
523
  link.style.display = 'none';
1✔
524
  document.body.appendChild(link);
1✔
525
  link.click();
1✔
526
  document.body.removeChild(link);
1✔
527
};
528

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

531
export const getISOStringBoundaries = currentDate => {
184✔
532
  const date = [currentDate.getUTCFullYear(), `0${currentDate.getUTCMonth() + 1}`.slice(-2), `0${currentDate.getUTCDate()}`.slice(-2)].join('-');
305✔
533
  return { start: `${date}T00:00:00.000`, end: `${date}T23:59:59.999` };
305✔
534
};
535

536
export const isDarkMode = mode => mode === DARK_MODE;
497✔
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