зеркало из https://github.com/Azure/BatchExplorer.git
Merge remote-tracking branch 'origin/master' into shpaster/merge-master
This commit is contained in:
Коммит
83f4a16385
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче