Merge remote-tracking branch 'origin/master' into shpaster/merge-master

This commit is contained in:
Shiran Pasternak 2023-05-01 10:52:56 -04:00
Родитель 635d441c96 e1888e71cd
Коммит 83f4a16385
55 изменённых файлов: 4430 добавлений и 10225 удалений

Просмотреть файл

@ -1,3 +1,22 @@
# 2.17.0
[All items](https://github.com/Azure/BatchExplorer/milestone/51?closed=1)
### Features
* Adds cloud service deprecation warning on pool details page [\#2716](https://github.com/Azure/BatchExplorer/issues/2716)
* Removes custom themes [\#2715](https://github.com/Azure/BatchExplorer/issues/2715)
* Adds developer menu item for Profiles [\#2714](https://github.com/Azure/BatchExplorer/issues/2714)
* Accessibility improvements [\#2713](https://github.com/Azure/BatchExplorer/issues/2713)
### Bugs
* User was unable to upload package zip from batch explorer 2.16.1-stable.713 [\#2690](https://github.com/Azure/BatchExplorer/issues/2690)
# 2.16.1
* Test data caused storage account to show as "classic" [\#2659](https://github.com/Azure/BatchExplorer/issues/2659)
# 2.16.0
[All items](https://github.com/Azure/BatchExplorer/milestone/50?closed=1)
@ -614,7 +633,7 @@
* Task progress not exposing validity of task count api [\#1475](https://github.com/Azure/BatchExplorer/issues/1475)
* Ability to override the BatchLabs-data branch that we pull templates from [\#1474](https://github.com/Azure/BatchExplorer/issues/1474)
* Use select query for task list to improve performance [\#1468](https://github.com/Azure/BatchExplorer/issues/1468)
* Batch Account URI should have https:// prefix [\#1435](https://github.com/Azure/BatchExplorer/issues/1435)
* Batch Account URI should have <https://> prefix [\#1435](https://github.com/Azure/BatchExplorer/issues/1435)
* Task table column layout a little funky [\#1422](https://github.com/Azure/BatchExplorer/issues/1422)
* BatchLabs: App splited in features that are can be enabled and disabled [\#1449](https://github.com/Azure/BatchExplorer/issues/1449)
* BatchLabs one click node connect [\#1452](https://github.com/Azure/BatchExplorer/issues/1452)

Просмотреть файл

@ -26,25 +26,25 @@ Microsoft reserves all other rights not expressly granted under this agreement,
17. commander (https://github.com/tj/commander.js) - MIT
18. d3 (https://d3js.org) - ISC
19. decode-uri-component (https://github.com/SamVerschueren/decode-uri-component) - MIT
20. download (https://github.com/kevva/download) - MIT
21. electron-updater (https://github.com/electron-userland/electron-builder) - MIT
22. element-resize-detector (https://github.com/wnr/element-resize-detector) - MIT
23. extract-zip (https://github.com/maxogden/extract-zip) - BSD-2-Clause
24. focus-visible (https://github.com/WICG/focus-visible) - W3C
25. font-awesome (http://fontawesome.io/) - (OFL-1.1 AND MIT)
26. get-proxy-settings (https://github.com/Azure/get-proxy-settings) - MIT
27. glob (https://github.com/isaacs/node-glob) - ISC
28. hammerjs (http://hammerjs.github.io/) - MIT
29. https-proxy-agent (https://github.com/TooTallNate/node-https-proxy-agent) - MIT
30. immutable (https://facebook.github.com/immutable-js) - MIT
31. inflection (https://github.com/dreamerslab/node.inflection) - MIT
32. js-yaml (https://github.com/nodeca/js-yaml) - MIT
33. jschardet (https://github.com/aadsm/jschardet) - LGPL-2.1+
34. keytar (http://atom.github.io/node-keytar) - MIT
35. load-json-file (https://github.com/sindresorhus/load-json-file) - MIT
36. luxon (https://github.com/moment/luxon) - MIT
37. make-dir (https://github.com/sindresorhus/make-dir) - MIT
38. node-abi (https://github.com/lgeiger/node-abi#readme) - MIT
20. electron-updater (https://github.com/electron-userland/electron-builder) - MIT
21. element-resize-detector (https://github.com/wnr/element-resize-detector) - MIT
22. extract-zip (https://github.com/maxogden/extract-zip) - BSD-2-Clause
23. focus-visible (https://github.com/WICG/focus-visible) - W3C
24. font-awesome (http://fontawesome.io/) - (OFL-1.1 AND MIT)
25. get-proxy-settings (https://github.com/Azure/get-proxy-settings) - MIT
26. glob (https://github.com/isaacs/node-glob) - ISC
27. hammerjs (http://hammerjs.github.io/) - MIT
28. https-proxy-agent (https://github.com/TooTallNate/node-https-proxy-agent) - MIT
29. immutable (https://facebook.github.com/immutable-js) - MIT
30. inflection (https://github.com/dreamerslab/node.inflection) - MIT
31. js-yaml (https://github.com/nodeca/js-yaml) - MIT
32. jschardet (https://github.com/aadsm/jschardet) - LGPL-2.1+
33. keytar (http://atom.github.io/node-keytar) - MIT
34. load-json-file (https://github.com/sindresorhus/load-json-file) - MIT
35. luxon (https://github.com/moment/luxon) - MIT
36. make-dir (https://github.com/sindresorhus/make-dir) - MIT
37. node-abi (https://github.com/lgeiger/node-abi#readme) - MIT
38. node-downloader-helper (https://github.com/hgouveia/node-downloader-helper) - MIT
39. node-forge (https://github.com/digitalbazaar/forge) - (BSD-3-Clause OR GPL-2.0)
40. patternomaly (https://github.com/ashiguruma/patternomaly) - MIT
41. reflect-metadata (http://rbuckton.github.io/reflect-metadata) - Apache-2.0
@ -67,7 +67,7 @@ Microsoft reserves all other rights not expressly granted under this agreement,
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -125,7 +125,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -154,7 +154,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -183,7 +183,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -212,7 +212,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -270,7 +270,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -299,7 +299,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -328,7 +328,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -357,7 +357,7 @@ THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -580,23 +580,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
End license for decode-uri-component
============================================================
============================================================
Start license for download
------------------------------------------------------------
MIT License
Copyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> (github.com/kevva)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------
End license for download
============================================================
============================================================
Start license for electron-updater
------------------------------------------------------------
@ -690,7 +673,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
============================================================
Start license for focus-visible
------------------------------------------------------------
All Reports in this Repository are licensed by Contributors under the
All Reports in this Repository are licensed by Contributors under the
[W3C Software and Document
License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). Contributions to
Specifications are made under the [W3C CLA](https://www.w3.org/community/about/agreements/cla/).
@ -728,7 +711,7 @@ as SVG and JS file types.
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com)
Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
@ -828,7 +811,7 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2022 Fonticons, Inc.
Copyright 2023 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
@ -1675,6 +1658,34 @@ SOFTWARE.
End license for node-abi
============================================================
============================================================
Start license for node-downloader-helper
------------------------------------------------------------
MIT License
Copyright (c) 2018 Jose De Gouveia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------
End license for node-downloader-helper
============================================================
============================================================
Start license for node-forge
------------------------------------------------------------
@ -2050,7 +2061,7 @@ Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@ -2309,7 +2320,7 @@ END OF TERMS AND CONDITIONS
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
------------------------------------------------------------
End license for rxjs
@ -2479,7 +2490,7 @@ IN THE SOFTWARE.
------------------------------------------------------------
The MIT License
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -2542,7 +2553,7 @@ Azure CLI
Copyright (c) Microsoft Corporation
All rights reserved.
All rights reserved.
MIT License
@ -2697,7 +2708,7 @@ The externally maintained libraries used by Node.js are:
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2022 Unicode, Inc. All rights reserved.
Copyright © 1991-2023 Unicode, Inc. All rights reserved.
Distributed under the Terms of Use in https://www.unicode.org/copyright.html.
Permission is hereby granted, free of charge, to any person obtaining
@ -3125,6 +3136,7 @@ The externally maintained libraries used by Node.js are:
File: aclocal.m4 (only for ICU4C)
Section: pkg.m4 - Macros to locate and utilise pkg-config.
Copyright © 2004 Scott James Remnant .
Copyright © 2012-2015 Dan Nicholson
@ -3149,6 +3161,7 @@ The externally maintained libraries used by Node.js are:
the same distribution terms that you use for the rest of that
program.
(The condition for the exception is fulfilled because
ICU4C includes a configuration script generated by Autoconf,
namely the `configure` script.)
@ -3157,6 +3170,7 @@ The externally maintained libraries used by Node.js are:
File: config.guess (only for ICU4C)
This file is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
@ -3177,6 +3191,7 @@ The externally maintained libraries used by Node.js are:
program. This Exception is an additional permission under section 7
of the GNU General Public License, version 3 ("GPLv3").
(The condition for the exception is fulfilled because
ICU4C includes a configuration script generated by Autoconf,
namely the `configure` script.)
@ -3185,6 +3200,7 @@ The externally maintained libraries used by Node.js are:
File: install-sh (only for ICU4C)
Copyright 1991 by the Massachusetts Institute of Technology
Permission to use, copy, modify, distribute, and sell this software and its
@ -3899,6 +3915,47 @@ The externally maintained libraries used by Node.js are:
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
- ada, located at deps/ada, is licensed as follows:
"""
Copyright 2023 Ada authors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
- minimatch, located at deps/minimatch, is licensed as follows:
"""
The ISC License
Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
- npm, located at deps/npm, is licensed as follows:
"""
The npm application

Просмотреть файл

@ -35,6 +35,7 @@ exports.defineEnv = function(env) {
"NODE_ENV": JSON.stringify(env),
"RENDERER": JSON.stringify(true),
"HOT": helpers.hasProcessFlag("hot"),
"BE_ENABLE_A11Y_TESTING": process.env.BE_ENABLE_A11Y_TESTING,
},
});
};

Просмотреть файл

@ -3,7 +3,7 @@
"type": "standard",
"primary": "#2F71C4",
"primary-contrast": "#ffffff",
"danger": "#aa3939",
"danger": "#bc2f34",
"danger-contrast": "#ffffff",
"warn": "#a46026",
"warn-contrast": "#ffffff",
@ -18,7 +18,7 @@
"border": "#919191",
"editor": "vs",
"text": {
"primary": "#333333",
"primary": "#323130",
"secondary": "#666666"
},
"breadcrumb": {
@ -53,16 +53,16 @@
"disabled-bg": "#ededed"
},
"monitorChart": {
"core-count": "#1c3f95",
"core-count": "#004e8c",
"low-priority-core-count": "#a36a00",
"task-start-event": "#a36a00",
"task-complete-event": "#428000",
"task-fail-event": "#aa3939",
"starting-node-count": "#1c3f95",
"idle-node-count": "#be93d9",
"running-node-count": "#428000",
"start-task-failed-node-count": "#aa3939",
"rebooting-node-count": "#ff755c"
"task-start-event": "#6d5700",
"task-complete-event": "#0f700f",
"task-fail-event": "#bc2f34",
"starting-node-count": "#004e8c",
"idle-node-count": "#8764b8",
"running-node-count": "#0f700f",
"start-task-failed-node-count": "#bc2f34",
"rebooting-node-count": "#d93c20"
},
"input": {
"border": "#919191",
@ -74,5 +74,5 @@
"disabled-text": "var(--color-text-primary)",
"disabled-background": "#e5e5e5"
},
"chart-colors": ["#003f5c", "#aa3939", "#4caf50", "#ffa600"]
"chart-colors": ["#003f5c", "#bc2f34", "#4caf50", "#ffa600"]
}

Просмотреть файл

@ -14,6 +14,18 @@
"primary": "#cccccc",
"secondary": "#a7a8a9"
},
"monitorChart": {
"core-count": "#0a95ff",
"low-priority-core-count": "#ae8c00",
"task-start-event": "#ae8c00",
"task-complete-event": "#0a95ff",
"task-fail-event": "#f26363",
"starting-node-count": "#0a95ff",
"idle-node-count": "#c674d2",
"running-node-count": "#44a744",
"start-task-failed-node-count": "#f26363",
"rebooting-node-count": "#db7843"
},
"breadcrumb": {
"text": "white",
"background": "#5b5b5b",

2
desktop/definitions/index.d.ts поставляемый
Просмотреть файл

@ -4,7 +4,7 @@
declare type Environment = "production" | "development" | "test";
declare const ELECTRON_ENV: "renderer" | "main";
// Gloval variables set by webpack
// Global variables set by webpack
declare const ENV: Environment;

13747
desktop/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -16,7 +16,7 @@
"name": "Microsoft Corporation",
"email": "batchexplorer@microsoft.com"
},
"version": "2.16.0",
"version": "2.18.0",
"main": "build/client/main.prod.js",
"scripts": {
"ts": "ts-node --project tsconfig.node.json --files",
@ -63,7 +63,7 @@
"lint": "npm run eslint && npm run stylelint",
"lint:fix": "npm run eslint --fix && npm run stylelint",
"package": "npm run ts scripts/package/package.ts",
"postinstall": "npm run rebuild:app-deps",
"postinstall": "npm run rebuild:app-deps && npx patch-package",
"start-publish": "npm run ts scripts/publish/publish.ts",
"webpack": "node --trace-deprecation --max_old_space_size=4096 node_modules/webpack/bin/webpack.js",
"webpack:stats": "cross-env NODE_ENV=production npm run webpack -- --profile --json > coverage/webpack-stats.json",
@ -144,9 +144,10 @@
"html-webpack-plugin": "^3.2.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine": "~3.5.0",
"jasmine-axe": "^1.1.0",
"jasmine-core": "^3.6.0",
"jasmine-spec-reporter": "^4.2.1",
"karma": "^6.3.14",
"karma": "^6.3.16",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^2.0.3",
"karma-electron": "^6.3.4",
@ -162,6 +163,7 @@
"monaco-editor-webpack-plugin": "^1.9.0",
"node-fetch": "^2.6.7",
"nyc": "^15.1.0",
"patch-package": "^6.5.1",
"playwright": "^1.18.1",
"prettier": "^2.2.1",
"proxyquire": "^2.1.3",
@ -207,7 +209,6 @@
"commander": "^8.0.0",
"d3": "^7.8.2",
"decode-uri-component": "^0.2.1",
"download": "^8.0.0",
"electron-updater": "^4.3.8",
"element-resize-detector": "^1.2.1",
"extract-zip": "^1.6.7",
@ -227,7 +228,8 @@
"make-dir": "^2.1.0",
"monaco-editor": "^0.19.0",
"node-abi": "^2.18.0",
"node-forge": "^1.0.0",
"node-downloader-helper": "^2.1.6",
"node-forge": "^1.3.0",
"mkdirp": "^1.0.4",
"patternomaly": "^1.3.2",
"reflect-metadata": "^0.1.13",

Просмотреть файл

@ -0,0 +1,39 @@
diff --git a/node_modules/@azure/core-http/dist-esm/src/util/utils.js b/node_modules/@azure/core-http/dist-esm/src/util/utils.js
index 407e56c..34b73f8 100644
--- a/node_modules/@azure/core-http/dist-esm/src/util/utils.js
+++ b/node_modules/@azure/core-http/dist-esm/src/util/utils.js
@@ -7,10 +7,14 @@ const validUuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F
/**
* A constant that indicates whether the environment is node.js or browser based.
*/
+// KLUDGE: @azure/storage-blob uses isNode variable exported from @azure/core-http to
+// determine how it should process data. However, in the renderer process, isNode is
+// set to be true, which causes @azure/storage-blob fails to process data. Thus we need
+// to patch isNode to be false in the renderer process.
+// github issue: https://github.com/Azure/azure-sdk-for-js/issues/21110
export const isNode = typeof process !== "undefined" &&
- !!process.version &&
- !!process.versions &&
- !!process.versions.node;
+ !!process.env &&
+ !process.env.RENDERER
/**
* Checks if a parsed URL is HTTPS
*
diff --git a/node_modules/@azure/core-http/dist/index.js b/node_modules/@azure/core-http/dist/index.js
index 682b20d..d43a572 100644
--- a/node_modules/@azure/core-http/dist/index.js
+++ b/node_modules/@azure/core-http/dist/index.js
@@ -318,9 +318,9 @@ const validUuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F
* A constant that indicates whether the environment is node.js or browser based.
*/
const isNode = typeof process !== "undefined" &&
- !!process.version &&
- !!process.versions &&
- !!process.versions.node;
+ !!process.env &&
+ !process.env.RENDERER;
+
/**
* Encodes an URI.
*

Просмотреть файл

@ -1,5 +1,7 @@
websockets==10.1
pylint==2.3.0
azure-batch-extensions==9.0.0
pyinstaller==4.9
pyinstaller==5.9
pylint==2.17.0
azure-batch==12.0.0
azure-batch-extensions==9.0.0
# Explicitly needed for azure-batch-extensions:
azure-multiapi-storage==1.0.0
websockets==10.4

Просмотреть файл

@ -1,5 +0,0 @@
Set-Location "desktop"
$version = npm run -s ts "scripts/package/get-version.ts"
Write-Host "Updating build number to $version"
Write-Host "##vso[build.updatebuildnumber]$version"

Просмотреть файл

@ -23,7 +23,7 @@ export class FileSystemService {
private _makeDir: typeof import("make-dir").default;
private _glob: typeof import("glob");
private _chokidar: typeof import("chokidar");
private _download: typeof import("download");
private _download: typeof import("node-downloader-helper");
private _extractZip: typeof import("extract-zip");
constructor(app: ElectronApp) {
@ -31,7 +31,7 @@ export class FileSystemService {
this._makeDir = app.require("make-dir");
this._glob = app.require("glob");
this._chokidar = app.require("chokidar");
this._download = app.require("download");
this._download = app.require("node-downloader-helper");
this._extractZip = app.require("extract-zip");
this.commonFolders = {
@ -106,9 +106,12 @@ export class FileSystemService {
public async download(source: string, dest: string): Promise<string> {
await this.ensureDir(path.dirname(dest));
await this._download(source, path.dirname(dest), {
filename: path.basename(dest),
});
const downloader = new this._download.DownloaderHelper(
source,
path.dirname(dest),
{ fileName: path.basename(dest) }
);
await downloader.start();
return dest;
}

Просмотреть файл

@ -7,6 +7,7 @@ import { SortingStatus } from "@batch-flask/ui/abstract-list/list-data-sorter";
import { ClickableComponent } from "@batch-flask/ui/buttons";
import { BehaviorSubject, Subject, of } from "rxjs";
import { click } from "test/utils/helpers";
import { runAxe } from "test/utils/helpers/axe-helpers";
import { PartialSortWarningComponent } from "./partial-sort-warning.component";
@Component({
@ -59,6 +60,10 @@ describe("PartialSortWarningComponent", () => {
expect(de.query(By.css(".auto-update-warning"))).toBeFalsy();
});
it("should pass accessibility test", async () => {
expect(await runAxe(fixture.nativeElement)).toHaveNoViolations();
});
describe("when sorting is partial", () => {
beforeEach(() => {
testComponent.presenter.sortingStatus.next(SortingStatus.Partial);
@ -98,6 +103,10 @@ describe("PartialSortWarningComponent", () => {
expect(de.query(By.css(".partial-sort-warning"))).toBeFalsy();
expect(de.query(By.css(".auto-update-warning"))).toBeFalsy();
});
it("should pass accessibility test", async () => {
expect(await runAxe(fixture.nativeElement)).toHaveNoViolations();
});
});
describe("when auto upodate", () => {
@ -118,5 +127,9 @@ describe("PartialSortWarningComponent", () => {
expect(testComponent.presenter.update).toHaveBeenCalledOnce();
});
it("should pass accessibility test", async () => {
expect(await runAxe(fixture.nativeElement)).toHaveNoViolations();
});
});
});

Просмотреть файл

@ -10,6 +10,7 @@ import {
ActivityService,
} from "@batch-flask/ui/activity";
import { AsyncSubject } from "rxjs";
import { runAxe } from "test/utils/helpers/axe-helpers";
describe("ActivityMonitorFooterComponent", () => {
let fixture: ComponentFixture<ActivityMonitorFooterComponent>;
@ -89,4 +90,8 @@ describe("ActivityMonitorFooterComponent", () => {
expect(routerSpy.navigate).toHaveBeenCalledOnce();
expect(routerSpy.navigate).toHaveBeenCalledWith(["/activitymonitor", 3]);
});
it("should pass accessibility test", async () => {
expect(await runAxe(fixture.nativeElement)).toHaveNoViolations();
});
});

Просмотреть файл

@ -6,6 +6,7 @@ import { MaterialModule } from "@batch-flask/core";
import { ButtonComponent } from "@batch-flask/ui/buttons/button.component";
import { PermissionService } from "@batch-flask/ui/permission";
import { click } from "test/utils/helpers";
import { runAxe } from "test/utils/helpers/axe-helpers";
@Component({
template: `
@ -97,6 +98,10 @@ describe("ButtonComponent", () => {
expect(describedbyId).toBeBlank();
});
it("should pass accessibility test", async () => {
expect(await runAxe(fixture.nativeElement)).toHaveNoViolations();
});
describe("when disabled", () => {
beforeEach(() => {
testComponent.disabled = true;
@ -112,6 +117,10 @@ describe("ButtonComponent", () => {
fixture.detectChanges();
expect(testComponent.onAction).not.toHaveBeenCalled();
});
it("should pass accessibility test", async () => {
expect(await runAxe(fixture.nativeElement)).toHaveNoViolations();
});
});
describe("when enabled", () => {
@ -129,5 +138,9 @@ describe("ButtonComponent", () => {
fixture.detectChanges();
expect(testComponent.onAction).toHaveBeenCalledOnce();
});
it("should pass accessibility test", async () => {
expect(await runAxe(fixture.nativeElement)).toHaveNoViolations();
});
});
});

Просмотреть файл

@ -87,6 +87,15 @@ bl-button {
width: $action-btn-size;
}
&[type=square][color="primary"] {
&.focus-outline.focus-visible, &.focus-visible {
outline-color: white;
outline-width: 2px;
outline-style: solid;
outline-offset: -4px;
}
}
&[type="round"] {
border-radius: 99%;

Просмотреть файл

@ -1,5 +1,5 @@
import {
AfterViewInit, ChangeDetectorRef, Component, ContentChildren, HostBinding, Input, OnChanges, QueryList, Type,
AfterViewInit, ChangeDetectorRef, Component, ContentChildren, ElementRef, HostBinding, Input, OnChanges, QueryList, Type, ViewChild,
} from "@angular/core";
import { FormControl } from "@angular/forms";
import { AsyncTask, Dto, ServerError, autobind } from "@batch-flask/core";
@ -71,6 +71,8 @@ export class ComplexFormComponent extends FormBase implements AfterViewInit, OnC
@ContentChildren(FormPageComponent) public pages: QueryList<FormPageComponent>;
@ViewChild('formElement') formElement: ElementRef;
public mainPage: FormPageComponent;
public currentPage: FormPageComponent;
public showJsonEditor = false;
@ -95,6 +97,9 @@ export class ComplexFormComponent extends FormBase implements AfterViewInit, OnC
this.currentPage = page;
this.mainPage = page;
this.changeDetector.detectChanges();
setTimeout(() => {
this.focusFirstFocusableElement();
})
}
public ngOnChanges(changes) {
@ -168,6 +173,10 @@ export class ComplexFormComponent extends FormBase implements AfterViewInit, OnC
}
this._pageStack.push(this.currentPage);
this.currentPage = page;
setTimeout(() => {
this.focusFirstFocusableElement()
});
}
@autobind()
@ -268,4 +277,8 @@ export class ComplexFormComponent extends FormBase implements AfterViewInit, OnC
multiUse: this.multiUse,
};
}
private focusFirstFocusableElement() {
this.formElement.nativeElement?.querySelector('[autofocus], button, input, textarea, select')?.focus()
}
}

Просмотреть файл

@ -10,7 +10,7 @@
</div>
</div>
<div *ngIf="!showJsonEditor" class="classic-form-container">
<form novalidate>
<form #formElement novalidate>
<ng-template [ngTemplateOutlet]="currentPage.content"></ng-template>
</form>
</div>

Просмотреть файл

@ -24,7 +24,7 @@
</td>
<td class="action-column">
<bl-clickable *ngIf="!isLast" class="delete-item-btn" [attr.aria-label]="'editable-table.deleteRow' | i18n"
(do)="deleteItem(i)">
(do)="deleteItem(i)" title="delete metadata row">
<i class="fa fa-times" aria-hidden="true"></i>
</bl-clickable>
</td>

Просмотреть файл

@ -8,7 +8,7 @@ bl-form-field {
sup.required {
line-height: 1em;
.fa-asterisk {
color: var(--color-danger);
color: $danger-color;
font-size: 8px;
}
}
@ -20,7 +20,7 @@ bl-form-field {
> .input-prefix, > .input-suffix {
background: $input-border-color;
background: $secondary-background;
border: 1px solid $input-border-color;
height: 26px;
padding: 0 5px;
@ -28,6 +28,14 @@ bl-form-field {
vertical-align: middle;
color: $primary-text;
}
> .input-prefix {
border-right: none;
}
> .input-suffix {
border-left: none;
}
}
.bl-form-field-label {}
@ -43,6 +51,7 @@ bl-form-field {
width: 100%;
flex: 1;
min-width: 0;
border: 1px solid $input-border-color;
&::placeholder {
color: transparent;

Просмотреть файл

@ -7,6 +7,9 @@ bl-metrics-monitor {
> .tab-navigation-item {
padding: 0;
}
canvas {
pointer-events: none;
}
}
.preview {

Просмотреть файл

@ -2,7 +2,6 @@
bl-property-content {
color: $primary-text;
background-color: $main-background;
padding: 3px 8px;
margin: 0;
text-overflow: ellipsis;
@ -10,6 +9,7 @@ bl-property-content {
display: block;
width: 100%;
min-height: 24px;
border: 1px solid $input-border-color;
&:not(.wrap) {
white-space: nowrap;

Просмотреть файл

@ -532,6 +532,7 @@ export class SelectComponent<TValue = any> implements FormFieldControl<any>, Opt
} else if (isArrowKey && event.altKey || keyCode === ESCAPE) {
// Close the select on ALT + arrow key to match the native <select>
event.preventDefault();
event.stopPropagation();
this.closeDropdown();
} else if ((keyCode === ENTER || keyCode === SPACE) && navigator.focusedItem) {
event.preventDefault();

Просмотреть файл

@ -102,6 +102,11 @@ bl-table {
}
}
&.focused {
outline: 1px dashed $primary-color;
outline-offset: -2px;
}
&.focused.selected {
outline: 1px dashed $primary-contrast-color;
outline-offset: -2px;

Просмотреть файл

@ -86,8 +86,7 @@ export class AutoStorageAccountPickerComponent implements OnInit, ControlValueAc
private _processStorageAccounts(storageAccounts: List<StorageAccount>) {
const prefered = [];
const others = [];
storageAccounts.forEach((account, i) => {
account.isClassic = i % 2 === 0;
storageAccounts.forEach((account) => {
if (account.location.toLowerCase() === this.account.location.toLowerCase()) {
prefered.push(account);
} else {
@ -113,13 +112,13 @@ export class AutoStorageAccountPickerComponent implements OnInit, ControlValueAc
* to define a custom handler.
*/
onKeydown(event: KeyboardEvent) {
this.classicTooltip.hide();
this.classicTooltip?.hide();
if (event.key === "ArrowDown" || event.key === "ArrowUp" ||
event.key === "Tab") {
if (this.selectedStorageAccounts.size === 1) {
const id = this.selectedStorageAccounts.values().next().value;
if (this.classicAccounts.has(id)) {
this.classicTooltip.show();
this.classicTooltip?.show();
}
}
}

Просмотреть файл

@ -1,6 +1,6 @@
<bl-loading [status]="loadingStatus">
<div *ngIf="loadingStatus">
<bl-button type="wide" (do)="pickStorageAccount(noSelectionKey)">
<bl-button type="wide" (do)="pickStorageAccount(noSelectionKey)" autofocus>
{{'auto-storage-account-picker.clear-button-label' | i18n}}
</bl-button>
<h3>{{'auto-storage-account-picker.same-region-title' | i18n: {

Просмотреть файл

@ -12,4 +12,7 @@ bl-account-monitoring-section {
bl-time-range-picker {
margin-right: 10px;
}
canvas {
pointer-events: none;
}
}

Просмотреть файл

@ -16,6 +16,6 @@ bl-programming-sample {
}
bl-tp-cell {
border: 1px solid var(--color-input-border);
border: 1px solid $input-border-color;
}
}

Просмотреть файл

@ -196,7 +196,8 @@ export class MonitorChartComponent implements OnChanges, OnDestroy {
},
tooltips: {
enabled: true,
mode: "index",
mode: "single",
position: "nearest",
callbacks: {
title: (tooltipItems, data) => {
return this._computeTooltipTitle(tooltipItems[0], data);

Просмотреть файл

@ -62,5 +62,6 @@ bl-monitor-chart {
> .monitor-chart-preview {
height: 100%;
width: 100%;
pointer-events: none;
}
}

Просмотреть файл

@ -17,6 +17,7 @@ import * as TestConstants from "test/test-constants";
import { validateControl } from "test/utils/helpers";
import { MockedFile } from "test/utils/mocks";
import { ServerErrorMockComponent, complexFormMockComponents } from "test/utils/mocks/components";
import { isNode } from '@azure/core-http';
describe("ApplicationCreateDialogComponent ", () => {
let fixture: ComponentFixture<ApplicationCreateDialogComponent>;
@ -304,5 +305,11 @@ describe("ApplicationCreateDialogComponent ", () => {
expect(appAddedSpy).toHaveBeenCalledTimes(1);
expect(appAddedSpy).toHaveBeenCalledWith("activate-fail");
});
it("isNode from @azure/core-http should be false in the renderer process", async () => {
// see patches/@azure+core-http+2.2.7.patch
expect(isNode).toBe(false);
});
});
});

Просмотреть файл

@ -108,7 +108,7 @@ export class ApplicationCreateDialogComponent {
if (!this.hasValidFile()) {
return throwError("Valid file not selected");
}
return this.storageBlobService.uploadToSasUrl(sasUrl, file.path);
return this.storageBlobService.uploadToSasUrl(sasUrl, file);
}
private _tryActivate(applicationName: string, pkg: BatchApplicationPackage): Observable<any> {

Просмотреть файл

@ -4,7 +4,7 @@
</div>
<div blBrowseLayoutButtons>
<bl-button type="plain" icon="fa fa-plus spin-hover" color="light" title="Add a file group" [matMenuTriggerFor]="addFileGroupMenu"
[disabled]="!hasAutoStorage" [@.disabled]="true">
(click)="addFileGroup()" (do)="addFileGroup()" [disabled]="!hasAutoStorage" [@.disabled]="true">
</bl-button>
<mat-menu #addFileGroupMenu="matMenu" [@.disabled]="true">
<button mat-menu-item (click)="openEmptyContainerForm()"> Empty container </button>

Просмотреть файл

@ -10,7 +10,7 @@
<bl-form-page [title]="title" main-form-page [formGroup]="form">
<bl-form-section title="Mode" *ngIf="multipleModes">
<div class="modes">
<bl-button type="wide" [class.selected]="modeState === NcjTemplateMode.NewPoolAndJob" (do)="pickMode(NcjTemplateMode.NewPoolAndJob)">Run job with auto pool</bl-button>
<bl-button type="wide" [class.selected]="modeState === NcjTemplateMode.NewPoolAndJob" (do)="pickMode(NcjTemplateMode.NewPoolAndJob)" autofocus>Run job with auto pool</bl-button>
<bl-button type="wide" [class.selected]="modeState === NcjTemplateMode.ExistingPoolAndJob" (do)="pickMode(NcjTemplateMode.ExistingPoolAndJob)">Run job with existing pool</bl-button>
<bl-button type="wide" [class.selected]="modeState === NcjTemplateMode.NewPool" (do)="pickMode(NcjTemplateMode.NewPool)">Create pool for later use</bl-button>
</div>

Просмотреть файл

@ -1,7 +1,7 @@
<div [formGroup]="form">
<div class="form-element">
<bl-form-field>
<input blInput formControlName="id" placeholder="ID">
<input blInput autofocus formControlName="id" placeholder="ID">
</bl-form-field>
<bl-error controlName="id" code="required">ID is a required field</bl-error>
</div>

Просмотреть файл

@ -1,7 +1,7 @@
<div [formGroup]="form">
<div class="form-element">
<bl-form-field>
<input blInput formControlName="id" placeholder="ID">
<input blInput autofocus formControlName="id" placeholder="ID">
</bl-form-field>
<bl-error controlName="id" code="required">ID is a required field</bl-error>
</div>

Просмотреть файл

@ -130,7 +130,7 @@ describe("ProfileButtonComponent", () => {
fixture.detectChanges();
expect(contextMenuServiceSpy.openMenu).toHaveBeenCalledOnce();
const items = contextMenuServiceSpy.lastMenu.items;
expect(items.length).toBe(16);
expect(items.length).toBe(13);
});
describe("Clicking on the profile", () => {
@ -138,7 +138,7 @@ describe("ProfileButtonComponent", () => {
click(clickableEl);
expect(contextMenuServiceSpy.openMenu).toHaveBeenCalled();
const items = contextMenuServiceSpy.lastMenu.items;
expect(items.length).toEqual(16);
expect(items.length).toEqual(13);
let i = 0;
const expectMenuItem= (menuItemType, label?) => {
@ -155,15 +155,12 @@ describe("ProfileButtonComponent", () => {
expectMenuItem(ContextMenuItem, "profile-button.authentication");
expectMenuItem(ContextMenuItem, "profile-button.keybindings");
expectMenuItem(MultiContextMenuItem, "Language (Preview)");
expectMenuItem(MultiContextMenuItem, "Developer");
expectMenuItem(ContextMenuItem, "profile-button.thirdPartyNotices");
expectMenuItem(ContextMenuItem, "profile-button.viewLogs");
expectMenuItem(ContextMenuItem, "profile-button.report");
expectMenuItem(ContextMenuItem, "profile-button.about");
expectMenuItem(ContextMenuSeparator);
expectMenuItem(ContextMenuItem, "profile-button.viewTheme");
expectMenuItem(ContextMenuSeparator);
expectMenuItem(ContextMenuItem, "profile-button.playground");
expectMenuItem(ContextMenuSeparator);
expectMenuItem(ContextMenuItem, "profile-button.logout");
});

Просмотреть файл

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, isDevMode, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { I18nService, Locale, LocaleService, TranslatedLocales } from "@batch-flask/core";
import {
@ -112,20 +112,20 @@ export class ProfileButtonComponent implements OnDestroy, OnInit {
new ContextMenuItem({ label: this.i18n.t("profile-button.report"), click: () => this._openGithubIssues() }),
new ContextMenuItem({ label: this.i18n.t("profile-button.about"), click: () => this._showAboutPage() }),
new ContextMenuSeparator(),
new ContextMenuItem({
label: this.i18n.t("profile-button.viewTheme"),
click: () => this._gotoThemeColors(),
}),
new ContextMenuSeparator(),
new ContextMenuItem({
label: this.i18n.t("profile-button.playground"),
click: () => this._gotoPlayground(),
}),
new ContextMenuSeparator(),
new ContextMenuItem({ label: this.i18n.t("profile-button.logout"), click: () => this._logout() }),
];
if (isDevMode()) {
const devMenuItem = new MultiContextMenuItem({
label: "Developer",
subitems: [
new ContextMenuItem({ label: this.i18n.t("profile-button.viewTheme"), click: () => this._gotoThemeColors() })
],
});
items.splice(5, 0, devMenuItem);
}
items.unshift(this._getAutoUpdateMenuItem());
this.contextMenuService.openMenu(new ContextMenu(items));
}

Просмотреть файл

@ -63,6 +63,9 @@ bl-os-offer-tile {
> .name {
text-transform: capitalize;
display: inline-block;
word-break: break-word;
line-height: 1.3;
}
}

Просмотреть файл

@ -48,7 +48,7 @@
<bl-banner class="create-pool-deprecation-warning" type="warning">
<div message>
Cloud Services pools are deprecated and will be retired on 29 February 2024. Existing CloudServiceConfiguration pools will need to be deleted and new
<a class="create-pool-warning-alternative-link" (click)="openLink('https://docs.microsoft.com/azure/batch/batch-pool-cloud-service-to-virtual-machine-configuration?WT.mc_id=Portal-Microsoft_Azure_Batch')" href="javascript:void(0)">VirtualMachineConfiguration pools created. </a>
<a class="create-pool-warning-alternative-link" (click)="openLink('https://docs.microsoft.com/azure/batch/batch-pool-cloud-service-to-virtual-machine-configuration')" href="javascript:void(0)">VirtualMachineConfiguration pools created. </a>
For new pools, select either a different Distributions or Custom Image option.
</div>
</bl-banner>

Просмотреть файл

@ -25,8 +25,8 @@
.formula-list {
height: 100%;
overflow: auto;
border-top: 1px solid #d5d5d5;
border-bottom: 1px solid #d5d5d5;
border-top: 1px solid $input-disabled-border-color;
border-bottom: 1px solid $input-disabled-border-color;
> .formula {
display: flex;
@ -41,7 +41,7 @@
}
&:not(:last-child) {
border-bottom: 1px solid #d5d5d5;
border-bottom: 1px solid $input-disabled-border-color;
}
&:hover {

Просмотреть файл

@ -45,6 +45,9 @@ export class PoolDetailsComponent implements OnInit, OnDestroy {
public get isImageDeprecated() {
return this._isImageDeprecated;
}
public get isCloudServicePool() {
return this._isCloudServicePool;
}
public get hasDeprecationLink() {
return this.isImageDeprecated && PoolUtils.getEndOfLifeHyperlinkforPoolDetails(this.poolDecorator.poolOs);
}
@ -61,6 +64,7 @@ export class PoolDetailsComponent implements OnInit, OnDestroy {
private _paramsSubscriber: Subscription;
private _pool: Pool;
private _isImageDeprecated: boolean;
private _isCloudServicePool: boolean;
private _supportedImages: ImageInformation[];
private _selectedImageEndOfLifeDate: Date;
@ -81,6 +85,7 @@ export class PoolDetailsComponent implements OnInit, OnDestroy {
this.changeDetector.markForCheck();
this._updatePrice();
this._updatePoolDeprecationWarning();
this._cloudServiceDeprecationWarning();
});
this.data.deleted.subscribe((key) => {
@ -99,6 +104,7 @@ export class PoolDetailsComponent implements OnInit, OnDestroy {
this.poolOsService.supportedImages.subscribe(val => {
this._supportedImages = val.toArray();
this._updatePoolDeprecationWarning();
this._cloudServiceDeprecationWarning();
});
}
@ -173,6 +179,13 @@ export class PoolDetailsComponent implements OnInit, OnDestroy {
}
}
private _cloudServiceDeprecationWarning() {
this._isCloudServicePool = false;
if (this.pool && this.pool.cloudServiceConfiguration) {
this._isCloudServicePool = true;
}
}
private _updateImageEOLState() {
const diff = this._selectedImageEndOfLifeDate.getTime() - Date.now();
const days = diff / (1000 * 3600 * 24);

Просмотреть файл

@ -46,6 +46,16 @@
</bl-banner>
</ng-container>
<ng-container *ngIf="isCloudServicePool">
<bl-banner class="pool-details-summary-deprecation-warning" type="warning">
<div message>
This is a Cloud Service pool, which is deprecated and will be retired on 29 February 2024. After this date, you will not be able to create new CloudServiceConfiguration pools or add new nodes to existing pools.
Please delete and recreate this pool using VirtualMachineConfiguration to avoid disruption to your applications.
<a class="pool-details-summary-alternative-link" (click)="openLink('https://docs.microsoft.com/azure/batch/batch-pool-cloud-service-to-virtual-machine-configuration')" href="javascript:void(0)"> Learn more </a>
</div>
</bl-banner>
</ng-container>
<bl-pool-error-display [pool]="pool"></bl-pool-error-display>
<bl-card class="details">
<bl-tab-group>

Просмотреть файл

@ -33,7 +33,7 @@ be-tenant-card {
}
background: $card-background;
border: 1px solid #ccc;
border: 1px solid $border-color;
border-radius: $tenant-card-padding;
&.home-tenant, &.home-tenant .main-tile {

Просмотреть файл

@ -1,13 +1,17 @@
import { TestBed } from "@angular/core/testing";
import { AccessToken, I18nService, ServerError, TenantSettingsService } from "@batch-flask/core";
import { AuthService } from "app/services/aad";
import { I18nTestingModule } from "@batch-flask/core/testing";
import { AuthService, TenantAuthorization } from "app/services/aad";
import { IpcEvent } from "common/constants";
import { DateTime } from "luxon";
import { BehaviorSubject, combineLatest, Observable } from "rxjs";
import { TenantErrorService } from ".";
const tenant1 = "tenant-1";
const tenant2 = "tenant-2";
const FakeTenants = {
One: "tenant-1",
Two: "tenant-2",
Three: "tenant-3",
}
const resource1 = "batch";
const token1 = new AccessToken({
accessToken: "sometoken",
@ -32,9 +36,9 @@ describe("AuthService spec", () => {
beforeEach(() => {
aadServiceSpy = {
tenants: new BehaviorSubject([
{ tenantId: tenant1 }, { tenantId: tenant2 }
]),
tenants: new BehaviorSubject(
Object.values(FakeTenants).map(t => ({ tenantId: t }))
),
currentUser: new BehaviorSubject([]),
};
remoteSpy = {
@ -66,13 +70,8 @@ describe("AuthService spec", () => {
};
TestBed.configureTestingModule({
imports: [I18nTestingModule],
providers: [
{
provide: I18nService,
useValue: {
t: jasmine.createSpy()
}
},
{
provide: TenantSettingsService,
useValue: tenantSettingsServiceSpy
@ -120,11 +119,12 @@ describe("AuthService spec", () => {
});
it("#accessTokenFor returns observable with token string", (done) => {
service.accessTokenFor(tenant1, resource1).subscribe((token) => {
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(token).toEqual(token1.accessToken);
done();
});
service.accessTokenFor(FakeTenants.One, resource1)
.subscribe((token) => {
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(token).toEqual(token1.accessToken);
done();
});
});
describe("getAccessToken()", () => {
@ -139,17 +139,18 @@ describe("AuthService spec", () => {
describe("#accessTokenData", () => {
it("#accessTokenData returns observable with token", done => {
service.accessTokenData(tenant1, resource1).subscribe((token) => {
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(token).toEqual(token1);
done();
});
service.accessTokenData(FakeTenants.One, resource1)
.subscribe((token) => {
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(token).toEqual(token1);
done();
});
});
it("loads a new token by calling aadService", done => {
combineLatest([
service.accessTokenData(tenant1, resource1),
service.accessTokenData(tenant2, resource1)
service.accessTokenData(FakeTenants.One, resource1),
service.accessTokenData(FakeTenants.Two, resource1)
]).subscribe(([tokenA, tokenB]) => {
expect(remoteSpy.send).toHaveBeenCalledTimes(2);
expect(tokenA).toEqual(token1);
@ -160,21 +161,25 @@ describe("AuthService spec", () => {
it("reuses remote calls for same tenant", done => {
combineLatest([
service.accessTokenData(tenant1, resource1),
service.accessTokenData(tenant2, resource1),
service.accessTokenData(tenant1, resource1)
service.accessTokenData(FakeTenants.One, resource1),
service.accessTokenData(FakeTenants.Two, resource1),
service.accessTokenData(FakeTenants.One, resource1)
]).subscribe(([tokenA, tokenB, tokenC]) => {
expect(remoteSpy.send).toHaveBeenCalledTimes(2);
expect(remoteSpy.send.calls.allArgs()).toEqual([
[
IpcEvent.AAD.accessTokenData,
{ tenantId: tenant1, resource: resource1,
forceRefresh: false }
{
tenantId: FakeTenants.One, resource: resource1,
forceRefresh: false
}
],
[
IpcEvent.AAD.accessTokenData,
{ tenantId: tenant2, resource: resource1,
forceRefresh: false }
{
tenantId: FakeTenants.Two, resource: resource1,
forceRefresh: false
}
]
]);
expect(tokenA).toEqual(token1);
@ -185,32 +190,32 @@ describe("AuthService spec", () => {
});
it("calls again the main process if previous call returned an error",
async () => {
remoteSpy.send = jasmine.createSpy("send").and.returnValues(
Promise.reject("some-error"),
Promise.resolve(token1),
);
async () => {
remoteSpy.send = jasmine.createSpy("send").and.returnValues(
Promise.reject("some-error"),
Promise.resolve(token1),
);
await new Promise<void>(resolve => {
service.accessTokenData(tenant1, resource1).subscribe({
next: () => fail("Should not have a next() call"),
error: error => {
expect(error).toEqual("some-error");
resolve();
}
await new Promise<void>(resolve => {
service.accessTokenData(FakeTenants.One, resource1).subscribe({
next: () => fail("Should not have a next() call"),
error: error => {
expect(error).toEqual("some-error");
resolve();
}
});
});
});
await new Promise<void>(resolve => {
service.accessTokenData(tenant1, resource1).subscribe({
next: token => {
expect(token).toEqual(token1);
resolve();
}
await new Promise<void>(resolve => {
service.accessTokenData(FakeTenants.One, resource1).subscribe({
next: token => {
expect(token).toEqual(token1);
resolve();
}
});
});
expect(remoteSpy.send).toHaveBeenCalledTimes(2);
});
expect(remoteSpy.send).toHaveBeenCalledTimes(2);
});
});
it("updates the tenants when updated by the auth service", () => {
@ -223,98 +228,114 @@ describe("AuthService spec", () => {
});
describe("getTenantAuthorizations", () => {
it("notifies on error by default", done => {
function auth(opts?): Promise<TenantAuthorization[]> {
return new Promise(resolve => {
service.getTenantAuthorizations(opts).subscribe(
authorizations => resolve(authorizations)
);
});
}
it("notifies on error by default", async () => {
remoteSpy.send.and.callFake(async (_, { tenantId }) => {
if (tenantId === tenant1) {
if (tenantId === FakeTenants.One) {
throw new Error("Fake error for tenant-1");
} else {
return token2;
}
});
service.getTenantAuthorizations()
.subscribe(authorizations => {
expect(tenantErrorServiceSpy.showError).toHaveBeenCalledOnce();
expect(tenantErrorServiceSpy.showError).toHaveBeenCalledWith(
authorizations[0]
)
done();
});
});
it("skips inactive tenants", done => {
tenantSettingsServiceSpy.current.next({
"tenant-1": "inactive",
"tenant-2": "active"
[FakeTenants.One]: "active"
});
service.getTenantAuthorizations()
.subscribe(authorizations => {
expect(authorizations.length).toEqual(2);
expect(authorizations[0].tenant.tenantId).toEqual("tenant-1");
expect(authorizations[0].active).toBeFalsy();
expect(authorizations[1].tenant.tenantId).toEqual("tenant-2");
expect(authorizations[1].active).toBeTruthy();
done();
})
const authorizations = await auth();
expect(tenantErrorServiceSpy.showError).toHaveBeenCalledOnce();
expect(tenantErrorServiceSpy.showError).toHaveBeenCalledWith(
authorizations[0]
);
});
it("forces refresh on specific tenant reauthentication", done => {
service.getTenantAuthorizations({ reauthenticate: tenant1 })
.subscribe(() => {
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: tenant1,
resource: null,
forceRefresh: true
});
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: tenant2,
resource: null,
forceRefresh: false
},
);
done();
})
it("skips inactive tenants", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "inactive",
[FakeTenants.Two]: "active"
});
const authorizations = await auth();
expect(authorizations.length).toEqual(3);
expect(authorizations[0].tenant.tenantId)
.toEqual(FakeTenants.One);
expect(authorizations[0].active).toBeFalsy();
expect(authorizations[1].tenant.tenantId)
.toEqual(FakeTenants.Two);
expect(authorizations[1].active).toBeTruthy();
expect(authorizations[2].tenant.tenantId)
.toEqual(FakeTenants.Three);
expect(authorizations[2].active).toBeFalsy();
});
it("force-refreshes on all tenants", done => {
service.getTenantAuthorizations({ reauthenticate: "*" })
.subscribe(() => {
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: tenant1,
resource: null,
forceRefresh: true
});
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: tenant2,
resource: null,
forceRefresh: true
},
);
done();
})
it("assumes unconfigured tenants are inactive", async () => {
tenantSettingsServiceSpy.current.next({});
const authorizations = await auth();
expect(authorizations.length).toEqual(3);
expect(authorizations[0].active).toBeFalsy();
expect(authorizations[1].active).toBeFalsy();
expect(authorizations[2].active).toBeFalsy();
});
it("doesn't force-refresh previous failure when reauthenticating all tenants", done => {
it("forces refresh on specific tenant reauthentication", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active",
[FakeTenants.Two]: "active"
});
await auth({ reauthenticate: FakeTenants.One });
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.One,
resource: null,
forceRefresh: true
});
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.Two,
resource: null,
forceRefresh: false
});
});
it("force-refreshes on all tenants", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active",
[FakeTenants.Two]: "active"
});
await auth({ reauthenticate: "*" });
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.One,
resource: null,
forceRefresh: true
});
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.Two,
resource: null,
forceRefresh: true
});
});
it("doesn't refresh failure when reauthenticating all", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active",
[FakeTenants.Two]: "active"
});
remoteSpy.send.and.callFake(async (_, { tenantId }) => {
if (tenantId === tenant1) {
if (tenantId === FakeTenants.One) {
throw new Error("Fake error for tenant-1");
} else {
return token2;
}
});
service.getTenantAuthorizations().subscribe(() => {
remoteSpy.send.calls.reset();
service.getTenantAuthorizations({ reauthenticate: "*" })
.subscribe(() => {
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: tenant2,
resource: null,
forceRefresh: true
},
);
done();
});
await auth(); // First call results in failure
remoteSpy.send.calls.reset();
await auth({ reauthenticate: "*" });
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.Two,
resource: null,
forceRefresh: true
});
});
});

Просмотреть файл

@ -91,18 +91,21 @@ export class AuthService implements OnDestroy {
this.tenants,
this.tenantSettingsService.current,
]).pipe(
map(([tenants, settings]: [TenantDetails[], TenantSettings]) =>
tenants.map((tenant: TenantDetails) => ({
map(([tenants, settings]: [TenantDetails[], TenantSettings]) => {
return tenants.map((tenant: TenantDetails) => ({
tenant,
active: !(tenant.tenantId in settings) ||
// A tenant is active if it is the home tenant or if it is
// explicitly set to active in the settings.
active: tenant.homeTenantId === tenant.tenantId ||
settings[tenant.tenantId] === "active",
status: TenantStatus.unknown
} as TenantAuthorization))
),
}),
switchMap(authorizations => forkJoin(authorizations.map(
authorization =>
this.authorizeTenant(authorization, authOptions)
))
))
),
share()
);
@ -178,7 +181,7 @@ export class AuthService implements OnDestroy {
public accessTokenData(
tenantId: string, resource: AADResourceName = null, forceRefresh = false
):
Observable<AccessToken> {
Observable<AccessToken> {
const key = [tenantId, resource].join("|");
if (key in this.tokenObservableCache) {
return this.tokenObservableCache[key];
@ -223,7 +226,7 @@ export class AuthService implements OnDestroy {
// Caches current authorization state to avoid reauthenticating failed
// tenants without user request.
private cacheAuthorization(authorization: TenantAuthorization):
Observable<TenantAuthorization> {
Observable<TenantAuthorization> {
this.previousTenantState[authorization.tenant.tenantId] =
authorization;
return of(authorization);

Просмотреть файл

@ -11,7 +11,7 @@ import {
enterZone,
} from "@batch-flask/core";
import { FileSystemService } from "@batch-flask/electron";
import { File, FileLoadOptions, FileLoader, FileNavigator, FileLoadResult } from "@batch-flask/ui";
import { File as FileRecord, FileLoadOptions, FileLoader, FileNavigator, FileLoadResult } from "@batch-flask/ui";
import { CloudPathUtils, log } from "@batch-flask/utils";
import { StorageEntityGetter, StorageListGetter } from "app/services/core";
import { ListBlobOptions, SharedAccessPolicy, StorageBlobResult, UploadFileResult } from "app/services/storage/models";
@ -86,12 +86,9 @@ export class InvalidSasUrlError extends Error {
// Regex to extract the host, container and blob from a sasUrl
const storageBlobUrlRegex = /^(https:\/\/[\w\._\-]+)\/([\w\-_]+)\/([\w\-_.]+)\?(.*)$/i;
function createBlobClient(params: CreateBlobParams) {
const containerClient = new ContainerClient(
params.accountUrl + params.sasToken,
params.container
);
return containerClient.getBlockBlobClient(params.blob);
function createBlobClient(url: string, blob: string) {
const containerClient = new ContainerClient(url);
return containerClient.getBlockBlobClient(blob);
}
@Injectable({ providedIn: "root" })
@ -99,26 +96,26 @@ export class StorageBlobService {
public maxBlobPageSize: number = 200; // 500 slows down the UI too much.
public maxContainerPageSize: number = 100;
private _blobListCache = new TargetedDataCache<ListBlobParams, File>({
private _blobListCache = new TargetedDataCache<ListBlobParams, FileRecord>({
key: ({ storageAccountId, container }) => `${storageAccountId}/${container}`,
}, "name");
private _blobGetter: StorageEntityGetter<File, BlobFileParams>;
private _blobGetter: StorageEntityGetter<FileRecord, BlobFileParams>;
private _blobListGetter: StorageListGetter<File, ListBlobParams>;
private _blobListGetter: StorageListGetter<FileRecord, ListBlobParams>;
constructor(
private storageClient: StorageClientService,
private fs: FileSystemService,
private zone: NgZone) {
this._blobGetter = new StorageEntityGetter(File, this.storageClient, {
this._blobGetter = new StorageEntityGetter(FileRecord, this.storageClient, {
cache: (params) => this.getBlobFileCache(params),
getFn: (client, params: BlobFileParams) =>
client.getBlobProperties(params.container, params.blobName, params.blobPrefix),
});
this._blobListGetter = new StorageListGetter(File, this.storageClient, {
this._blobListGetter = new StorageListGetter(FileRecord, this.storageClient, {
cache: (params) => this.getBlobFileCache(params),
getData: (client: BlobStorageClientProxy,
params, options, continuationToken) => {
@ -129,23 +126,23 @@ export class StorageBlobService {
maxPageSize: this.maxBlobPageSize
};
// N.B. `BlobItem` and `File` are nearly identical
// N.B. `BlobItem` and `FileRecord` are nearly identical
return client.listBlobs(
params.container,
blobOptions,
continuationToken,
) as Promise<StorageBlobResult<File[]>>;
) as Promise<StorageBlobResult<FileRecord[]>>;
},
logIgnoreError: storageIgnoredErrors,
});
}
public getBlobFileCache(params: ListBlobParams): DataCache<File> {
public getBlobFileCache(params: ListBlobParams): DataCache<FileRecord> {
return this._blobListCache.getCache(params);
}
public listView(storageAccountId: string, container: string, options: ListBlobOptions = {})
: ListView<File, ListBlobParams> {
: ListView<FileRecord, ListBlobParams> {
const view = new ListView({
cache: (params) => this.getBlobFileCache(params),
@ -160,7 +157,7 @@ export class StorageBlobService {
storageAccountId: string,
container: string,
options: ListBlobOptions = {},
forceNew = false): Observable<ListResponse<File>> {
forceNew = false): Observable<ListResponse<FileRecord>> {
return this._blobListGetter.fetch({ storageAccountId, container }, options, forceNew);
}
@ -192,11 +189,11 @@ export class StorageBlobService {
* @param blobName - Name of the blob, not including prefix
* @param blobPrefix - Optional prefix of the blob, i.e. {container}/{blobPrefix}+{blobName}
*/
public get(storageAccountId: string, container: string, blobName: string, blobPrefix?: string): Observable<File> {
public get(storageAccountId: string, container: string, blobName: string, blobPrefix?: string): Observable<FileRecord> {
return this._blobGetter.fetch({ storageAccountId, container, blobName, blobPrefix });
}
public blobView(): EntityView<File, BlobFileParams> {
public blobView(): EntityView<FileRecord, BlobFileParams> {
return new EntityView({
cache: (params) => this.getBlobFileCache(params),
getter: this._blobGetter,
@ -295,13 +292,15 @@ export class StorageBlobService {
});
}
public uploadToSasUrl(sasUrl: string, filePath: string): Observable<any> {
public uploadToSasUrl(sasUrl: string, file: File): Observable<any> {
const subject = new AsyncSubject<UploadFileResult>();
const blobParams = this._parseSasUrl(sasUrl);
const blobClient = createBlobClient(blobParams);
const { accountUrl, sasToken, container, blob } = blobParams;
const urlForClient = `${accountUrl}/${container}?${sasToken}`;
const blobClient = createBlobClient(urlForClient, blob);
this.zone.run(() => {
blobClient.uploadFile(filePath)
blobClient.uploadData(file)
.then(result => {
subject.next(result);
}).catch(error => subject.error(ServerError.fromStorage(error))

Просмотреть файл

@ -61,8 +61,7 @@ export class ThemeService implements OnDestroy {
});
this._themesLoadPath = [
path.join(batchExplorer.resourcesFolder, "data", "themes"),
path.join(fs.commonFolders.userData, "themes"),
path.join(batchExplorer.resourcesFolder, "data", "themes")
];
}

Просмотреть файл

@ -70,7 +70,7 @@ fieldset {
display: flex;
flex-flow: row nowrap;
padding: 0;
border: 1px solid #aaa;
border: 1px solid $border-color;
background-color: $main-background;
margin-top: 16px;

Просмотреть файл

@ -9,7 +9,7 @@
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue.
$mat-primary: mat-palette($mat-indigo);
$mat-accent: mat-palette($mat-grey, A200, A100, A400);
$mat-accent: mat-palette($mat-grey, A200, A100, A400);
// Create the theme object (a Sass map containing all of the palettes).
$mat-theme: mat-light-theme($mat-primary, $mat-accent);
@ -30,6 +30,11 @@ $mat-theme: mat-light-theme($mat-primary, $mat-accent);
mat-tab-group {
.mat-tab-label {
opacity: 1;
color: $secondary-text;
}
.mat-tab-label-active {
color: $primary-text;
}
}
@ -41,9 +46,6 @@ mat-tab-group:not(.form-tabs) {
height: 32px;
line-height: 32px;
&.mat-tab-label-active {
}
&.mat-tab-disabled {
color: $button-disabled-text-color;
}
@ -72,13 +74,22 @@ mat-icon.mat-icon {
mat-radio-button {
margin-right: 15px;
&.cdk-keyboard-focused {
.mat-focus-indicator {
border-radius: 100%;
background-color: $primary-color-light;
opacity: 0.25;
z-index: -1;
}
}
&.mat-radio-checked {
.mat-radio-outer-circle {
border-color: $primary-color;
border-color: $primary-color !important;
}
.mat-radio-inner-circle {
background: $primary-color;
background: $primary-color !important;
}
}
}
@ -128,7 +139,7 @@ bl-form-field.bl-textarea {
}
.mat-autocomplete-panel {
max-width : 30vw;
max-width: 30vw;
.mat-option {
height: 24px;
@ -171,17 +182,17 @@ mat-button-toggle-group {
}
}
.mat-drawer-container, .mat-drawer {
.mat-drawer-container,
.mat-drawer {
color: $primary-text;
background-color: $main-background;
}
.mat-menu-item {
height: 30px !important;
line-height: 30px !important;
&.cdk-program-focused, &.cdk-keyboard-focused {
&.cdk-program-focused,
&.cdk-keyboard-focused {
border: 1px solid $outline-color;
}
}

Просмотреть файл

@ -162,7 +162,7 @@ export class PoolUtils {
if (pool.virtualMachineConfiguration) {
const config = pool.virtualMachineConfiguration;
if (!config.imageReference) {
return "Unkown";
return "Unknown";
}
if (config.imageReference.virtualMachineImageId) {

Просмотреть файл

@ -1,4 +1,4 @@
import * as download from "download";
import { DownloaderHelper } from "node-downloader-helper";
import * as extract from "extract-zip";
import * as path from "path";
@ -9,9 +9,18 @@ import * as path from "path";
*/
export class FileUtils {
public download(source: string, dest: string): Promise<string> {
return download(source, path.dirname(dest), {
filename: path.basename(dest),
}).then(() => dest);
const downloader = new DownloaderHelper(source, path.dirname(dest), {
fileName: path.basename(dest)
});
return new Promise((resolve, reject) => {
downloader.on("end", () => {
resolve(dest);
});
downloader.on("error", (err) => {
reject(err);
});
downloader.start();
});
}
public unzip(source: string, dest: string): Promise<void> {

Просмотреть файл

@ -1,4 +1,5 @@
import { StorageBlobAdapter } from "./storage-blob-adapter";
import { isNode } from "@azure/core-http";
describe("StorageBlobAdapter", () => {
let adapter: StorageBlobAdapter;
@ -114,6 +115,9 @@ describe("StorageBlobAdapter", () => {
);
});
});
it("isNode from @azure/core-http should be true in the main process", () => {
expect(isNode).toBe(true);
});
});
const indexes = {

Просмотреть файл

@ -0,0 +1,23 @@
import { axe } from "jasmine-axe";
import { AxeResults, RunOptions } from "axe-core";
/**
* Runs the globally configured axe function
*/
export async function runAxe(
html: Element,
options?: RunOptions
): Promise<AxeResults> {
if (!process.env.BE_ENABLE_A11Y_TESTING) {
// Accessibility testing is disabled. Return a fake AxeResults object which
// always has no violations
return {
passes: [],
violations: [],
incomplete: [],
inapplicable: [],
} as unknown as AxeResults;
}
return axe(html, options) as Promise<AxeResults>;
}

Просмотреть файл

@ -1,7 +1,9 @@
import * as immutableMatchers from "./immutable-matchers";
import * as miscMatchers from "./misc-matchers";
import { toHaveNoViolations } from "jasmine-axe";
beforeEach(() => {
jasmine.addMatchers(immutableMatchers.matchers);
jasmine.addMatchers(miscMatchers.matchers);
jasmine.addMatchers(toHaveNoViolations);
});