diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index e8966910e5..68cd00e034 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -44,20 +44,13 @@
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
+ "editor.formatOnPaste": true,
+ "editor.formatOnSave": true,
"python.pythonPath": "/usr/local/bin/python",
- "python.linting.enabled": true,
- "python.linting.pylintEnabled": false,
- "python.linting.flake8Enabled": true,
"python.formatting.provider": "black",
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
- "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
- "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
- "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
- "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
- "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
- "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": [
@@ -276,6 +269,7 @@
"ms-python.python",
"ms-python.pylance",
"ms-python.flake8",
+ "nwgh.bandit",
"hashicorp.terraform",
"github.vscode-pull-request-github",
"gitHub.copilot",
@@ -295,5 +289,7 @@
],
// Run commands after the container is created.
"postCreateCommand": "./.devcontainer/scripts/post-create.sh",
- "initializeCommand": ["./.devcontainer/scripts/initialize"]
+ "initializeCommand": [
+ "./.devcontainer/scripts/initialize"
+ ]
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e015c950ff..c596b48c02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ ENHANCEMENTS:
* Subnet definitions are now inline in the `azurerm_virtual_network` resource, and NSG associations are set using `security_group` in each subnet block (no separate `azurerm_subnet_network_security_group_association` needed). ([[#4255](https://github.com/microsoft/AzureTRE/pull/4255/)])
* Azure Cosmos DB should disable public network access ([#4322](https://github.com/microsoft/AzureTRE/issues/4322))
* Add bundle target to Makefile for handling different bundle types in single command ([#4372](https://github.com/microsoft/AzureTRE/issues/4372))
+* Migrate UI to Vite build engine and update dependencies ([#4368](https://github.com/microsoft/AzureTRE/pull/4368))
* Add Windows image field to the Admin VM template ([#4274](https://github.com/microsoft/AzureTRE/pull/4274))
* Update TLS to the latest version for web apps / function apps (([#4351](https://github.com/microsoft/AzureTRE/issues/4351))
diff --git a/docs/tre-developers/ui.md b/docs/tre-developers/ui.md
index e9b4a04441..872a883d5f 100644
--- a/docs/tre-developers/ui.md
+++ b/docs/tre-developers/ui.md
@@ -4,7 +4,7 @@ This project contains a React-based web UI which covers the core aspects of a TR
## Chosen UI Stack + Components
The UI is built upon several popular web frameworks:
-- React v18 (created via create-react-app, with all build configurations left as defaults)
+- React v18 (with Vite)
- Typescript
- React Router v6 for client side routing
- Fluent UI [Fluent UI Docs](https://developer.microsoft.com/en-us/fluentui#/controls/web)
@@ -54,4 +54,53 @@ The UI is deployed as part of the `tre-deploy` make target (unless you set `depl
To re-deploy _just_ the UI (after an initial deploy), run `make build-and-deploy-ui` from the root of the dev container. This will:
- Use the environment variables from your deployment to create a `config.json` file for the UI
- Build the source code, via `yarn build`
-- Deploy the code to Azure blob storage, where it will be statically served behind the App Gateway that also fronts the APi.
+- Deploy the code to Azure blob storage, where it will be statically served behind the App Gateway that also fronts the API.
+
+## Run the UI
+- Ensure `deploy_ui=false` is not set in your `./config.yaml` file
+- In the root of the repo, run `make tre-deploy`. This will provision the necessary resources in Azure, build and deploy the UI to Azure blob storage, behind the App Gateway used for the API. The deployment process will also create the necessary `config.json`, using the `config.source.json` as a template.
+- In Microsoft Entra ID, locate the TRE Client Apps app (possibly called Swagger App). In the Authentication section add reply URIs for:
+ - `http://localhost:3000` (if wanting to run locally)
+ - Your deployed App Url - `https://{TRE_ID}.{LOCATION}.cloudapp.azure.com`.
+
+At this point you should be able to navigate to the web app in Azure, log in, and see your workspaces.
+
+## Available Scripts
+
+In the UI directory, you can run:
+
+### `yarn start`
+
+Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.
+You will also see any lint errors in the console.
+
+### `yarn test`
+
+Launches the test runner in the interactive watch mode.
+
+### `yarn run build`
+
+Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.
+Your app is ready to be deployed!
+
+### `yarn run serve`
+
+Serves the production build from the `build` folder.
+
+### `yarn run test:coverage`
+
+Runs the tests and generates a coverage report.
+
+### `yarn lint`
+
+Runs the linter on the project.
+
+### `yarn format`
+
+Runs the formatter on the project.
diff --git a/ui/README.md b/ui/README.md
deleted file mode 100644
index b605e4bc71..0000000000
--- a/ui/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# TRE UI
-
-Please see the docs for a full overview and deployment instructions.
-
-The UI was built using Create React App and Microsoft Fluent UI. Further details on this in the ./app/README.
-
-## Run the UI
-- Ensure `deploy_ui=false` is not set in your `./config.yaml` file
-- In the root of the repo, run `make tre-deploy`. This will provision the necessary resources in Azure, build and deploy the UI to Azure blob storage, behind the App Gateway used for the API. The deployment process will also create the necessary `config.json`, using the `config.source.json` as a template.
-- In Microsoft Entra ID, locate the TRE Client Apps app (possibly called Swagger App). In the Authentication section add reply URIs for:
- - `http://localhost:3000` (if wanting to run locally)
- - Your deployed App Url - `https://{TRE_ID}.{LOCATION}.cloudapp.azure.com`.
-
-At this point you should be able to navigate to the web app in Azure, log in, and see your workspaces.
-
-### To run locally
-- `cd ./ui/app`
-- `yarn start`
-
-After making changes to the code, redeploy to Azure by running `make build-and-deploy-ui` in the root of the dev container.
diff --git a/ui/app/.prettierrc b/ui/app/.prettierrc
new file mode 100644
index 0000000000..dc6958febb
--- /dev/null
+++ b/ui/app/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "singleQuote": false,
+ "semi": true
+}
diff --git a/ui/app/README.md b/ui/app/README.md
deleted file mode 100644
index 387b475e1e..0000000000
--- a/ui/app/README.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Getting Started with Create React App and Fluent UI
-
-This is a [Create React App](https://github.com/facebook/create-react-app) based repo that comes with Fluent UI pre-installed!
-
-## Available Scripts
-
-In the project directory, you can run:
-
-### `yarn start`
-
-Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
-
-The page will reload if you make edits.
-You will also see any lint errors in the console.
-
-### `yarn test`
-
-Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
-
-### `yarn build`
-
-Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance.
-
-The build is minified and the filenames include the hashes.
-Your app is ready to be deployed!
-
-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
-
-### `yarn eject`
-
-**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
-
-If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
-
-Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
-
-You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
-
-## Learn More
-
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
-
-To learn React, check out the [React documentation](https://reactjs.org/).
-
-## Contributing
-
-This project welcomes contributions and suggestions. Most contributions require you to agree to a
-Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
-the rights to use your contribution. For details, visit [CLA](https://cla.microsoft.com).
-
-When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
-a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
-provided by the bot. You will only need to do this once across all repos using our CLA.
-
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
-For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
-contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
diff --git a/ui/app/eslint.config.js b/ui/app/eslint.config.js
new file mode 100644
index 0000000000..c61fac8c72
--- /dev/null
+++ b/ui/app/eslint.config.js
@@ -0,0 +1,24 @@
+import eslintConfigPrettier from "eslint-config-prettier";
+import typescriptEslint from "@typescript-eslint/eslint-plugin";
+import typescriptParser from "@typescript-eslint/parser";
+
+export default [
+ {
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ parser: typescriptParser,
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ plugins: {
+ "@typescript-eslint": typescriptEslint,
+ },
+ rules: {},
+ },
+ eslintConfigPrettier,
+];
diff --git a/ui/app/index.html b/ui/app/index.html
new file mode 100644
index 0000000000..119f64be91
--- /dev/null
+++ b/ui/app/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+ Azure TRE
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/ui/app/package.json b/ui/app/package.json
index eec9d31ead..9d8321c24a 100644
--- a/ui/app/package.json
+++ b/ui/app/package.json
@@ -2,6 +2,7 @@
"name": "tre-ui",
"version": "0.7.0",
"private": true,
+ "type": "module",
"dependencies": {
"@azure/msal-browser": "^2.35.0",
"@azure/msal-react": "^1.5.12",
@@ -12,16 +13,11 @@
"@rjsf/fluent-ui": "^5.24.3",
"@rjsf/utils": "^5.24.3",
"@rjsf/validator-ajv8": "^5.24.3",
- "@testing-library/dom": "^7.21.4",
- "@testing-library/jest-dom": "^6.2.0",
- "@testing-library/react": "^14.0.0",
- "@testing-library/user-event": "^14.6.1",
- "@types/jest": "^29.5.0",
"@types/node": "^20.17.14",
"@types/react": "^18.3.16",
"@types/react-dom": "^18.2.6",
+ "@vitejs/plugin-react-swc": "latest",
"moment": "^2.29.4",
- "node-sass": "^8.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^8.0.3",
@@ -29,26 +25,43 @@
"react-router-dom": "6.28.2",
"remark-gfm": "^3.0.1",
"typescript": "^5.6.3",
+ "vite": "latest",
+ "vite-plugin-eslint": "^1.8.1",
+ "vite-plugin-svgr": "latest",
+ "vite-tsconfig-paths": "latest",
"web-vitals": "^3.3.0"
},
"devDependencies": {
- "@babel/core": "^7.23.7",
- "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
- "@babel/plugin-syntax-flow": "^7.23.3",
- "@babel/plugin-transform-react-jsx": "^7.23.4",
- "react-scripts": "5.0.1"
+ "@testing-library/dom": "^7.21.4",
+ "@testing-library/jest-dom": "^6.2.0",
+ "@testing-library/react": "^14.0.0",
+ "@types/jest": "^29.5.0",
+ "@types/node": "^20.17.14",
+ "@types/react": "^18.3.16",
+ "@types/react-dom": "^18.2.6",
+ "@typescript-eslint/eslint-plugin": "^8.24.0",
+ "@typescript-eslint/parser": "^8.24.0",
+ "@vitest/coverage-v8": "latest",
+ "eslint": "^9.20.1",
+ "eslint-config-prettier": "^10.0.1",
+ "eslint-plugin-prettier": "^5.2.3",
+ "eslint-plugin-react": "^7.37.4",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.14.0",
+ "jsdom": "latest",
+ "prettier": "3.5.0",
+ "sass-embedded": "^1.83.4",
+ "vitest": "latest"
},
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
- "eject": "react-scripts eject"
- },
- "eslintConfig": {
- "extends": [
- "react-app",
- "react-app/jest"
- ]
+ "start": "vite",
+ "build": "tsc && vite build",
+ "serve": "vite preview",
+ "test": "vitest",
+ "test:coverage": "vitest run --coverage --watch=false",
+ "lint": "eslint .",
+ "format": "prettier --write ."
},
"browserslist": {
"production": [
diff --git a/ui/app/public/index.html b/ui/app/public/index.html
index 3412482b4e..493346824c 100644
--- a/ui/app/public/index.html
+++ b/ui/app/public/index.html
@@ -1,15 +1,18 @@
-
+
-
+
-
+
-
+
Azure TRE
diff --git a/ui/app/src/App.scss b/ui/app/src/App.scss
index 704e6dfeb0..0f7c87cd8b 100644
--- a/ui/app/src/App.scss
+++ b/ui/app/src/App.scss
@@ -1,7 +1,8 @@
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
- 'Droid Sans', 'Helvetica Neue', sans-serif;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
+ "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -16,7 +17,8 @@ h2 {
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+ font-family:
+ source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
.tre-logout-message {
@@ -88,7 +90,7 @@ ul.tre-notifications-steps-list li {
.tre-home-link {
color: #fff;
text-decoration: none;
- font-size:1.2rem;
+ font-size: 1.2rem;
}
.tre-user-menu {
@@ -118,7 +120,7 @@ ul.tre-notifications-steps-list li {
}
}
-.tre-hide-chevron i[data-icon-name=ChevronDown] {
+.tre-hide-chevron i[data-icon-name="ChevronDown"] {
display: none;
}
@@ -174,50 +176,50 @@ ul.tre-notifications-steps-list li {
margin-bottom: 10px;
}
-input[readonly]{
- background-color:#efefef;
+input[readonly] {
+ background-color: #efefef;
}
-.tre-badge{
- border-radius:4px;
+.tre-badge {
+ border-radius: 4px;
background-color: #efefef;
- padding:2px 6px;
+ padding: 2px 6px;
text-transform: capitalize;
- display:inline-block;
- font-size:12px;
+ display: inline-block;
+ font-size: 12px;
}
-.tre-badge-in-progress{
+.tre-badge-in-progress {
background-color: #ce7b00;
color: #fff;
}
-.tre-badge-failed{
+.tre-badge-failed {
background-color: #990000;
color: #fff;
padding-top: 4px;
padding-left: 7px;
font-size: 16px;
}
-.tre-badge-success{
+.tre-badge-success {
background-color: #006600;
color: #fff;
}
-.tre-complex-list{
+.tre-complex-list {
list-style: none;
- padding:0 0 0 20px;
- margin:0;
+ padding: 0 0 0 20px;
+ margin: 0;
}
-.tre-complex-list-border{
+.tre-complex-list-border {
border-bottom: 1px #ccc solid;
- margin-left:-15px;
+ margin-left: -15px;
}
-.tre-complex-list-string{
- padding-left:20px;
+.tre-complex-list-string {
+ padding-left: 20px;
}
-.tre-complex-list .ms-Icon{
- font-size:12px!important;
+.tre-complex-list .ms-Icon {
+ font-size: 12px !important;
font-weight: bold;
position: relative;
- top:2px;
+ top: 2px;
}
// Classes for rendering power state badges
@@ -226,7 +228,8 @@ input[readonly]{
color: #636262;
margin: 6px;
- .tre-power-on, .tre-power-off {
+ .tre-power-on,
+ .tre-power-off {
height: 8px;
width: 8px;
background-color: #006600;
@@ -267,28 +270,81 @@ input[readonly]{
}
/* border around sub-blocks */
-.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object,
-.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-array {
+.ms-Panel-content
+ .rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-object,
+.ms-Panel-content
+ .rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-array {
border: 1px #ccc dashed;
padding: 10px;
background-color: #fcfcfc;
}
/* sub titles and sub-sub titles */
-.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > label.ms-Label,
-.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-array > label.ms-Label {
+.ms-Panel-content
+ .rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-object
+ > label.ms-Label,
+.ms-Panel-content
+ .rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-array
+ > label.ms-Label {
font-size: 20px;
}
-.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > .ms-Grid > .ms-Grid-row > .ms-Grid-col > label.ms-Label,
-.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-array > .ms-Grid > .ms-Grid-row > .ms-Grid-col > label.ms-Label {
+.ms-Panel-content
+ .rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-object
+ > .ms-Grid
+ > .ms-Grid-row
+ > .ms-Grid-col
+ > label.ms-Label,
+.ms-Panel-content
+ .rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-array
+ > .ms-Grid
+ > .ms-Grid-row
+ > .ms-Grid-col
+ > label.ms-Label {
font-size: 16px;
}
/* remove secondary template description at the bottom of each template + sub blocks */
.rjsf > .ms-Grid-col > span:last-of-type,
-.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > span:last-of-type,
-.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > .ms-Grid > .ms-Grid-row > .ms-Grid-col > span:last-of-type {
+.rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-object
+ > span:last-of-type,
+.rjsf
+ > .ms-Grid-col
+ > .ms-Grid
+ > .ms-Grid-row
+ > .field-object
+ > .ms-Grid
+ > .ms-Grid-row
+ > .ms-Grid-col
+ > span:last-of-type {
display: none;
}
diff --git a/ui/app/src/App.test.tsx b/ui/app/src/App.test.tsx
index ac873a8bce..d55194710b 100644
--- a/ui/app/src/App.test.tsx
+++ b/ui/app/src/App.test.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { App } from './App';
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { App } from "./App";
it('renders "Welcome to Your Fluent UI App"', () => {
render( );
diff --git a/ui/app/src/App.tsx b/ui/app/src/App.tsx
index 8f21c2d260..24d0b5220d 100644
--- a/ui/app/src/App.tsx
+++ b/ui/app/src/App.tsx
@@ -1,45 +1,65 @@
-import React, { useEffect, useState } from 'react';
-import { DefaultPalette, IStackStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
-import './App.scss';
-import { TopNav } from './components/shared/TopNav';
-import { Routes, Route } from 'react-router-dom';
-import { RootLayout } from './components/root/RootLayout';
-import { WorkspaceProvider } from './components/workspaces/WorkspaceProvider';
-import { MsalAuthenticationTemplate } from '@azure/msal-react';
-import { InteractionType } from '@azure/msal-browser';
-import { Workspace } from './models/workspace';
-import { AppRolesContext } from './contexts/AppRolesContext';
-import { WorkspaceContext } from './contexts/WorkspaceContext';
-import { GenericErrorBoundary } from './components/shared/GenericErrorBoundary';
-import { HttpMethod, ResultType, useAuthApiCall } from './hooks/useAuthApiCall';
-import { ApiEndpoint } from './models/apiEndpoints';
-import { CreateUpdateResource } from './components/shared/create-update-resource/CreateUpdateResource';
-import { CreateUpdateResourceContext } from './contexts/CreateUpdateResourceContext';
-import { CreateFormResource, ResourceType } from './models/resourceType';
-import { Footer } from './components/shared/Footer';
-import { initializeFileTypeIcons } from '@fluentui/react-file-type-icons';
-import { CostResource } from './models/costs';
-import { CostsContext } from './contexts/CostsContext';
-import { LoadingState } from './models/loadingState';
+import React, { useEffect, useState } from "react";
+import {
+ DefaultPalette,
+ IStackStyles,
+ MessageBar,
+ MessageBarType,
+ Stack,
+} from "@fluentui/react";
+import "./App.scss";
+import { TopNav } from "./components/shared/TopNav";
+import { Routes, Route } from "react-router-dom";
+import { RootLayout } from "./components/root/RootLayout";
+import { WorkspaceProvider } from "./components/workspaces/WorkspaceProvider";
+import { MsalAuthenticationTemplate } from "@azure/msal-react";
+import { InteractionType } from "@azure/msal-browser";
+import { Workspace } from "./models/workspace";
+import { AppRolesContext } from "./contexts/AppRolesContext";
+import { WorkspaceContext } from "./contexts/WorkspaceContext";
+import { GenericErrorBoundary } from "./components/shared/GenericErrorBoundary";
+import { HttpMethod, ResultType, useAuthApiCall } from "./hooks/useAuthApiCall";
+import { ApiEndpoint } from "./models/apiEndpoints";
+import { CreateUpdateResource } from "./components/shared/create-update-resource/CreateUpdateResource";
+import { CreateUpdateResourceContext } from "./contexts/CreateUpdateResourceContext";
+import { CreateFormResource, ResourceType } from "./models/resourceType";
+import { Footer } from "./components/shared/Footer";
+import { initializeFileTypeIcons } from "@fluentui/react-file-type-icons";
+import { CostResource } from "./models/costs";
+import { CostsContext } from "./contexts/CostsContext";
+import { LoadingState } from "./models/loadingState";
export const App: React.FunctionComponent = () => {
const [appRoles, setAppRoles] = useState([] as Array);
const [selectedWorkspace, setSelectedWorkspace] = useState({} as Workspace);
const [workspaceRoles, setWorkspaceRoles] = useState([] as Array);
- const [workspaceCosts, setWorkspaceCosts] = useState([] as Array);
+ const [workspaceCosts, setWorkspaceCosts] = useState(
+ [] as Array,
+ );
const [costs, setCosts] = useState([] as Array);
- const [costsLoadingState, setCostsLoadingState] = useState(LoadingState.Loading);
+ const [costsLoadingState, setCostsLoadingState] = useState(
+ LoadingState.Loading,
+ );
const [createFormOpen, setCreateFormOpen] = useState(false);
- const [createFormResource, setCreateFormResource] = useState({ resourceType: ResourceType.Workspace } as CreateFormResource);
+ const [createFormResource, setCreateFormResource] = useState({
+ resourceType: ResourceType.Workspace,
+ } as CreateFormResource);
const apiCall = useAuthApiCall();
// set the app roles
useEffect(() => {
const setAppRolesOnLoad = async () => {
- await apiCall(ApiEndpoint.Workspaces, HttpMethod.Get, undefined, undefined, ResultType.JSON, (roles: Array) => {
- setAppRoles(roles);
- }, true);
+ await apiCall(
+ ApiEndpoint.Workspaces,
+ HttpMethod.Get,
+ undefined,
+ undefined,
+ ResultType.JSON,
+ (roles: Array) => {
+ setAppRoles(roles);
+ },
+ true,
+ );
};
setAppRolesOnLoad();
}, [apiCall]);
@@ -49,77 +69,113 @@ export const App: React.FunctionComponent = () => {
return (
<>
-
- ) => { setAppRoles(roles) }
- }}>
- {
- setCreateFormResource(createFormResource);
- setCreateFormOpen(true);
- }
- }} >
-
- setCreateFormOpen(false)}
- resourceType={createFormResource.resourceType}
- parentResource={createFormResource.resourceParent}
- onAddResource={createFormResource.onAdd}
- workspaceApplicationIdURI={createFormResource.workspaceApplicationIdURI}
- updateResource={createFormResource.updateResource}
- />
-
-
-
-
-
-
- ) => {setCosts(costs)},
- setLoadingState: (loadingState: LoadingState) => {setCostsLoadingState(loadingState)}
- }}>
-
- } />
- ) => {setWorkspaceRoles(roles)},
- costs: workspaceCosts,
- setCosts: (costs: Array) => {setWorkspaceCosts(costs)},
- workspace: selectedWorkspace,
- setWorkspace: (w: Workspace) => {setSelectedWorkspace(w)},
- workspaceApplicationIdURI: selectedWorkspace.properties?.scope_id
- }}>
-
-
- } />
-
-
-
-
-
-
-
-
-
-
-
- } />
-
-
- You are logged out.
- It's a good idea to close your browser windows.
-
- } />
+ ) => {
+ setAppRoles(roles);
+ },
+ }}
+ >
+ {
+ setCreateFormResource(createFormResource);
+ setCreateFormOpen(true);
+ },
+ }}
+ >
+ setCreateFormOpen(false)}
+ resourceType={createFormResource.resourceType}
+ parentResource={createFormResource.resourceParent}
+ onAddResource={createFormResource.onAdd}
+ workspaceApplicationIdURI={
+ createFormResource.workspaceApplicationIdURI
+ }
+ updateResource={createFormResource.updateResource}
+ />
+
+
+
+
+
+
+ ) => {
+ setCosts(costs);
+ },
+ setLoadingState: (loadingState: LoadingState) => {
+ setCostsLoadingState(loadingState);
+ },
+ }}
+ >
+
+ } />
+ ) => {
+ setWorkspaceRoles(roles);
+ },
+ costs: workspaceCosts,
+ setCosts: (costs: Array) => {
+ setWorkspaceCosts(costs);
+ },
+ workspace: selectedWorkspace,
+ setWorkspace: (w: Workspace) => {
+ setSelectedWorkspace(w);
+ },
+ workspaceApplicationIdURI:
+ selectedWorkspace.properties?.scope_id,
+ }}
+ >
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ You are logged out.
+ It's a good idea to close your browser windows.
+
+
+ }
+ />
>
);
@@ -128,12 +184,10 @@ export const App: React.FunctionComponent = () => {
const stackStyles: IStackStyles = {
root: {
background: DefaultPalette.white,
- height: '100vh',
+ height: "100vh",
},
};
export const Admin: React.FunctionComponent = () => {
- return (
- Admin (wip)
- )
-}
+ return Admin (wip) ;
+};
diff --git a/ui/app/src/authConfig.ts b/ui/app/src/authConfig.ts
index 346f6ede65..386416cdb5 100644
--- a/ui/app/src/authConfig.ts
+++ b/ui/app/src/authConfig.ts
@@ -1,14 +1,14 @@
-import { Configuration, PublicClientApplication } from "@azure/msal-browser";
-import config from "./config.json"
+import { Configuration, PublicClientApplication } from "@azure/msal-browser";
+import config from "./config.json";
// MSAL configuration
const configuration: Configuration = {
- auth: {
- clientId: config.rootClientId,
- authority: `${config.activeDirectoryUri}/${config.rootTenantId}`,
- redirectUri: `${window.location.protocol}//${window.location.hostname}:${window.location.port}`,
- postLogoutRedirectUri: `${window.location.protocol}//${window.location.hostname}:${window.location.port}/logout`
- }
+ auth: {
+ clientId: config.rootClientId,
+ authority: `${config.activeDirectoryUri}/${config.rootTenantId}`,
+ redirectUri: `${window.location.protocol}//${window.location.hostname}:${window.location.port}`,
+ postLogoutRedirectUri: `${window.location.protocol}//${window.location.hostname}:${window.location.port}/logout`,
+ },
};
export const pca = new PublicClientApplication(configuration);
diff --git a/ui/app/src/components/root/LeftNav.tsx b/ui/app/src/components/root/LeftNav.tsx
index 706cc8c8b1..ea3eaf857c 100644
--- a/ui/app/src/components/root/LeftNav.tsx
+++ b/ui/app/src/components/root/LeftNav.tsx
@@ -1,61 +1,62 @@
-import React, { useContext } from 'react';
-import { Nav, INavLinkGroup } from '@fluentui/react/lib/Nav';
-import { useLocation, useNavigate } from 'react-router-dom';
-import { AppRolesContext } from '../../contexts/AppRolesContext';
-import { RoleName } from '../../models/roleNames';
+import React, { useContext } from "react";
+import { Nav, INavLinkGroup } from "@fluentui/react/lib/Nav";
+import { useLocation, useNavigate } from "react-router-dom";
+import { AppRolesContext } from "../../contexts/AppRolesContext";
+import { RoleName } from "../../models/roleNames";
export const LeftNav: React.FunctionComponent = () => {
const navigate = useNavigate();
const location = useLocation();
const appRolesCtx = useContext(AppRolesContext);
- const isRequestsRoute = location.pathname.startsWith('/requests'); // ← True if URL starts with /requests
+ const isRequestsRoute = location.pathname.startsWith("/requests"); // ← True if URL starts with /requests
const navLinkGroups: INavLinkGroup[] = [
{
links: [
{
- name: 'Workspaces',
- url: '/',
- key: '/',
- icon: 'WebAppBuilderFragment'
- }
+ name: "Workspaces",
+ url: "/",
+ key: "/",
+ icon: "WebAppBuilderFragment",
+ },
],
},
];
// show shared-services link if TRE Admin
if (appRolesCtx.roles.includes(RoleName.TREAdmin)) {
- navLinkGroups[0].links.push(
- {
- name: 'Shared Services',
- url: '/shared-services',
- key: 'shared-services',
- icon: 'Puzzle'
- });
+ navLinkGroups[0].links.push({
+ name: "Shared Services",
+ url: "/shared-services",
+ key: "shared-services",
+ icon: "Puzzle",
+ });
}
- const requestsLinkArray: { name: string; url: string; key: string; icon: string }[] = [];
-
- requestsLinkArray.push(
- {
- name: 'Airlock',
- url: '/requests/airlock',
- key: 'airlock',
- icon: 'Lock',
+ const requestsLinkArray: {
+ name: string;
+ url: string;
+ key: string;
+ icon: string;
+ }[] = [];
- });
+ requestsLinkArray.push({
+ name: "Airlock",
+ url: "/requests/airlock",
+ key: "airlock",
+ icon: "Lock",
+ });
// add Requests link
- navLinkGroups[0].links.push(
- {
- name: 'Requests',
- url: '/requests',
- key: 'requests',
- icon: '',
- links: requestsLinkArray,
- isExpanded: isRequestsRoute
- });
+ navLinkGroups[0].links.push({
+ name: "Requests",
+ url: "/requests",
+ key: "requests",
+ icon: "",
+ links: requestsLinkArray,
+ isExpanded: isRequestsRoute,
+ });
return (
{
if (!item || !item.url) return;
item.isExpanded = true;
if (item.url !== "/requests") {
- navigate(item.url)
+ navigate(item.url);
}
}}
ariaLabel="TRE Left Navigation"
@@ -72,4 +73,3 @@ export const LeftNav: React.FunctionComponent = () => {
/>
);
};
-
diff --git a/ui/app/src/components/root/RootDashboard.tsx b/ui/app/src/components/root/RootDashboard.tsx
index 0daeaf71c7..99fbb6e592 100644
--- a/ui/app/src/components/root/RootDashboard.tsx
+++ b/ui/app/src/components/root/RootDashboard.tsx
@@ -1,23 +1,25 @@
-import React, { useContext } from 'react';
-import { Workspace } from '../../models/workspace';
+import React, { useContext } from "react";
+import { Workspace } from "../../models/workspace";
-import { ResourceCardList } from '../shared/ResourceCardList';
-import { Resource } from '../../models/resource';
-import { PrimaryButton, Stack } from '@fluentui/react';
-import { ResourceType } from '../../models/resourceType';
-import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
-import { RoleName } from '../../models/roleNames';
-import { SecuredByRole } from '../shared/SecuredByRole';
+import { ResourceCardList } from "../shared/ResourceCardList";
+import { Resource } from "../../models/resource";
+import { PrimaryButton, Stack } from "@fluentui/react";
+import { ResourceType } from "../../models/resourceType";
+import { CreateUpdateResourceContext } from "../../contexts/CreateUpdateResourceContext";
+import { RoleName } from "../../models/roleNames";
+import { SecuredByRole } from "../shared/SecuredByRole";
interface RootDashboardProps {
- selectWorkspace?: (workspace: Workspace) => void,
- workspaces: Array,
- updateWorkspace: (w: Workspace) => void,
- removeWorkspace: (w: Workspace) => void,
- addWorkspace: (w: Workspace) => void
+ selectWorkspace?: (workspace: Workspace) => void;
+ workspaces: Array;
+ updateWorkspace: (w: Workspace) => void;
+ removeWorkspace: (w: Workspace) => void;
+ addWorkspace: (w: Workspace) => void;
}
-export const RootDashboard: React.FunctionComponent = (props: RootDashboardProps) => {
+export const RootDashboard: React.FunctionComponent = (
+ props: RootDashboardProps,
+) => {
const createFormCtx = useContext(CreateUpdateResourceContext);
return (
@@ -25,25 +27,40 @@ export const RootDashboard: React.FunctionComponent = (props
- Workspaces
-
- {
- createFormCtx.openCreateForm({
- resourceType: ResourceType.Workspace,
- onAdd: (r: Resource) => props.addWorkspace(r as Workspace)
- })
- }} />
- } />
+
+ Workspaces
+
+
+ {
+ createFormCtx.openCreateForm({
+ resourceType: ResourceType.Workspace,
+ onAdd: (r: Resource) =>
+ props.addWorkspace(r as Workspace),
+ });
+ }}
+ />
+ }
+ />
props.updateWorkspace(r as Workspace)}
- removeResource={(r: Resource) => props.removeWorkspace(r as Workspace)}
- emptyText="No workspaces to display. Create one to get started." />
+ updateResource={(r: Resource) =>
+ props.updateWorkspace(r as Workspace)
+ }
+ removeResource={(r: Resource) =>
+ props.removeWorkspace(r as Workspace)
+ }
+ emptyText="No workspaces to display. Create one to get started."
+ />
>
diff --git a/ui/app/src/components/root/RootLayout.tsx b/ui/app/src/components/root/RootLayout.tsx
index 2084ce7a73..ab842b3672 100644
--- a/ui/app/src/components/root/RootLayout.tsx
+++ b/ui/app/src/components/root/RootLayout.tsx
@@ -1,28 +1,34 @@
-import { Spinner, SpinnerSize, Stack } from '@fluentui/react';
-import React, { useContext, useEffect, useRef, useState } from 'react';
-import { Route, Routes } from 'react-router-dom';
-import { Admin } from '../../App';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { Workspace } from '../../models/workspace';
-import { useAuthApiCall, HttpMethod, ResultType } from '../../hooks/useAuthApiCall';
-import { RootDashboard } from './RootDashboard';
-import { LeftNav } from './LeftNav';
-import { LoadingState } from '../../models/loadingState';
-import { RequestsList } from '../shared/RequestsList';
-import { SharedServices } from '../shared/SharedServices';
-import { SharedServiceItem } from '../shared/SharedServiceItem';
-import { SecuredByRole } from '../shared/SecuredByRole';
-import { RoleName } from '../../models/roleNames';
-import { APIError } from '../../models/exceptions';
-import { ExceptionLayout } from '../shared/ExceptionLayout';
-import { AppRolesContext } from '../../contexts/AppRolesContext';
-import { CostsContext } from '../../contexts/CostsContext';
+import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
+import React, { useContext, useEffect, useRef, useState } from "react";
+import { Route, Routes } from "react-router-dom";
+import { Admin } from "../../App";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { Workspace } from "../../models/workspace";
+import {
+ useAuthApiCall,
+ HttpMethod,
+ ResultType,
+} from "../../hooks/useAuthApiCall";
+import { RootDashboard } from "./RootDashboard";
+import { LeftNav } from "./LeftNav";
+import { LoadingState } from "../../models/loadingState";
+import { RequestsList } from "../shared/RequestsList";
+import { SharedServices } from "../shared/SharedServices";
+import { SharedServiceItem } from "../shared/SharedServiceItem";
+import { SecuredByRole } from "../shared/SecuredByRole";
+import { RoleName } from "../../models/roleNames";
+import { APIError } from "../../models/exceptions";
+import { ExceptionLayout } from "../shared/ExceptionLayout";
+import { AppRolesContext } from "../../contexts/AppRolesContext";
+import { CostsContext } from "../../contexts/CostsContext";
import config from "../../config.json";
export const RootLayout: React.FunctionComponent = () => {
const [workspaces, setWorkspaces] = useState([] as Array);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
- const [loadingCostState, setLoadingCostState] = useState(LoadingState.Loading);
+ const [loadingCostState, setLoadingCostState] = useState(
+ LoadingState.Loading,
+ );
const [apiError, setApiError] = useState({} as APIError);
const [costApiError, setCostApiError] = useState({} as APIError);
const apiCall = useAuthApiCall();
@@ -32,11 +38,17 @@ export const RootLayout: React.FunctionComponent = () => {
useEffect(() => {
const getWorkspaces = async () => {
try {
- const r = await apiCall(ApiEndpoint.Workspaces, HttpMethod.Get, undefined, undefined, ResultType.JSON);
+ const r = await apiCall(
+ ApiEndpoint.Workspaces,
+ HttpMethod.Get,
+ undefined,
+ undefined,
+ ResultType.JSON,
+ );
setLoadingState(LoadingState.Ok);
r && r.workspaces && setWorkspaces(r.workspaces);
} catch (e: any) {
- e.userMessage = 'Error retrieving resources';
+ e.userMessage = "Error retrieving resources";
setApiError(e);
setLoadingState(LoadingState.Error);
}
@@ -45,49 +57,54 @@ export const RootLayout: React.FunctionComponent = () => {
getWorkspaces();
}, [apiCall]);
-
useEffect(() => {
const getCosts = async () => {
try {
if (appRolesCtx.roles.includes(RoleName.TREAdmin)) {
- costsWriteCtx.current.setLoadingState(LoadingState.Loading)
- const r = await apiCall(ApiEndpoint.Costs, HttpMethod.Get, undefined, undefined, ResultType.JSON);
+ costsWriteCtx.current.setLoadingState(LoadingState.Loading);
+ const r = await apiCall(
+ ApiEndpoint.Costs,
+ HttpMethod.Get,
+ undefined,
+ undefined,
+ ResultType.JSON,
+ );
costsWriteCtx.current.setCosts([
...r.workspaces,
- ...r.shared_services
+ ...r.shared_services,
]);
- costsWriteCtx.current.setLoadingState(LoadingState.Ok)
+ costsWriteCtx.current.setLoadingState(LoadingState.Ok);
setLoadingCostState(LoadingState.Ok);
} else {
- costsWriteCtx.current.setLoadingState(LoadingState.AccessDenied)
+ costsWriteCtx.current.setLoadingState(LoadingState.AccessDenied);
setLoadingCostState(LoadingState.AccessDenied);
}
- }
- catch (e: any) {
+ } catch (e: any) {
if (e instanceof APIError) {
if (e.status === 404 /*subscription not supported*/) {
config.debug && console.warn(e.message);
setLoadingCostState(LoadingState.NotSupported);
- }
- else if (e.status === 429 /*too many requests*/ || e.status === 503 /*service unavaiable*/) {
+ } else if (
+ e.status === 429 /*too many requests*/ ||
+ e.status === 503 /*service unavaiable*/
+ ) {
let msg = JSON.parse(e.message);
let retryAfter = Number(msg.error["retry-after"]);
- config.debug && console.info("retrying after " + retryAfter + " seconds");
+ config.debug &&
+ console.info("retrying after " + retryAfter + " seconds");
setTimeout(getCosts, retryAfter * 1000);
- }
- else {
- e.userMessage = 'Error retrieving costs';
+ } else {
+ e.userMessage = "Error retrieving costs";
setLoadingCostState(LoadingState.Error);
}
- }
- else {
- e.userMessage = 'Error retrieving costs';
+ } else {
+ e.userMessage = "Error retrieving costs";
setLoadingCostState(LoadingState.Error);
}
- costsWriteCtx.current.setLoadingState(LoadingState.Error)
+ costsWriteCtx.current.setLoadingState(LoadingState.Error);
setCostApiError(e);
}
};
@@ -97,77 +114,111 @@ export const RootLayout: React.FunctionComponent = () => {
const ctx = costsWriteCtx.current;
// run this on unmount - to clear the context
- return (() => ctx.setCosts([]));
+ return () => ctx.setCosts([]);
}, [apiCall, appRolesCtx.roles]);
const addWorkspace = (w: Workspace) => {
- const ws = [...workspaces]
+ const ws = [...workspaces];
ws.push(w);
setWorkspaces(ws);
- }
+ };
const updateWorkspace = (w: Workspace) => {
const i = workspaces.findIndex((f: Workspace) => f.id === w.id);
- const ws = [...workspaces]
+ const ws = [...workspaces];
ws.splice(i, 1, w);
setWorkspaces(ws);
- }
+ };
const removeWorkspace = (w: Workspace) => {
const i = workspaces.findIndex((f: Workspace) => f.id === w.id);
const ws = [...workspaces];
ws.splice(i, 1);
setWorkspaces(ws);
- }
+ };
switch (loadingState) {
case LoadingState.Ok:
return (
<>
- {
- loadingCostState === LoadingState.Error &&
+ {loadingCostState === LoadingState.Error && (
- }
-
-
+ )}
+
+
-
+
+
- addWorkspace(w)}
- updateWorkspace={(w: Workspace) => updateWorkspace(w)}
- removeWorkspace={(w: Workspace) => removeWorkspace(w)} />
- } />
+ addWorkspace(w)}
+ updateWorkspace={(w: Workspace) => updateWorkspace(w)}
+ removeWorkspace={(w: Workspace) => removeWorkspace(w)}
+ />
+ }
+ />
} />
-
- } allowedAppRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"} />} />
- } allowedAppRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"} />} />
-
- } />
-
-
- } />
-
- } />
+
+ }
+ allowedAppRoles={[RoleName.TREAdmin]}
+ errorString={
+ "You must be a TRE Admin to access this area"
+ }
+ />
+ }
+ />
+ }
+ allowedAppRoles={[RoleName.TREAdmin]}
+ errorString={
+ "You must be a TRE Admin to access this area"
+ }
+ />
+ }
+ />
+
+ }
+ />
+
+
+ } />
+
+ }
+ />
>
);
case LoadingState.Error:
- return (
-
- );
+ return ;
default:
return (
-
-
+
+
);
}
-
};
diff --git a/ui/app/src/components/shared/CliCommand.tsx b/ui/app/src/components/shared/CliCommand.tsx
index 4230586b85..ffd7b4a353 100644
--- a/ui/app/src/components/shared/CliCommand.tsx
+++ b/ui/app/src/components/shared/CliCommand.tsx
@@ -1,78 +1,116 @@
import { IconButton, Spinner, Stack, TooltipHost } from "@fluentui/react";
import React, { useState } from "react";
-import { Text } from '@fluentui/react/lib/Text';
+import { Text } from "@fluentui/react/lib/Text";
interface CliCommandProps {
- command: string,
- title: string,
- isLoading: boolean
+ command: string;
+ title: string;
+ isLoading: boolean;
}
-export const CliCommand: React.FunctionComponent
= (props: CliCommandProps) => {
-
- const COPY_TOOL_TIP_DEFAULT_MESSAGE = "Copy to clipboard"
- const [copyToolTipMessage, setCopyToolTipMessage] = useState(COPY_TOOL_TIP_DEFAULT_MESSAGE);
+export const CliCommand: React.FunctionComponent = (
+ props: CliCommandProps,
+) => {
+ const COPY_TOOL_TIP_DEFAULT_MESSAGE = "Copy to clipboard";
+ const [copyToolTipMessage, setCopyToolTipMessage] = useState(
+ COPY_TOOL_TIP_DEFAULT_MESSAGE,
+ );
const handleCopyCommand = () => {
navigator.clipboard.writeText(props.command);
- setCopyToolTipMessage("Copied")
- setTimeout(() => setCopyToolTipMessage(COPY_TOOL_TIP_DEFAULT_MESSAGE), 3000);
- }
+ setCopyToolTipMessage("Copied");
+ setTimeout(
+ () => setCopyToolTipMessage(COPY_TOOL_TIP_DEFAULT_MESSAGE),
+ 3000,
+ );
+ };
const renderCommand = () => {
// regex to match only the command part (without the parameters)
const commandMatches = props.command.match(/^((?! -).)*/);
if (!commandMatches) {
- return
+ return;
}
- const commandWithoutParams = commandMatches[0]
- const paramsOnly = props.command.replace(commandWithoutParams, '')
+ const commandWithoutParams = commandMatches[0];
+ const paramsOnly = props.command.replace(commandWithoutParams, "");
// regex to match all the parameters, along with their assigned values
- const paramsList = paramsOnly.match(/(?<= )-{1,2}[\w-]+(?:(?!( -){1,2}).)*/g)
+ const paramsList = paramsOnly.match(
+ /(?<= )-{1,2}[\w-]+(?:(?!( -){1,2}).)*/g,
+ );
- return
-
- {commandWithoutParams}
-
-
- {paramsList?.map((paramWithValue) => {
- // split the parameter from it's value
- const splitParam = paramWithValue.split(/\s(.*)/)
+ return (
+
+
+ {commandWithoutParams}
+
+
+ {paramsList?.map((paramWithValue) => {
+ // split the parameter from it's value
+ const splitParam = paramWithValue.split(/\s(.*)/);
- const param = splitParam[0];
- const paramValue = ` ${splitParam[1] || ''}`;
- const paramValueIsComment = paramValue?.match(/<.*?>/);
+ const param = splitParam[0];
+ const paramValue = ` ${splitParam[1] || ""}`;
+ const paramValueIsComment = paramValue?.match(/<.*?>/);
- return (
-
- {param}
- {paramValue}
-
- );
- })}
-
-
- }
+ return (
+
+ {param}
+
+ {paramValue}
+
+
+ );
+ })}
+
+
+ );
+ };
return (
-
+
- {props.title}
+ {props.title}
{ props.command && handleCopyCommand() }} />
+ iconProps={{ iconName: "copy" }}
+ styles={{ root: { minWidth: "40px" } }}
+ onClick={() => {
+ props.command && handleCopyCommand();
+ }}
+ />
- {(!props.isLoading) ? renderCommand() :
- }
+ {!props.isLoading ? (
+ renderCommand()
+ ) : (
+
+ )}
);
-}
+};
diff --git a/ui/app/src/components/shared/ComplexItemDisplay.tsx b/ui/app/src/components/shared/ComplexItemDisplay.tsx
index 51c6d0d531..76abdbd61c 100644
--- a/ui/app/src/components/shared/ComplexItemDisplay.tsx
+++ b/ui/app/src/components/shared/ComplexItemDisplay.tsx
@@ -1,18 +1,28 @@
-import { FontWeights, getTheme, IButtonStyles, IconButton, IIconProps, Link, mergeStyleSets, Modal } from "@fluentui/react";
+import {
+ FontWeights,
+ getTheme,
+ IButtonStyles,
+ IconButton,
+ IIconProps,
+ Link,
+ mergeStyleSets,
+ Modal,
+} from "@fluentui/react";
import React, { useState } from "react";
interface ComplexPropertyModalProps {
- val: any,
- title: string
-};
-export const ComplexPropertyModal: React.FunctionComponent = (props: ComplexPropertyModalProps) => {
+ val: any;
+ title: string;
+}
+export const ComplexPropertyModal: React.FunctionComponent<
+ ComplexPropertyModalProps
+> = (props: ComplexPropertyModalProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
setIsOpen(true)}>[details]
- {
- isOpen &&
+ {isOpen && (
-
+
- }
+ )}
>
- )
+ );
};
-
interface NestedDisplayItemProps {
- val: any,
- isExpanded?: boolean,
- topLayer?: boolean
-};
+ val: any;
+ isExpanded?: boolean;
+ topLayer?: boolean;
+}
-const NestedDisplayItem: React.FunctionComponent = (props: NestedDisplayItemProps) => {
- const [isExpanded, setIsExpanded] = useState(props.isExpanded === true)
+const NestedDisplayItem: React.FunctionComponent = (
+ props: NestedDisplayItemProps,
+) => {
+ const [isExpanded, setIsExpanded] = useState(props.isExpanded === true);
return (
<>
- {
- !props.topLayer &&
- setIsExpanded(!isExpanded)} iconProps={{iconName: isExpanded ? 'ChevronUp' : 'ChevronDown'}} />
- }
- {
- isExpanded &&
+ {!props.topLayer && (
+ setIsExpanded(!isExpanded)}
+ iconProps={{ iconName: isExpanded ? "ChevronUp" : "ChevronDown" }}
+ />
+ )}
+ {isExpanded && (
- {
- Object.keys(props.val).map((key: string, i) => {
- if (typeof (props.val[key]) === 'object') {
- return (
-
- {key}:
-
-
- );
- }
+ {Object.keys(props.val).map((key: string, i) => {
+ if (typeof props.val[key] === "object") {
return (
- {isNaN(parseInt(key)) && key + ':'} {props.val[key]} );
- })
- }
+
+ {key}:
+
+
+ );
+ }
+ return (
+
+ {isNaN(parseInt(key)) && key + ":"} {props.val[key]}
+
+ );
+ })}
- }
+ )}
>
);
};
-const cancelIcon: IIconProps = { iconName: 'Cancel' };
+const cancelIcon: IIconProps = { iconName: "Cancel" };
const theme = getTheme();
const contentStyles = mergeStyleSets({
container: {
- display: 'flex',
- flexFlow: 'column nowrap',
- alignItems: 'stretch',
+ display: "flex",
+ flexFlow: "column nowrap",
+ alignItems: "stretch",
},
header: [
theme.fonts.xxLarge,
{
- flex: '1 1 auto',
+ flex: "1 1 auto",
borderTop: `4px solid ${theme.palette.themePrimary}`,
color: theme.palette.neutralPrimary,
- display: 'flex',
- alignItems: 'center',
+ display: "flex",
+ alignItems: "center",
fontWeight: FontWeights.semibold,
- padding: '12px 12px 14px 24px',
+ padding: "12px 12px 14px 24px",
},
],
body: {
- flex: '4 4 auto',
- padding: '0 24px 24px 24px',
- overflowY: 'hidden',
+ flex: "4 4 auto",
+ padding: "0 24px 24px 24px",
+ overflowY: "hidden",
selectors: {
- p: { margin: '14px 0' },
- 'p:first-child': { marginTop: 0 },
- 'p:last-child': { marginBottom: 0 },
+ p: { margin: "14px 0" },
+ "p:first-child": { marginTop: 0 },
+ "p:last-child": { marginBottom: 0 },
},
},
});
const iconButtonStyles: Partial = {
root: {
color: theme.palette.neutralPrimary,
- marginLeft: 'auto',
- marginTop: '4px',
- marginRight: '2px',
+ marginLeft: "auto",
+ marginTop: "4px",
+ marginRight: "2px",
},
rootHovered: {
color: theme.palette.neutralDark,
diff --git a/ui/app/src/components/shared/ConfirmCopyUrlToClipboard.tsx b/ui/app/src/components/shared/ConfirmCopyUrlToClipboard.tsx
index bfd3f2bc49..7fa7026dc0 100644
--- a/ui/app/src/components/shared/ConfirmCopyUrlToClipboard.tsx
+++ b/ui/app/src/components/shared/ConfirmCopyUrlToClipboard.tsx
@@ -1,60 +1,82 @@
-import { Dialog, PrimaryButton, DialogType, Stack, TooltipHost, TextField } from '@fluentui/react';
-import React, { useState } from 'react';
-import { Resource } from '../../models/resource';
+import {
+ Dialog,
+ PrimaryButton,
+ DialogType,
+ Stack,
+ TooltipHost,
+ TextField,
+} from "@fluentui/react";
+import React, { useState } from "react";
+import { Resource } from "../../models/resource";
interface ConfirmCopyUrlToClipboardProps {
- resource: Resource,
- onDismiss: () => void
+ resource: Resource;
+ onDismiss: () => void;
}
// show a explanation about why connect is disabled, and show a copy to clipboard tool tip
-export const ConfirmCopyUrlToClipboard: React.FunctionComponent = (props: ConfirmCopyUrlToClipboardProps) => {
- const COPY_TOOL_TIP_DEFAULT_MESSAGE = "Copy to clipboard"
+export const ConfirmCopyUrlToClipboard: React.FunctionComponent<
+ ConfirmCopyUrlToClipboardProps
+> = (props: ConfirmCopyUrlToClipboardProps) => {
+ const COPY_TOOL_TIP_DEFAULT_MESSAGE = "Copy to clipboard";
- const [copyToolTipMessage, setCopyToolTipMessage] = useState(COPY_TOOL_TIP_DEFAULT_MESSAGE);
+ const [copyToolTipMessage, setCopyToolTipMessage] = useState(
+ COPY_TOOL_TIP_DEFAULT_MESSAGE,
+ );
const copyUrlToClipboardProps = {
type: DialogType.normal,
- title: 'Access a Protected Endpoint',
- closeButtonAriaLabel: 'Close',
+ title: "Access a Protected Endpoint",
+ closeButtonAriaLabel: "Close",
subText: `Copy the link below, paste it and use it from a workspace virtual machine`,
};
const dialogStyles = { main: { maxWidth: 450 } };
const modalProps = {
- titleAriaId: 'labelId',
- subtitleAriaId: 'subTextId',
+ titleAriaId: "labelId",
+ subtitleAriaId: "subTextId",
isBlocking: true,
- styles: dialogStyles
+ styles: dialogStyles,
};
const handleCopyUrl = () => {
navigator.clipboard.writeText(props.resource.properties.connection_uri);
- setCopyToolTipMessage("Copied")
- setTimeout(() => setCopyToolTipMessage(COPY_TOOL_TIP_DEFAULT_MESSAGE), 3000);
- }
-
- return (<>
- props.onDismiss()}
- dialogContentProps={copyUrlToClipboardProps}
- modalProps={modalProps}
- >
-
-
-
-
-
- { handleCopyUrl() }}
- />
-
-
-
+ setCopyToolTipMessage("Copied");
+ setTimeout(
+ () => setCopyToolTipMessage(COPY_TOOL_TIP_DEFAULT_MESSAGE),
+ 3000,
+ );
+ };
- >);
+ return (
+ <>
+ props.onDismiss()}
+ dialogContentProps={copyUrlToClipboardProps}
+ modalProps={modalProps}
+ >
+
+
+
+
+
+ {
+ handleCopyUrl();
+ }}
+ />
+
+
+
+ >
+ );
};
-
diff --git a/ui/app/src/components/shared/ConfirmDeleteResource.tsx b/ui/app/src/components/shared/ConfirmDeleteResource.tsx
index 3dc31a396d..2aef7d54f3 100644
--- a/ui/app/src/components/shared/ConfirmDeleteResource.tsx
+++ b/ui/app/src/components/shared/ConfirmDeleteResource.tsx
@@ -1,22 +1,35 @@
-import { Dialog, DialogFooter, PrimaryButton, DefaultButton, DialogType, Spinner } from '@fluentui/react';
-import React, { useContext, useState } from 'react';
-import { Resource } from '../../models/resource';
-import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { ResourceType } from '../../models/resourceType';
-import { APIError } from '../../models/exceptions';
-import { LoadingState } from '../../models/loadingState';
-import { ExceptionLayout } from './ExceptionLayout';
-import { useAppDispatch } from '../../hooks/customReduxHooks';
-import { addUpdateOperation } from '../shared/notifications/operationsSlice';
+import {
+ Dialog,
+ DialogFooter,
+ PrimaryButton,
+ DefaultButton,
+ DialogType,
+ Spinner,
+} from "@fluentui/react";
+import React, { useContext, useState } from "react";
+import { Resource } from "../../models/resource";
+import {
+ HttpMethod,
+ ResultType,
+ useAuthApiCall,
+} from "../../hooks/useAuthApiCall";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { ResourceType } from "../../models/resourceType";
+import { APIError } from "../../models/exceptions";
+import { LoadingState } from "../../models/loadingState";
+import { ExceptionLayout } from "./ExceptionLayout";
+import { useAppDispatch } from "../../hooks/customReduxHooks";
+import { addUpdateOperation } from "../shared/notifications/operationsSlice";
interface ConfirmDeleteProps {
- resource: Resource,
- onDismiss: () => void
+ resource: Resource;
+ onDismiss: () => void;
}
// show a 'are you sure' modal, and then send a patch if the user confirms
-export const ConfirmDeleteResource: React.FunctionComponent = (props: ConfirmDeleteProps) => {
+export const ConfirmDeleteResource: React.FunctionComponent<
+ ConfirmDeleteProps
+> = (props: ConfirmDeleteProps) => {
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const [loading, setLoading] = useState(LoadingState.Ok);
@@ -25,56 +38,65 @@ export const ConfirmDeleteResource: React.FunctionComponent
const deleteProps = {
type: DialogType.normal,
- title: 'Delete Resource?',
- closeButtonAriaLabel: 'Close',
+ title: "Delete Resource?",
+ closeButtonAriaLabel: "Close",
subText: `Are you sure you want to permanently delete ${props.resource.properties.display_name}?`,
};
const dialogStyles = { main: { maxWidth: 450 } };
const modalProps = {
- titleAriaId: 'labelId',
- subtitleAriaId: 'subTextId',
+ titleAriaId: "labelId",
+ subtitleAriaId: "subTextId",
isBlocking: true,
- styles: dialogStyles
+ styles: dialogStyles,
};
- const wsAuth = (props.resource.resourceType === ResourceType.WorkspaceService || props.resource.resourceType === ResourceType.UserResource);
+ const wsAuth =
+ props.resource.resourceType === ResourceType.WorkspaceService ||
+ props.resource.resourceType === ResourceType.UserResource;
const deleteCall = async () => {
setLoading(LoadingState.Loading);
try {
- let op = await apiCall(props.resource.resourcePath, HttpMethod.Delete, wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined, undefined, ResultType.JSON);
+ let op = await apiCall(
+ props.resource.resourcePath,
+ HttpMethod.Delete,
+ wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined,
+ undefined,
+ ResultType.JSON,
+ );
dispatch(addUpdateOperation(op.operation));
props.onDismiss();
} catch (err: any) {
- err.userMessage = 'Failed to delete resource';
+ err.userMessage = "Failed to delete resource";
setApiError(err);
setLoading(LoadingState.Error);
}
- }
+ };
- return (<>
- props.onDismiss()}
- dialogContentProps={deleteProps}
- modalProps={modalProps}
- >
- {
- loading === LoadingState.Ok &&
-
- deleteCall()} />
- props.onDismiss()} />
-
- }
- {
- loading === LoadingState.Loading &&
-
- }
- {
- loading === LoadingState.Error &&
-
- }
-
- >);
+ return (
+ <>
+ props.onDismiss()}
+ dialogContentProps={deleteProps}
+ modalProps={modalProps}
+ >
+ {loading === LoadingState.Ok && (
+
+ deleteCall()} />
+ props.onDismiss()} />
+
+ )}
+ {loading === LoadingState.Loading && (
+
+ )}
+ {loading === LoadingState.Error && }
+
+ >
+ );
};
diff --git a/ui/app/src/components/shared/ConfirmDisableEnableResource.tsx b/ui/app/src/components/shared/ConfirmDisableEnableResource.tsx
index a788480d6c..0414ec13b3 100644
--- a/ui/app/src/components/shared/ConfirmDisableEnableResource.tsx
+++ b/ui/app/src/components/shared/ConfirmDisableEnableResource.tsx
@@ -1,23 +1,36 @@
-import { Dialog, DialogFooter, PrimaryButton, DefaultButton, DialogType, Spinner } from '@fluentui/react';
-import React, { useContext, useState } from 'react';
-import { Resource } from '../../models/resource';
-import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { ResourceType } from '../../models/resourceType';
-import { LoadingState } from '../../models/loadingState';
-import { APIError } from '../../models/exceptions';
-import { ExceptionLayout } from './ExceptionLayout';
-import { useAppDispatch } from '../../hooks/customReduxHooks';
-import { addUpdateOperation } from '../shared/notifications/operationsSlice';
+import {
+ Dialog,
+ DialogFooter,
+ PrimaryButton,
+ DefaultButton,
+ DialogType,
+ Spinner,
+} from "@fluentui/react";
+import React, { useContext, useState } from "react";
+import { Resource } from "../../models/resource";
+import {
+ HttpMethod,
+ ResultType,
+ useAuthApiCall,
+} from "../../hooks/useAuthApiCall";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { ResourceType } from "../../models/resourceType";
+import { LoadingState } from "../../models/loadingState";
+import { APIError } from "../../models/exceptions";
+import { ExceptionLayout } from "./ExceptionLayout";
+import { useAppDispatch } from "../../hooks/customReduxHooks";
+import { addUpdateOperation } from "../shared/notifications/operationsSlice";
interface ConfirmDisableEnableResourceProps {
- resource: Resource,
- isEnabled: boolean,
- onDismiss: () => void
+ resource: Resource;
+ isEnabled: boolean;
+ onDismiss: () => void;
}
// show a 'are you sure' modal, and then send a patch if the user confirms
-export const ConfirmDisableEnableResource: React.FunctionComponent = (props: ConfirmDisableEnableResourceProps) => {
+export const ConfirmDisableEnableResource: React.FunctionComponent<
+ ConfirmDisableEnableResourceProps
+> = (props: ConfirmDisableEnableResourceProps) => {
const apiCall = useAuthApiCall();
const [loading, setLoading] = useState(LoadingState.Ok);
const [apiError, setApiError] = useState({} as APIError);
@@ -26,41 +39,52 @@ export const ConfirmDisableEnableResource: React.FunctionComponent {
setLoading(LoadingState.Loading);
try {
- let body = { isEnabled: props.isEnabled }
- let op = await apiCall(props.resource.resourcePath, HttpMethod.Patch, wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined, body, ResultType.JSON, undefined, undefined, props.resource._etag);
+ let body = { isEnabled: props.isEnabled };
+ let op = await apiCall(
+ props.resource.resourcePath,
+ HttpMethod.Patch,
+ wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined,
+ body,
+ ResultType.JSON,
+ undefined,
+ undefined,
+ props.resource._etag,
+ );
dispatch(addUpdateOperation(op.operation));
props.onDismiss();
} catch (err: any) {
- err.userMessage = 'Failed to enable/disable resource';
+ err.userMessage = "Failed to enable/disable resource";
setApiError(err);
setLoading(LoadingState.Error);
}
- }
+ };
return (
<>
@@ -70,25 +94,31 @@ export const ConfirmDisableEnableResource: React.FunctionComponent
- {
- loading === LoadingState.Ok &&
+ {loading === LoadingState.Ok && (
- {props.isEnabled ?
- toggleDisableCall()} />
- :
- toggleDisableCall()} />
- }
+ {props.isEnabled ? (
+ toggleDisableCall()}
+ />
+ ) : (
+ toggleDisableCall()}
+ />
+ )}
props.onDismiss()} />
- }
- {
- loading === LoadingState.Loading &&
-
- }
- {
- loading === LoadingState.Error &&
-
- }
+ )}
+ {loading === LoadingState.Loading && (
+
+ )}
+ {loading === LoadingState.Error && }
- >);
+ >
+ );
};
diff --git a/ui/app/src/components/shared/ConfirmUpgradeResource.tsx b/ui/app/src/components/shared/ConfirmUpgradeResource.tsx
index 29a1417d52..8967b08e01 100644
--- a/ui/app/src/components/shared/ConfirmUpgradeResource.tsx
+++ b/ui/app/src/components/shared/ConfirmUpgradeResource.tsx
@@ -1,71 +1,98 @@
-import { Dialog, DialogFooter, PrimaryButton, DialogType, Spinner, Dropdown, MessageBar, MessageBarType, Icon } from '@fluentui/react';
-import React, { useContext, useState } from 'react';
-import { AvailableUpgrade, Resource } from '../../models/resource';
-import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { ResourceType } from '../../models/resourceType';
-import { APIError } from '../../models/exceptions';
-import { LoadingState } from '../../models/loadingState';
-import { ExceptionLayout } from './ExceptionLayout';
-import { useAppDispatch } from '../../hooks/customReduxHooks';
-import { addUpdateOperation } from '../shared/notifications/operationsSlice';
+import {
+ Dialog,
+ DialogFooter,
+ PrimaryButton,
+ DialogType,
+ Spinner,
+ Dropdown,
+ MessageBar,
+ MessageBarType,
+ Icon,
+} from "@fluentui/react";
+import React, { useContext, useState } from "react";
+import { AvailableUpgrade, Resource } from "../../models/resource";
+import {
+ HttpMethod,
+ ResultType,
+ useAuthApiCall,
+} from "../../hooks/useAuthApiCall";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { ResourceType } from "../../models/resourceType";
+import { APIError } from "../../models/exceptions";
+import { LoadingState } from "../../models/loadingState";
+import { ExceptionLayout } from "./ExceptionLayout";
+import { useAppDispatch } from "../../hooks/customReduxHooks";
+import { addUpdateOperation } from "../shared/notifications/operationsSlice";
interface ConfirmUpgradeProps {
- resource: Resource,
- onDismiss: () => void
+ resource: Resource;
+ onDismiss: () => void;
}
-export const ConfirmUpgradeResource: React.FunctionComponent = (props: ConfirmUpgradeProps) => {
+export const ConfirmUpgradeResource: React.FunctionComponent<
+ ConfirmUpgradeProps
+> = (props: ConfirmUpgradeProps) => {
const apiCall = useAuthApiCall();
- const [selectedVersion, setSelectedVersion] = useState("")
+ const [selectedVersion, setSelectedVersion] = useState("");
const [apiError, setApiError] = useState({} as APIError);
- const [requestLoadingState, setRequestLoadingState] = useState(LoadingState.Ok);
+ const [requestLoadingState, setRequestLoadingState] = useState(
+ LoadingState.Ok,
+ );
const workspaceCtx = useContext(WorkspaceContext);
const dispatch = useAppDispatch();
const upgradeProps = {
type: DialogType.normal,
title: `Upgrade Template Version?`,
- closeButtonAriaLabel: 'Close',
+ closeButtonAriaLabel: "Close",
subText: `Are you sure you want upgrade the template version of ${props.resource.properties.display_name} from version ${props.resource.templateVersion}?`,
};
const dialogStyles = { main: { maxWidth: 450 } };
const modalProps = {
- titleAriaId: 'labelId',
- subtitleAriaId: 'subTextId',
+ titleAriaId: "labelId",
+ subtitleAriaId: "subTextId",
isBlocking: true,
- styles: dialogStyles
+ styles: dialogStyles,
};
- const wsAuth = (props.resource.resourceType === ResourceType.WorkspaceService || props.resource.resourceType === ResourceType.UserResource);
+ const wsAuth =
+ props.resource.resourceType === ResourceType.WorkspaceService ||
+ props.resource.resourceType === ResourceType.UserResource;
const upgradeCall = async () => {
setRequestLoadingState(LoadingState.Loading);
try {
- let body = { templateVersion: selectedVersion }
- let op = await apiCall(props.resource.resourcePath,
+ let body = { templateVersion: selectedVersion };
+ let op = await apiCall(
+ props.resource.resourcePath,
HttpMethod.Patch,
wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined,
body,
ResultType.JSON,
undefined,
undefined,
- props.resource._etag);
+ props.resource._etag,
+ );
dispatch(addUpdateOperation(op.operation));
props.onDismiss();
} catch (err: any) {
- err.userMessage = 'Failed to upgrade resource';
+ err.userMessage = "Failed to upgrade resource";
setApiError(err);
setRequestLoadingState(LoadingState.Error);
}
- }
+ };
const onRenderOption = (option: any): JSX.Element => {
return (
{option.data && option.data.icon && (
-
+
)}
{option.text}
@@ -73,48 +100,65 @@ export const ConfirmUpgradeResource: React.FunctionComponent) => {
- return upgrade.map(upgrade => ({ "key": upgrade.version, "text": upgrade.version, data: { icon: upgrade.forceUpdateRequired ? 'Warning' : '' } }))
- }
+ return upgrade.map((upgrade) => ({
+ key: upgrade.version,
+ text: upgrade.version,
+ data: { icon: upgrade.forceUpdateRequired ? "Warning" : "" },
+ }));
+ };
const getDropdownOptions = () => {
- const options = []
- const nonMajorUpgrades = props.resource.availableUpgrades.filter(upgrade => !upgrade.forceUpdateRequired)
- options.push(...convertToDropDownOptions(nonMajorUpgrades))
+ const options = [];
+ const nonMajorUpgrades = props.resource.availableUpgrades.filter(
+ (upgrade) => !upgrade.forceUpdateRequired,
+ );
+ options.push(...convertToDropDownOptions(nonMajorUpgrades));
return options;
- }
+ };
- return (<>
- props.onDismiss()}
- dialogContentProps={upgradeProps}
- modalProps={modalProps}
- >
- {
- requestLoadingState === LoadingState.Ok &&
- <>
- Upgrading the template version is irreversible.
-
- { option && setSelectedVersion(option.text); }}
- selectedKey={selectedVersion}
- />
- upgradeCall()} />
-
- >
- }
- {
- requestLoadingState === LoadingState.Loading &&
-
- }
- {
- requestLoadingState === LoadingState.Error &&
-
- }
-
- >);
+ return (
+ <>
+ props.onDismiss()}
+ dialogContentProps={upgradeProps}
+ modalProps={modalProps}
+ >
+ {requestLoadingState === LoadingState.Ok && (
+ <>
+
+ Upgrading the template version is irreversible.
+
+
+ {
+ option && setSelectedVersion(option.text);
+ }}
+ selectedKey={selectedVersion}
+ />
+ upgradeCall()}
+ />
+
+ >
+ )}
+ {requestLoadingState === LoadingState.Loading && (
+
+ )}
+ {requestLoadingState === LoadingState.Error && (
+
+ )}
+
+ >
+ );
};
diff --git a/ui/app/src/components/shared/CostsTag.tsx b/ui/app/src/components/shared/CostsTag.tsx
index 507ef37916..cfef6a7870 100644
--- a/ui/app/src/components/shared/CostsTag.tsx
+++ b/ui/app/src/components/shared/CostsTag.tsx
@@ -4,19 +4,27 @@ import { CostsContext } from "../../contexts/CostsContext";
import { LoadingState } from "../../models/loadingState";
import { WorkspaceContext } from "../../contexts/WorkspaceContext";
import { CostResource } from "../../models/costs";
-import { useAuthApiCall, HttpMethod, ResultType } from '../../hooks/useAuthApiCall';
+import {
+ useAuthApiCall,
+ HttpMethod,
+ ResultType,
+} from "../../hooks/useAuthApiCall";
import { ApiEndpoint } from "../../models/apiEndpoints";
interface CostsTagProps {
resourceId: string;
}
-export const CostsTag: React.FunctionComponent = (props: CostsTagProps) => {
+export const CostsTag: React.FunctionComponent = (
+ props: CostsTagProps,
+) => {
const costsCtx = useContext(CostsContext);
const workspaceCtx = useContext(WorkspaceContext);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const apiCall = useAuthApiCall();
- const [formattedCost, setFormattedCost] = useState(undefined);
+ const [formattedCost, setFormattedCost] = useState(
+ undefined,
+ );
useEffect(() => {
async function fetchCostData() {
@@ -25,10 +33,21 @@ export const CostsTag: React.FunctionComponent = (props: CostsTag
costs = workspaceCtx.costs;
} else if (costsCtx.costs.length > 0) {
costs = costsCtx.costs;
- } else if(!workspaceCtx.workspace.id) {
- let scopeId = (await apiCall(`${ApiEndpoint.Workspaces}/${props.resourceId}/scopeid`, HttpMethod.Get)).workspaceAuth.scopeId;
- const r = await apiCall(`${ApiEndpoint.Workspaces}/${props.resourceId}/${ApiEndpoint.Costs}`, HttpMethod.Get, scopeId, undefined, ResultType.JSON);
- costs = [{costs: r.costs, id: r.id, name: r.name }];
+ } else if (!workspaceCtx.workspace.id) {
+ let scopeId = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${props.resourceId}/scopeid`,
+ HttpMethod.Get,
+ )
+ ).workspaceAuth.scopeId;
+ const r = await apiCall(
+ `${ApiEndpoint.Workspaces}/${props.resourceId}/${ApiEndpoint.Costs}`,
+ HttpMethod.Get,
+ scopeId,
+ undefined,
+ ResultType.JSON,
+ );
+ costs = [{ costs: r.costs, id: r.id, name: r.name }];
}
const resourceCosts = costs.find((cost) => {
@@ -37,18 +56,24 @@ export const CostsTag: React.FunctionComponent = (props: CostsTag
if (resourceCosts && resourceCosts.costs.length > 0) {
const formattedCost = new Intl.NumberFormat(undefined, {
- style: 'currency',
+ style: "currency",
currency: resourceCosts?.costs[0].currency,
- currencyDisplay: 'narrowSymbol',
+ currencyDisplay: "narrowSymbol",
minimumFractionDigits: 2,
- maximumFractionDigits: 2
+ maximumFractionDigits: 2,
}).format(resourceCosts.costs[0].cost);
setFormattedCost(formattedCost);
}
setLoadingState(LoadingState.Ok);
}
fetchCostData();
- }, [apiCall, props.resourceId, workspaceCtx.costs, costsCtx.costs, workspaceCtx.workspace.id]);
+ }, [
+ apiCall,
+ props.resourceId,
+ workspaceCtx.costs,
+ costsCtx.costs,
+ workspaceCtx.workspace.id,
+ ]);
const costBadge = (
@@ -68,5 +93,5 @@ export const CostsTag: React.FunctionComponent = (props: CostsTag
);
- return (costBadge);
+ return costBadge;
};
diff --git a/ui/app/src/components/shared/ExceptionLayout.tsx b/ui/app/src/components/shared/ExceptionLayout.tsx
index dc29b3c1a9..6f3cf0cdeb 100644
--- a/ui/app/src/components/shared/ExceptionLayout.tsx
+++ b/ui/app/src/components/shared/ExceptionLayout.tsx
@@ -1,22 +1,26 @@
-import { MessageBar, MessageBarType, Link as FluentLink, Icon, } from '@fluentui/react';
-import React, { useState } from 'react';
-import { APIError } from '../../models/exceptions';
+import {
+ MessageBar,
+ MessageBarType,
+ Link as FluentLink,
+ Icon,
+} from "@fluentui/react";
+import React, { useState } from "react";
+import { APIError } from "../../models/exceptions";
interface ExceptionLayoutProps {
- e: APIError
+ e: APIError;
}
-export const ExceptionLayout: React.FunctionComponent = (props: ExceptionLayoutProps) => {
+export const ExceptionLayout: React.FunctionComponent = (
+ props: ExceptionLayoutProps,
+) => {
const [showDetails, setShowDetails] = useState(false);
const [showMessageBar, setShowMessageBar] = useState(true);
switch (props.e.status) {
case 403:
return (
-
+
Access Denied
{props.e.userMessage}
{props.e.message}
@@ -24,54 +28,82 @@ export const ExceptionLayout: React.FunctionComponent = (p
);
case 429:
- return (<>>);
+ return <>>;
default:
if (!showMessageBar) return null;
return (
- showMessageBar &&
+ showMessageBar && (
setShowMessageBar(false)}
- dismissButtonAriaLabel="Close"
- >
- {props.e.userMessage}
- {props.e.message}
+ messageBarType={MessageBarType.error}
+ isMultiline={true}
+ onDismiss={() => setShowMessageBar(false)}
+ dismissButtonAriaLabel="Close"
+ >
+ {props.e.userMessage}
+ {props.e.message}
+
- { setShowDetails(!showDetails) }} style={{ position: 'relative', top: '2px', paddingLeft: 0 }}>
- {
- showDetails ?
- <> {'Hide Details'}> :
- <> {'Show Details'} >
- }
-
- {
- showDetails &&
- <>
-
-
-
- Endpoint
- {props.e.endpoint}
-
-
- Status Code
- {props.e.status || '(none)'}
-
-
- Stack Trace
- {props.e.stack}
-
-
- Exception
- {props.e.exception}
-
-
-
- >
- }
-
-
+ {
+ setShowDetails(!showDetails);
+ }}
+ style={{ position: "relative", top: "2px", paddingLeft: 0 }}
+ >
+ {showDetails ? (
+ <>
+ {" "}
+ {"Hide Details"}
+ >
+ ) : (
+ <>
+ {" "}
+ {"Show Details"}{" "}
+ >
+ )}
+
+ {showDetails && (
+ <>
+
+
+
+
+ Endpoint
+
+ {props.e.endpoint}
+
+
+
+ Status Code
+
+ {props.e.status || "(none)"}
+
+
+
+ Stack Trace
+
+ {props.e.stack}
+
+
+
+ Exception
+
+ {props.e.exception}
+
+
+
+ >
+ )}
+
+ )
);
}
};
diff --git a/ui/app/src/components/shared/Footer.tsx b/ui/app/src/components/shared/Footer.tsx
index d659d0bcd8..a125e12d42 100644
--- a/ui/app/src/components/shared/Footer.tsx
+++ b/ui/app/src/components/shared/Footer.tsx
@@ -1,7 +1,19 @@
-import React, { useEffect, useState } from 'react';
-import { AnimationClassNames, Callout, IconButton, FontWeights, Stack, Text, getTheme, mergeStyles, mergeStyleSets, StackItem, IButtonStyles } from '@fluentui/react';
-import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { ApiEndpoint } from '../../models/apiEndpoints';
+import React, { useEffect, useState } from "react";
+import {
+ AnimationClassNames,
+ Callout,
+ IconButton,
+ FontWeights,
+ Stack,
+ Text,
+ getTheme,
+ mergeStyles,
+ mergeStyleSets,
+ StackItem,
+ IButtonStyles,
+} from "@fluentui/react";
+import { HttpMethod, useAuthApiCall } from "../../hooks/useAuthApiCall";
+import { ApiEndpoint } from "../../models/apiEndpoints";
import config from "../../config.json";
// TODO:
@@ -11,15 +23,17 @@ import config from "../../config.json";
export const Footer: React.FunctionComponent = () => {
const [showInfo, setShowInfo] = useState(false);
const [apiMetadata, setApiMetadata] = useState();
- const [health, setHealth] = useState<{services: [{service: string, status: string}]}>();
+ const [health, setHealth] = useState<{
+ services: [{ service: string; status: string }];
+ }>();
const apiCall = useAuthApiCall();
useEffect(() => {
- const getMeta = async() => {
+ const getMeta = async () => {
const result = await apiCall(ApiEndpoint.Metadata, HttpMethod.Get);
setApiMetadata(result);
};
- const getHealth = async() => {
+ const getHealth = async () => {
const result = await apiCall(ApiEndpoint.Health, HttpMethod.Get);
setHealth(result);
};
@@ -31,19 +45,19 @@ export const Footer: React.FunctionComponent = () => {
return (
-
+
Azure Trusted Research Environment
setShowInfo(!showInfo)}
/>
- {
- showInfo && {
Azure TRE
-
- {
- uiConfig.version &&
+
+ {uiConfig.version && (
+
UI Version:
{uiConfig.version}
- }
- {
- apiMetadata?.api_version &&
+ )}
+ {apiMetadata?.api_version && (
+
API Version:
{apiMetadata.api_version}
- }
+ )}
-
- {
- health?.services.map(s => {
- return
+
+ {health?.services.map((s) => {
+ return (
+
{s.service}:
{s.status}
- })
- }
+ );
+ })}
- }
+ )}
);
};
@@ -89,13 +114,13 @@ export const Footer: React.FunctionComponent = () => {
const theme = getTheme();
const contentClass = mergeStyles([
{
- alignItems: 'center',
+ alignItems: "center",
backgroundColor: theme.palette.themeDark,
color: theme.palette.white,
- lineHeight: '25px',
- padding: '0 20px',
+ lineHeight: "25px",
+ padding: "0 20px",
},
- AnimationClassNames.scaleUpIn100
+ AnimationClassNames.scaleUpIn100,
]);
const iconButtonStyles: Partial = {
@@ -110,10 +135,10 @@ const iconButtonStyles: Partial = {
const styles = mergeStyleSets({
callout: {
width: 250,
- padding: '20px 24px',
+ padding: "20px 24px",
},
title: {
marginBottom: 12,
- fontWeight: FontWeights.semilight
- }
+ fontWeight: FontWeights.semilight,
+ },
});
diff --git a/ui/app/src/components/shared/GenericErrorBoundary.tsx b/ui/app/src/components/shared/GenericErrorBoundary.tsx
index 624829d251..b728210bac 100644
--- a/ui/app/src/components/shared/GenericErrorBoundary.tsx
+++ b/ui/app/src/components/shared/GenericErrorBoundary.tsx
@@ -2,10 +2,14 @@ import { MessageBar, MessageBarType } from "@fluentui/react";
import React from "react";
interface ErrorState {
- hasError: boolean
+ hasError: boolean;
}
-export class GenericErrorBoundary extends React.Component {
+export class GenericErrorBoundary extends React.Component<
+ any,
+ ErrorState,
+ any
+> {
constructor(props: any) {
super(props);
this.state = { hasError: false };
@@ -18,23 +22,23 @@ export class GenericErrorBoundary extends React.Component
componentDidCatch(error: any, errorInfo: any) {
// You can also log the error to an error reporting service
- console.error('UNHANDLED EXCEPTION', error, errorInfo);
+ console.error("UNHANDLED EXCEPTION", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
-
+
Uh oh!
- This area encountered an error that we can't recover from. Please check your configuration and refresh.
- Further debugging details can be found in the browser console.
+
+ This area encountered an error that we can't recover from. Please
+ check your configuration and refresh.
+ Further debugging details can be found in the browser console.
+
);
}
return this.props.children;
}
-}
\ No newline at end of file
+}
diff --git a/ui/app/src/components/shared/PowerStateBadge.tsx b/ui/app/src/components/shared/PowerStateBadge.tsx
index 114cf4ea8d..f8aa9574d6 100644
--- a/ui/app/src/components/shared/PowerStateBadge.tsx
+++ b/ui/app/src/components/shared/PowerStateBadge.tsx
@@ -1,22 +1,24 @@
-import React from 'react';
-import { VMPowerStates } from '../../models/resource';
+import React from "react";
+import { VMPowerStates } from "../../models/resource";
interface PowerStateBadgeProps {
- state: VMPowerStates
+ state: VMPowerStates;
}
-export const PowerStateBadge: React.FunctionComponent = (props: PowerStateBadgeProps) => {
+export const PowerStateBadge: React.FunctionComponent = (
+ props: PowerStateBadgeProps,
+) => {
let stateClass = "tre-power-off";
if (props.state === VMPowerStates.Running) stateClass = " tre-power-on";
return (
<>
- {
- props.state &&
+ {props.state && (
+
- {props.state.replace('VM ', '')}
+ {props.state.replace("VM ", "")}
- }
+ )}
>
);
};
diff --git a/ui/app/src/components/shared/RequestsList.tsx b/ui/app/src/components/shared/RequestsList.tsx
index 1adc05650f..225d77c806 100644
--- a/ui/app/src/components/shared/RequestsList.tsx
+++ b/ui/app/src/components/shared/RequestsList.tsx
@@ -1,14 +1,35 @@
-import React, { useCallback, useEffect, useState } from 'react';
-import { ColumnActionsMode, CommandBar, CommandBarButton, ContextualMenu, DirectionalHint, getTheme, IColumn, ICommandBarItemProps, Icon, IContextualMenuItem, IContextualMenuProps, Persona, PersonaSize, SelectionMode, ShimmeredDetailsList, Stack } from '@fluentui/react';
-import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { AirlockRequest, AirlockRequestStatus, AirlockRequestType } from '../../models/airlock';
-import moment from 'moment';
-import { useNavigate } from 'react-router-dom';
-import { LoadingState } from '../../models/loadingState';
-import { APIError } from '../../models/exceptions';
-import { ExceptionLayout } from './ExceptionLayout';
-import { getFileTypeIconProps } from '@fluentui/react-file-type-icons';
+import React, { useCallback, useEffect, useState } from "react";
+import {
+ ColumnActionsMode,
+ CommandBar,
+ CommandBarButton,
+ ContextualMenu,
+ DirectionalHint,
+ getTheme,
+ IColumn,
+ ICommandBarItemProps,
+ Icon,
+ IContextualMenuItem,
+ IContextualMenuProps,
+ Persona,
+ PersonaSize,
+ SelectionMode,
+ ShimmeredDetailsList,
+ Stack,
+} from "@fluentui/react";
+import { HttpMethod, useAuthApiCall } from "../../hooks/useAuthApiCall";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import {
+ AirlockRequest,
+ AirlockRequestStatus,
+ AirlockRequestType,
+} from "../../models/airlock";
+import moment from "moment";
+import { useNavigate } from "react-router-dom";
+import { LoadingState } from "../../models/loadingState";
+import { APIError } from "../../models/exceptions";
+import { ExceptionLayout } from "./ExceptionLayout";
+import { getFileTypeIconProps } from "@fluentui/react-file-type-icons";
interface Workspace {
id: string;
@@ -18,26 +39,38 @@ interface Workspace {
}
export const RequestsList: React.FunctionComponent = () => {
- const [myAirlockRequests, setMyAirlockRequests] = useState([] as AirlockRequest[]);
- const [airlockManagerRequests, setAirlockManagerRequests] = useState([] as AirlockRequest[]);
- const [airlockRequests, setAirlockRequests] = useState([] as AirlockRequest[]);
+ const [myAirlockRequests, setMyAirlockRequests] = useState(
+ [] as AirlockRequest[],
+ );
+ const [airlockManagerRequests, setAirlockManagerRequests] = useState(
+ [] as AirlockRequest[],
+ );
+ const [airlockRequests, setAirlockRequests] = useState(
+ [] as AirlockRequest[],
+ );
const [requestColumns, setRequestColumns] = useState([] as IColumn[]);
- const [orderBy, setOrderBy] = useState('updatedWhen');
+ const [orderBy, setOrderBy] = useState("updatedWhen");
const [orderAscending, setOrderAscending] = useState(false);
const [filters, setFilters] = useState(new Map());
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
- const [contextMenuProps, setContextMenuProps] = useState();
+ const [contextMenuProps, setContextMenuProps] =
+ useState();
const [apiError, setApiError] = useState();
const apiCall = useAuthApiCall();
const theme = getTheme();
const navigate = useNavigate();
- const mapRequestsToWorkspace = (requests: AirlockRequest[], workspaces: Workspace[]) => {
- return requests.map(request => {
- const workspace = workspaces.find(w => w.id === request.workspaceId);
+ const mapRequestsToWorkspace = (
+ requests: AirlockRequest[],
+ workspaces: Workspace[],
+ ) => {
+ return requests.map((request) => {
+ const workspace = workspaces.find((w) => w.id === request.workspaceId);
return {
...request,
- workspace: workspace ? workspace.properties.display_name : 'Unknown Workspace'
+ workspace: workspace
+ ? workspace.properties.display_name
+ : "Unknown Workspace",
};
});
};
@@ -46,7 +79,7 @@ export const RequestsList: React.FunctionComponent = () => {
setApiError(undefined);
setLoadingState(LoadingState.Loading);
try {
- let query = '?';
+ let query = "?";
filters.forEach((value, key) => {
query += `${key}=${value}&`;
});
@@ -55,24 +88,36 @@ export const RequestsList: React.FunctionComponent = () => {
}
let fetchedWorkspaces: { workspaces: Workspace[] } = { workspaces: [] };
try {
- fetchedWorkspaces = await apiCall(ApiEndpoint.Workspaces, HttpMethod.Get);
+ fetchedWorkspaces = await apiCall(
+ ApiEndpoint.Workspaces,
+ HttpMethod.Get,
+ );
} catch (err: any) {
setApiError(err);
console.error("Failed to fetch workspaces:", err);
}
let requests: AirlockRequest[];
let airlock_manager_requests: AirlockRequest[];
- requests = await apiCall(`${ApiEndpoint.Requests}${query.slice(0, -1)}`, HttpMethod.Get);
+ requests = await apiCall(
+ `${ApiEndpoint.Requests}${query.slice(0, -1)}`,
+ HttpMethod.Get,
+ );
requests = mapRequestsToWorkspace(requests, fetchedWorkspaces.workspaces);
- airlock_manager_requests = await apiCall(`${ApiEndpoint.Requests}?${query.slice(0, -1)}&airlock_manager=true`, HttpMethod.Get);
- airlock_manager_requests = mapRequestsToWorkspace(airlock_manager_requests, fetchedWorkspaces.workspaces);
+ airlock_manager_requests = await apiCall(
+ `${ApiEndpoint.Requests}?${query.slice(0, -1)}&airlock_manager=true`,
+ HttpMethod.Get,
+ );
+ airlock_manager_requests = mapRequestsToWorkspace(
+ airlock_manager_requests,
+ fetchedWorkspaces.workspaces,
+ );
setMyAirlockRequests(requests);
setAirlockRequests(requests);
setLoadingState(LoadingState.Ok);
setAirlockManagerRequests(airlock_manager_requests);
} catch (err: any) {
- err.userMessage = 'Error fetching airlock requests';
+ err.userMessage = "Error fetching airlock requests";
setApiError(err);
setLoadingState(LoadingState.Error);
}
@@ -92,208 +137,244 @@ export const RequestsList: React.FunctionComponent = () => {
});
};
- const openContextMenu = useCallback((column: IColumn, ev: React.MouseEvent, options: Array) => {
- const filterOptions = options.map(option => {
- return {
- key: option,
- name: option,
- canCheck: true,
- checked: filters?.has(column.key) && filters.get(column.key) === option,
- onClick: () => {
- setFilters((f) => {
- if (f.get(column.key) === option) {
- f.delete(column.key);
- } else {
- f.set(column.key, option);
- }
- return new Map(f);
- });
- }
- }
- });
+ const openContextMenu = useCallback(
+ (
+ column: IColumn,
+ ev: React.MouseEvent,
+ options: Array,
+ ) => {
+ const filterOptions = options.map((option) => {
+ return {
+ key: option,
+ name: option,
+ canCheck: true,
+ checked:
+ filters?.has(column.key) && filters.get(column.key) === option,
+ onClick: () => {
+ setFilters((f) => {
+ if (f.get(column.key) === option) {
+ f.delete(column.key);
+ } else {
+ f.set(column.key, option);
+ }
+ return new Map(f);
+ });
+ },
+ };
+ });
- const items: IContextualMenuItem[] = [
- {
- key: 'sort',
- name: 'Sort',
- iconProps: { iconName: 'Sort' },
- onClick: () => orderRequests(column)
- },
- {
- key: 'filter',
- name: 'Filter',
- iconProps: { iconName: 'Filter' },
- subMenuProps: {
- items: filterOptions,
- }
- }
- ];
+ const items: IContextualMenuItem[] = [
+ {
+ key: "sort",
+ name: "Sort",
+ iconProps: { iconName: "Sort" },
+ onClick: () => orderRequests(column),
+ },
+ {
+ key: "filter",
+ name: "Filter",
+ iconProps: { iconName: "Filter" },
+ subMenuProps: {
+ items: filterOptions,
+ },
+ },
+ ];
- setContextMenuProps({
- items: items,
- target: ev.currentTarget as HTMLElement,
- directionalHint: DirectionalHint.bottomCenter,
- gapSpace: 0,
- onDismiss: () => setContextMenuProps(undefined),
- });
- }, [filters]);
+ setContextMenuProps({
+ items: items,
+ target: ev.currentTarget as HTMLElement,
+ directionalHint: DirectionalHint.bottomCenter,
+ gapSpace: 0,
+ onDismiss: () => setContextMenuProps(undefined),
+ });
+ },
+ [filters],
+ );
useEffect(() => {
- const orderByColumn = (ev: React.MouseEvent, column: IColumn) => {
+ const orderByColumn = (
+ ev: React.MouseEvent,
+ column: IColumn,
+ ) => {
orderRequests(column);
};
const columns: IColumn[] = [
{
- key: 'fileIcon',
- name: 'fileIcon',
+ key: "fileIcon",
+ name: "fileIcon",
minWidth: 16,
maxWidth: 16,
isIconOnly: true,
onRender: (request: AirlockRequest) => {
if (request.status === AirlockRequestStatus.Draft) {
- return
+ return (
+
+ );
} else if (request.files?.length > 0 && request.files[0].name) {
- const fileType = request.files[0].name.split('.').pop();
- return
+ const fileType = request.files[0].name.split(".").pop();
+ return (
+
+ );
} else {
- return
+ return (
+
+ );
}
- }
+ },
},
{
- key: 'workspace',
- name: 'Workspace',
- ariaLabel: 'Workspace of the airlock request',
+ key: "workspace",
+ name: "Workspace",
+ ariaLabel: "Workspace of the airlock request",
minWidth: 150,
maxWidth: 200,
isResizable: true,
- fieldName: 'workspace'
+ fieldName: "workspace",
},
{
- key: 'title',
- name: 'Title',
- ariaLabel: 'Title of the airlock request',
+ key: "title",
+ name: "Title",
+ ariaLabel: "Title of the airlock request",
minWidth: 150,
maxWidth: 300,
isResizable: true,
- fieldName: 'title'
+ fieldName: "title",
},
{
- key: 'createdBy',
- name: 'Creator',
- ariaLabel: 'Creator of the airlock request',
+ key: "createdBy",
+ name: "Creator",
+ ariaLabel: "Creator of the airlock request",
minWidth: 150,
maxWidth: 200,
isResizable: true,
- onRender: (request: AirlockRequest) => ,
- isFiltered: filters.has('creator_user_id')
+ onRender: (request: AirlockRequest) => (
+
+ ),
+ isFiltered: filters.has("creator_user_id"),
},
{
- key: 'type',
- name: 'Type',
- ariaLabel: 'Whether the request is import or export',
+ key: "type",
+ name: "Type",
+ ariaLabel: "Whether the request is import or export",
minWidth: 70,
maxWidth: 100,
isResizable: true,
- fieldName: 'type',
+ fieldName: "type",
columnActionsMode: ColumnActionsMode.hasDropdown,
- isSorted: orderBy === 'type',
+ isSorted: orderBy === "type",
isSortedDescending: !orderAscending,
- onColumnClick: (ev, column) => openContextMenu(column, ev, Object.values(AirlockRequestType)),
+ onColumnClick: (ev, column) =>
+ openContextMenu(column, ev, Object.values(AirlockRequestType)),
onColumnContextMenu: (column, ev) =>
- (column && ev) && openContextMenu(column, ev, Object.values(AirlockRequestType)),
- isFiltered: filters.has('type')
+ column &&
+ ev &&
+ openContextMenu(column, ev, Object.values(AirlockRequestType)),
+ isFiltered: filters.has("type"),
},
{
- key: 'status',
- name: 'Status',
- ariaLabel: 'Status of the request',
+ key: "status",
+ name: "Status",
+ ariaLabel: "Status of the request",
minWidth: 70,
isResizable: true,
- fieldName: 'status',
+ fieldName: "status",
columnActionsMode: ColumnActionsMode.hasDropdown,
- isSorted: orderBy === 'status',
+ isSorted: orderBy === "status",
isSortedDescending: !orderAscending,
- onColumnClick: (ev, column) => openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
+ onColumnClick: (ev, column) =>
+ openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
onColumnContextMenu: (column, ev) =>
- (column && ev) && openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
- isFiltered: filters.has('status'),
- onRender: (request: AirlockRequest) => request.status.replace("_", " ")
+ column &&
+ ev &&
+ openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
+ isFiltered: filters.has("status"),
+ onRender: (request: AirlockRequest) => request.status.replace("_", " "),
},
{
- key: 'createdTime',
- name: 'Created',
- ariaLabel: 'When the request was created',
+ key: "createdTime",
+ name: "Created",
+ ariaLabel: "When the request was created",
minWidth: 120,
- data: 'number',
+ data: "number",
isResizable: true,
- fieldName: 'createdTime',
- isSorted: orderBy === 'createdTime',
+ fieldName: "createdTime",
+ isSorted: orderBy === "createdTime",
isSortedDescending: !orderAscending,
onRender: (request: AirlockRequest) => {
- return {moment.unix(request.createdWhen).format('DD/MM/YYYY')} ;
+ return (
+ {moment.unix(request.createdWhen).format("DD/MM/YYYY")}
+ );
},
- onColumnClick: orderByColumn
+ onColumnClick: orderByColumn,
},
{
- key: 'updatedWhen',
- name: 'Updated',
- ariaLabel: 'When the request was last updated',
+ key: "updatedWhen",
+ name: "Updated",
+ ariaLabel: "When the request was last updated",
minWidth: 120,
- data: 'number',
+ data: "number",
isResizable: true,
- fieldName: 'updatedWhen',
- isSorted: orderBy === 'updatedWhen',
+ fieldName: "updatedWhen",
+ isSorted: orderBy === "updatedWhen",
isSortedDescending: !orderAscending,
onRender: (request: AirlockRequest) => {
return {moment.unix(request.updatedWhen).fromNow()} ;
},
- onColumnClick: orderByColumn
- }
+ onColumnClick: orderByColumn,
+ },
];
setRequestColumns(columns);
}, [openContextMenu, filters, orderAscending, orderBy]);
const quickFilters: ICommandBarItemProps[] = [
{
- key: 'reset',
- text: 'Clear filters',
- iconProps: { iconName: 'ClearFilter' },
- onClick: () => setFilters(new Map())
- }
+ key: "reset",
+ text: "Clear filters",
+ iconProps: { iconName: "ClearFilter" },
+ onClick: () => setFilters(new Map()),
+ },
];
if (airlockManagerRequests.length > 0) {
quickFilters.unshift({
- key: 'allRequests',
- text: 'All Requests',
- iconProps: { iconName: 'BulletedList' },
+ key: "allRequests",
+ text: "All Requests",
+ iconProps: { iconName: "BulletedList" },
onClick: () => {
setFilters(new Map());
setAirlockRequests(airlockManagerRequests);
- }
+ },
});
quickFilters.unshift({
- key: 'awaitingMyReview',
- text: 'Awaiting my review',
- iconProps: { iconName: 'TemporaryUser' },
+ key: "awaitingMyReview",
+ text: "Awaiting my review",
+ iconProps: { iconName: "TemporaryUser" },
onClick: () => {
- setFilters(new Map([['status', 'in_review']]));
+ setFilters(new Map([["status", "in_review"]]));
setAirlockRequests(airlockManagerRequests);
- }
+ },
});
}
quickFilters.unshift({
- key: 'myRequests',
- text: 'My Requests',
- iconProps: { iconName: 'EditContact' },
+ key: "myRequests",
+ text: "My Requests",
+ iconProps: { iconName: "EditContact" },
onClick: () => {
- setFilters(new Map())
+ setFilters(new Map());
setAirlockRequests(myAirlockRequests);
- }
+ },
});
return (
@@ -301,38 +382,55 @@ export const RequestsList: React.FunctionComponent = () => {
- Airlock Requests
+
+ Airlock Requests
+
getAirlockRequests()}
/>
{apiError && }
-
+
item?.id}
- onItemInvoked={(item) => navigate(`/${ApiEndpoint.Workspaces}/${item.workspaceId}/${ApiEndpoint.AirlockRequests}/${item.id}`)}
+ onItemInvoked={(item) =>
+ navigate(
+ `/${ApiEndpoint.Workspaces}/${item.workspaceId}/${ApiEndpoint.AirlockRequests}/${item.id}`,
+ )
+ }
className="tre-table"
enableShimmer={loadingState === LoadingState.Loading}
/>
{contextMenuProps && }
- {airlockRequests.length === 0 && loadingState !== LoadingState.Loading &&
-
No requests found
- {filters.size > 0
- ? There are no requests matching your selected filter(s).
- : Looks like there are no airlock requests yet. Create a new request to get started.
- }
- }
+ {airlockRequests.length === 0 &&
+ loadingState !== LoadingState.Loading && (
+
+
No requests found
+ {filters.size > 0 ? (
+
+ There are no requests matching your selected filter(s).
+
+ ) : (
+
+ Looks like there are no airlock requests yet. Create a new
+ request to get started.
+
+ )}
+
+ )}
>
);
diff --git a/ui/app/src/components/shared/ResourceBody.tsx b/ui/app/src/components/shared/ResourceBody.tsx
index 28d7d6eecb..0df7391052 100644
--- a/ui/app/src/components/shared/ResourceBody.tsx
+++ b/ui/app/src/components/shared/ResourceBody.tsx
@@ -1,81 +1,103 @@
-import React, { useContext } from 'react';
-import { ResourceDebug } from '../shared/ResourceDebug';
-import { Pivot, PivotItem } from '@fluentui/react';
-import { ResourcePropertyPanel } from '../shared/ResourcePropertyPanel';
-import { Resource } from '../../models/resource';
-import { ResourceHistoryList } from '../shared/ResourceHistoryList';
-import { ResourceOperationsList } from '../shared/ResourceOperationsList';
-import ReactMarkdown from 'react-markdown';
-import remarkGfm from 'remark-gfm';
-import { RoleName, WorkspaceRoleName } from '../../models/roleNames';
-import { ResourceType } from '../../models/resourceType';
-import { SecuredByRole } from './SecuredByRole';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
+import React, { useContext } from "react";
+import { ResourceDebug } from "../shared/ResourceDebug";
+import { Pivot, PivotItem } from "@fluentui/react";
+import { ResourcePropertyPanel } from "../shared/ResourcePropertyPanel";
+import { Resource } from "../../models/resource";
+import { ResourceHistoryList } from "../shared/ResourceHistoryList";
+import { ResourceOperationsList } from "../shared/ResourceOperationsList";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import { RoleName, WorkspaceRoleName } from "../../models/roleNames";
+import { ResourceType } from "../../models/resourceType";
+import { SecuredByRole } from "./SecuredByRole";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
interface ResourceBodyProps {
- resource: Resource,
+ resource: Resource;
readonly?: boolean;
}
-export const ResourceBody: React.FunctionComponent
= (props: ResourceBodyProps) => {
-
+export const ResourceBody: React.FunctionComponent = (
+ props: ResourceBodyProps,
+) => {
const workspaceCtx = useContext(WorkspaceContext);
const operationsRolesByResourceType = {
- [ResourceType.Workspace]: [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner],
+ [ResourceType.Workspace]: [
+ RoleName.TREAdmin,
+ WorkspaceRoleName.WorkspaceOwner,
+ ],
[ResourceType.SharedService]: [RoleName.TREAdmin],
[ResourceType.WorkspaceService]: [WorkspaceRoleName.WorkspaceOwner],
- [ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]
+ [ResourceType.UserResource]: [
+ WorkspaceRoleName.WorkspaceOwner,
+ WorkspaceRoleName.WorkspaceResearcher,
+ ],
};
const historyRolesByResourceType = {
- [ResourceType.Workspace]: [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner],
+ [ResourceType.Workspace]: [
+ RoleName.TREAdmin,
+ WorkspaceRoleName.WorkspaceOwner,
+ ],
[ResourceType.SharedService]: [RoleName.TREAdmin],
[ResourceType.WorkspaceService]: [WorkspaceRoleName.WorkspaceOwner],
- [ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]
+ [ResourceType.UserResource]: [
+ WorkspaceRoleName.WorkspaceOwner,
+ WorkspaceRoleName.WorkspaceResearcher,
+ ],
};
- const operationsRoles = operationsRolesByResourceType[props.resource.resourceType];
+ const operationsRoles =
+ operationsRolesByResourceType[props.resource.resourceType];
const historyRoles = historyRolesByResourceType[props.resource.resourceType];
const workspaceId = workspaceCtx.workspace?.id || "";
return (
-
+
{props.readonly}
- {props.resource.properties?.overview || props.resource.properties?.description}
+
+ {props.resource.properties?.overview ||
+ props.resource.properties?.description}
+
- {
- !props.readonly &&
+ {!props.readonly && (
- }
- {
- !props.readonly && historyRoles &&
+ )}
+ {!props.readonly && historyRoles && (
-
- } />
+ }
+ />
- }
- {
- !props.readonly && operationsRoles &&
+ )}
+ {!props.readonly && operationsRoles && (
-
- } />
+ }
+ />
- }
+ )}
);
};
diff --git a/ui/app/src/components/shared/ResourceCard.tsx b/ui/app/src/components/shared/ResourceCard.tsx
index 10b9c05136..771d6cd5f4 100644
--- a/ui/app/src/components/shared/ResourceCard.tsx
+++ b/ui/app/src/components/shared/ResourceCard.tsx
@@ -1,70 +1,103 @@
-import React, { useCallback, useContext, useState } from 'react';
-import { ComponentAction, VMPowerStates, Resource } from '../../models/resource';
-import { Callout, DefaultPalette, FontWeights, IconButton, IStackStyles, IStyle, mergeStyleSets, PrimaryButton, Shimmer, Stack, Text, TooltipHost } from '@fluentui/react';
-import { useNavigate } from 'react-router-dom';
-import moment from 'moment';
-import { ResourceContextMenu } from './ResourceContextMenu';
-import { useComponentManager } from '../../hooks/useComponentManager';
-import { StatusBadge } from './StatusBadge';
-import { actionsDisabledStates, successStates } from '../../models/operation';
-import { PowerStateBadge } from './PowerStateBadge';
-import { ResourceType } from '../../models/resourceType';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { CostsTag } from './CostsTag';
-import { ConfirmCopyUrlToClipboard } from './ConfirmCopyUrlToClipboard';
-import { AppRolesContext } from '../../contexts/AppRolesContext';
-import { SecuredByRole } from './SecuredByRole';
-import { RoleName, WorkspaceRoleName } from '../../models/roleNames';
-
+import React, { useCallback, useContext, useState } from "react";
+import {
+ ComponentAction,
+ VMPowerStates,
+ Resource,
+} from "../../models/resource";
+import {
+ Callout,
+ DefaultPalette,
+ FontWeights,
+ IconButton,
+ IStackStyles,
+ IStyle,
+ mergeStyleSets,
+ PrimaryButton,
+ Shimmer,
+ Stack,
+ Text,
+ TooltipHost,
+} from "@fluentui/react";
+import { useNavigate } from "react-router-dom";
+import moment from "moment";
+import { ResourceContextMenu } from "./ResourceContextMenu";
+import { useComponentManager } from "../../hooks/useComponentManager";
+import { StatusBadge } from "./StatusBadge";
+import { actionsDisabledStates, successStates } from "../../models/operation";
+import { PowerStateBadge } from "./PowerStateBadge";
+import { ResourceType } from "../../models/resourceType";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { CostsTag } from "./CostsTag";
+import { ConfirmCopyUrlToClipboard } from "./ConfirmCopyUrlToClipboard";
+import { AppRolesContext } from "../../contexts/AppRolesContext";
+import { SecuredByRole } from "./SecuredByRole";
+import { RoleName, WorkspaceRoleName } from "../../models/roleNames";
interface ResourceCardProps {
- resource: Resource,
- itemId: number,
- selectResource?: (resource: Resource) => void,
- onUpdate: (resource: Resource) => void,
- onDelete: (resource: Resource) => void,
+ resource: Resource;
+ itemId: number;
+ selectResource?: (resource: Resource) => void;
+ onUpdate: (resource: Resource) => void;
+ onDelete: (resource: Resource) => void;
readonly?: boolean;
isExposedExternally?: boolean;
}
-export const ResourceCard: React.FunctionComponent = (props: ResourceCardProps) => {
+export const ResourceCard: React.FunctionComponent = (
+ props: ResourceCardProps,
+) => {
const [loading] = useState(false);
const [showCopyUrl, setShowCopyUrl] = useState(false);
const [showInfo, setShowInfo] = useState(false);
const workspaceCtx = useContext(WorkspaceContext);
const latestUpdate = useComponentManager(
props.resource,
- (r: Resource) => { props.onUpdate(r); },
- (r: Resource) => { props.onDelete(r); }
+ (r: Resource) => {
+ props.onUpdate(r);
+ },
+ (r: Resource) => {
+ props.onDelete(r);
+ },
);
const navigate = useNavigate();
const costTagRolesByResourceType = {
- [ResourceType.Workspace]: [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner],
+ [ResourceType.Workspace]: [
+ RoleName.TREAdmin,
+ WorkspaceRoleName.WorkspaceOwner,
+ ],
[ResourceType.SharedService]: [RoleName.TREAdmin],
[ResourceType.WorkspaceService]: [WorkspaceRoleName.WorkspaceOwner],
- [ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner] // when implemented WorkspaceRoleName.WorkspaceResearcher]
+ [ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner], // when implemented WorkspaceRoleName.WorkspaceResearcher]
};
- const costsTagsRoles = costTagRolesByResourceType[props.resource.resourceType];
+ const costsTagsRoles =
+ costTagRolesByResourceType[props.resource.resourceType];
const goToResource = useCallback(() => {
const { resource } = props;
const { resourceType, resourcePath, id } = resource;
// shared services are accessed from the root and the workspace, have to handle the URL differently
- const resourceUrl = (ResourceType.SharedService === resourceType) && (workspaceCtx.workspace.id) ? id : resourcePath;
+ const resourceUrl =
+ ResourceType.SharedService === resourceType && workspaceCtx.workspace.id
+ ? id
+ : resourcePath;
props.selectResource?.(resource);
navigate(resourceUrl);
}, [navigate, props, workspaceCtx.workspace]);
- let connectUri = props.resource.properties && props.resource.properties.connection_uri;
+ let connectUri =
+ props.resource.properties && props.resource.properties.connection_uri;
const shouldDisable = () => {
- return latestUpdate.componentAction === ComponentAction.Lock
- || actionsDisabledStates.includes(props.resource.deploymentStatus)
- || !props.resource.isEnabled
- || (props.resource.azureStatus?.powerState && props.resource.azureStatus.powerState !== VMPowerStates.Running);
+ return (
+ latestUpdate.componentAction === ComponentAction.Lock ||
+ actionsDisabledStates.includes(props.resource.deploymentStatus) ||
+ !props.resource.isEnabled ||
+ (props.resource.azureStatus?.powerState &&
+ props.resource.azureStatus.powerState !== VMPowerStates.Running)
+ );
};
const resourceStatus = latestUpdate.operation?.status
@@ -79,21 +112,31 @@ export const ResourceCard: React.FunctionComponent = (props:
successStates.includes(resourceStatus) &&
props.resource.isEnabled
) {
- headerBadge = ;
+ headerBadge = (
+
+ );
} else {
- headerBadge = ;
+ headerBadge = (
+
+ );
}
const appRoles = useContext(AppRolesContext);
- const authNotProvisioned = props.resource.resourceType === ResourceType.Workspace && !props.resource.properties.scope_id;
- const enableClickOnCard = !authNotProvisioned || appRoles.roles.includes(RoleName.TREAdmin);
- const workspaceId = props.resource.resourceType === ResourceType.Workspace ? props.resource.id : "";
+ const authNotProvisioned =
+ props.resource.resourceType === ResourceType.Workspace &&
+ !props.resource.properties.scope_id;
+ const enableClickOnCard =
+ !authNotProvisioned || appRoles.roles.includes(RoleName.TREAdmin);
+ const workspaceId =
+ props.resource.resourceType === ResourceType.Workspace
+ ? props.resource.id
+ : "";
const cardStyles = enableClickOnCard ? noNavCardStyles : clickableCardStyles;
return (
<>
- {
- loading ?
+ {loading ? (
+
@@ -106,18 +149,28 @@ export const ResourceCard: React.FunctionComponent = (props:
- :
+ ) : (
+
{ if (enableClickOnCard) goToResource(); }}
+ onClick={() => {
+ if (enableClickOnCard) goToResource();
+ }}
>
- {props.resource.properties.display_name}
+
+ {props.resource.properties.display_name}
+
{headerBadge}
@@ -130,7 +183,7 @@ export const ResourceCard: React.FunctionComponent = (props:
{
// Stop onClick triggering parent handler
@@ -140,38 +193,52 @@ export const ResourceCard: React.FunctionComponent = (props:
/>
- {
- !props.readonly &&
- }
+ )}
-
- }
+ }
/>
- {
- connectUri && { e.stopPropagation(); props.isExposedExternally === false ? setShowCopyUrl(true) : window.open(connectUri); }}
+ {connectUri && (
+ {
+ e.stopPropagation();
+ props.isExposedExternally === false
+ ? setShowCopyUrl(true)
+ : window.open(connectUri);
+ }}
disabled={shouldDisable()}
- title={shouldDisable() ? 'Resource must be enabled, successfully deployed & powered on to connect' : 'Connect to resource'}
+ title={
+ shouldDisable()
+ ? "Resource must be enabled, successfully deployed & powered on to connect"
+ : "Connect to resource"
+ }
className={styles.button}
>
Connect
- }
- {
- showCopyUrl && setShowCopyUrl(false)} resource={props.resource} />
- }
+ )}
+ {showCopyUrl && (
+ setShowCopyUrl(false)}
+ resource={props.resource}
+ />
+ )}
- }
- {
- showInfo && = (props:
onDismiss={() => setShowInfo(false)}
setInitialFocus
>
-
+
{props.resource.templateName} ({props.resource.templateVersion})
@@ -189,86 +261,99 @@ export const ResourceCard: React.FunctionComponent = (props:
Resource Id:
- {props.resource.id}
+
+ {props.resource.id}
+
- Last Modified By:
- {props.resource.user.name}
+
+ Last Modified By:
+
+
+ {props.resource.user.name}
+
- Last Updated:
- {moment.unix(props.resource.updatedWhen).toDate().toDateString()}
+
+ Last Updated:
+
+
+ {moment
+ .unix(props.resource.updatedWhen)
+ .toDate()
+ .toDateString()}
+
- }
+ )}
>
);
};
const baseCardStyles: IStyle = {
- width: '100%',
- borderRadius: '5px',
- boxShadow: '0 1.6px 3.6px 0 rgba(0,0,0,.132),0 .3px .9px 0 rgba(0,0,0,.108)',
+ width: "100%",
+ borderRadius: "5px",
+ boxShadow: "0 1.6px 3.6px 0 rgba(0,0,0,.132),0 .3px .9px 0 rgba(0,0,0,.108)",
backgroundColor: DefaultPalette.white,
- padding: 10
+ padding: 10,
};
const noNavCardStyles: IStackStyles = {
- root: { ...baseCardStyles }
+ root: { ...baseCardStyles },
};
const clickableCardStyles: IStackStyles = {
root: {
...baseCardStyles,
"&:hover": {
- transition: 'all .2s ease-in-out',
- transform: 'scale(1.02)',
- cursor: 'pointer'
- }
- }
+ transition: "all .2s ease-in-out",
+ transform: "scale(1.02)",
+ cursor: "pointer",
+ },
+ },
};
const headerStyles: React.CSSProperties = {
- padding: '5px 10px',
- fontSize: '1.2rem',
+ padding: "5px 10px",
+ fontSize: "1.2rem",
};
const bodyStyles: React.CSSProperties = {
- padding: '10px 10px',
- minHeight: '40px'
+ padding: "10px 10px",
+ minHeight: "40px",
};
const footerStyles: React.CSSProperties = {
- minHeight: '30px',
- alignItems: 'center'
+ minHeight: "30px",
+ alignItems: "center",
};
const calloutKeyStyles: React.CSSProperties = {
- width: 160
+ width: 160,
};
const calloutValueStyles: React.CSSProperties = {
- width: 180
+ width: 180,
};
const styles = mergeStyleSets({
button: {
width: 130,
- margin: 10
+ margin: 10,
},
callout: {
width: 350,
- padding: '20px 24px',
+ padding: "20px 24px",
},
title: {
marginBottom: 12,
- fontWeight: FontWeights.semilight
+ fontWeight: FontWeights.semilight,
},
link: {
- display: 'block',
+ display: "block",
marginTop: 20,
- }
+ },
});
diff --git a/ui/app/src/components/shared/ResourceCardList.tsx b/ui/app/src/components/shared/ResourceCardList.tsx
index 512ae4bb62..25ae3067e9 100644
--- a/ui/app/src/components/shared/ResourceCardList.tsx
+++ b/ui/app/src/components/shared/ResourceCardList.tsx
@@ -1,60 +1,72 @@
-import React, { } from 'react';
+import React from "react";
-import { IStackStyles, IStackTokens, Stack, Text } from '@fluentui/react';
-import { ResourceCard } from '../shared/ResourceCard';
-import { Resource } from '../../models/resource';
+import { IStackStyles, IStackTokens, Stack, Text } from "@fluentui/react";
+import { ResourceCard } from "../shared/ResourceCard";
+import { Resource } from "../../models/resource";
interface ResourceCardListProps {
- resources: Array,
- selectResource?: (resource: Resource) => void,
- updateResource: (resource: Resource) => void,
- removeResource: (resource: Resource) => void
- emptyText: string,
- readonly?: boolean
- isExposedExternally?: boolean
+ resources: Array;
+ selectResource?: (resource: Resource) => void;
+ updateResource: (resource: Resource) => void;
+ removeResource: (resource: Resource) => void;
+ emptyText: string;
+ readonly?: boolean;
+ isExposedExternally?: boolean;
}
-export const ResourceCardList: React.FunctionComponent = (props: ResourceCardListProps) => {
-
+export const ResourceCardList: React.FunctionComponent<
+ ResourceCardListProps
+> = (props: ResourceCardListProps) => {
return (
<>
- {
- props.resources.length > 0 ?
-
- {
- props.resources.map((r:Resource, i:number) => {
- return (
-
- props.selectResource && props.selectResource(resource)}
- onUpdate={(resource: Resource) => props.updateResource(resource)}
- onDelete={(resource: Resource) => props.removeResource(resource)}
- itemId={i}
- readonly={props.readonly}
- isExposedExternally={r.properties.is_exposed_externally === undefined ? props.isExposedExternally : r.properties.is_exposed_externally} />
-
- )
- })
- }
- :
- {props.emptyText}
- }
+ {props.resources.length > 0 ? (
+
+ {props.resources.map((r: Resource, i: number) => {
+ return (
+
+
+ props.selectResource && props.selectResource(resource)
+ }
+ onUpdate={(resource: Resource) =>
+ props.updateResource(resource)
+ }
+ onDelete={(resource: Resource) =>
+ props.removeResource(resource)
+ }
+ itemId={i}
+ readonly={props.readonly}
+ isExposedExternally={
+ r.properties.is_exposed_externally === undefined
+ ? props.isExposedExternally
+ : r.properties.is_exposed_externally
+ }
+ />
+
+ );
+ })}
+
+ ) : (
+
+ {props.emptyText}
+
+ )}
>
);
};
const stackStyles: IStackStyles = {
root: {
- width: 'calc(100% - 20px)'
+ width: "calc(100% - 20px)",
},
};
const wrapStackTokens: IStackTokens = { childrenGap: 20 };
const gridItemStyles: React.CSSProperties = {
- alignItems: 'left',
- display: 'flex',
+ alignItems: "left",
+ display: "flex",
width: 300,
- background: '#f9f9f9'
+ background: "#f9f9f9",
};
diff --git a/ui/app/src/components/shared/ResourceContextMenu.tsx b/ui/app/src/components/shared/ResourceContextMenu.tsx
index d3c714b4e0..20ca6be280 100644
--- a/ui/app/src/components/shared/ResourceContextMenu.tsx
+++ b/ui/app/src/components/shared/ResourceContextMenu.tsx
@@ -1,42 +1,61 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { ComponentAction, VMPowerStates, Resource } from '../../models/resource';
-import { CommandBar, IconButton, IContextualMenuItem, IContextualMenuProps } from '@fluentui/react';
-import { RoleName, WorkspaceRoleName } from '../../models/roleNames';
-import { SecuredByRole } from './SecuredByRole';
-import { ResourceType } from '../../models/resourceType';
-import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { UserResource } from '../../models/userResource';
-import { getActionIcon, ResourceTemplate, TemplateAction } from '../../models/resourceTemplate';
-import { ConfirmDeleteResource } from './ConfirmDeleteResource';
-import { ConfirmCopyUrlToClipboard } from './ConfirmCopyUrlToClipboard';
-import { ConfirmDisableEnableResource } from './ConfirmDisableEnableResource';
-import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
-import { Workspace } from '../../models/workspace';
-import { WorkspaceService } from '../../models/workspaceService';
-import { actionsDisabledStates } from '../../models/operation';
-import { AppRolesContext } from '../../contexts/AppRolesContext';
-import { useAppDispatch } from '../../hooks/customReduxHooks';
-import { addUpdateOperation } from '../shared/notifications/operationsSlice';
-import { ConfirmUpgradeResource } from './ConfirmUpgradeResource';
+import React, { useContext, useEffect, useState } from "react";
+import {
+ ComponentAction,
+ VMPowerStates,
+ Resource,
+} from "../../models/resource";
+import {
+ CommandBar,
+ IconButton,
+ IContextualMenuItem,
+ IContextualMenuProps,
+} from "@fluentui/react";
+import { RoleName, WorkspaceRoleName } from "../../models/roleNames";
+import { SecuredByRole } from "./SecuredByRole";
+import { ResourceType } from "../../models/resourceType";
+import { HttpMethod, useAuthApiCall } from "../../hooks/useAuthApiCall";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { UserResource } from "../../models/userResource";
+import {
+ getActionIcon,
+ ResourceTemplate,
+ TemplateAction,
+} from "../../models/resourceTemplate";
+import { ConfirmDeleteResource } from "./ConfirmDeleteResource";
+import { ConfirmCopyUrlToClipboard } from "./ConfirmCopyUrlToClipboard";
+import { ConfirmDisableEnableResource } from "./ConfirmDisableEnableResource";
+import { CreateUpdateResourceContext } from "../../contexts/CreateUpdateResourceContext";
+import { Workspace } from "../../models/workspace";
+import { WorkspaceService } from "../../models/workspaceService";
+import { actionsDisabledStates } from "../../models/operation";
+import { AppRolesContext } from "../../contexts/AppRolesContext";
+import { useAppDispatch } from "../../hooks/customReduxHooks";
+import { addUpdateOperation } from "../shared/notifications/operationsSlice";
+import { ConfirmUpgradeResource } from "./ConfirmUpgradeResource";
interface ResourceContextMenuProps {
- resource: Resource,
- componentAction: ComponentAction,
- commandBar?: boolean
+ resource: Resource;
+ componentAction: ComponentAction;
+ commandBar?: boolean;
}
-export const ResourceContextMenu: React.FunctionComponent = (props: ResourceContextMenuProps) => {
+export const ResourceContextMenu: React.FunctionComponent<
+ ResourceContextMenuProps
+> = (props: ResourceContextMenuProps) => {
const apiCall = useAuthApiCall();
const workspaceCtx = useContext(WorkspaceContext);
const [showDisable, setShowDisable] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [showCopyUrl, setShowCopyUrl] = useState(false);
const [showUpgrade, setShowUpgrade] = useState(false);
- const [resourceTemplate, setResourceTemplate] = useState({} as ResourceTemplate);
+ const [resourceTemplate, setResourceTemplate] = useState(
+ {} as ResourceTemplate,
+ );
const createFormCtx = useContext(CreateUpdateResourceContext);
- const [parentResource, setParentResource] = useState({} as WorkspaceService | Workspace);
+ const [parentResource, setParentResource] = useState(
+ {} as WorkspaceService | Workspace,
+ );
const [roles, setRoles] = useState([] as Array);
const appRoles = useContext(AppRolesContext); // the user is in these roles which apply across the app
const dispatch = useAppDispatch();
@@ -48,22 +67,28 @@ export const ResourceContextMenu: React.FunctionComponent;
@@ -73,11 +98,15 @@ export const ResourceContextMenu: React.FunctionComponent userRoles.includes(x)).length > 0) {
- const template = await apiCall(`${templatesPath}/${props.resource.templateName}`, HttpMethod.Get);
+ if (userRoles && r.filter((x) => userRoles.includes(x)).length > 0) {
+ const template = await apiCall(
+ `${templatesPath}/${props.resource.templateName}`,
+ HttpMethod.Get,
+ );
setResourceTemplate(template);
}
};
@@ -97,151 +129,197 @@ export const ResourceContextMenu: React.FunctionComponent {
- const action = await apiCall(`${props.resource.resourcePath}/${ApiEndpoint.InvokeAction}?action=${actionName}`, HttpMethod.Post, workspaceCtx.workspaceApplicationIdURI);
- action && action.operation && dispatch(addUpdateOperation(action.operation));
- }
+ const action = await apiCall(
+ `${props.resource.resourcePath}/${ApiEndpoint.InvokeAction}?action=${actionName}`,
+ HttpMethod.Post,
+ workspaceCtx.workspaceApplicationIdURI,
+ );
+ action &&
+ action.operation &&
+ dispatch(addUpdateOperation(action.operation));
+ };
// context menu
let menuItems: Array = [];
menuItems = [
{
- key: 'update',
- text: 'Update',
- iconProps: { iconName: 'WindowEdit' },
- onClick: () => createFormCtx.openCreateForm({
- resourceType: props.resource.resourceType,
- updateResource: props.resource,
- resourceParent: parentResource,
- workspaceApplicationIdURI: workspaceCtx.workspaceApplicationIdURI,
- }),
- disabled: (props.componentAction === ComponentAction.Lock)
+ key: "update",
+ text: "Update",
+ iconProps: { iconName: "WindowEdit" },
+ onClick: () =>
+ createFormCtx.openCreateForm({
+ resourceType: props.resource.resourceType,
+ updateResource: props.resource,
+ resourceParent: parentResource,
+ workspaceApplicationIdURI: workspaceCtx.workspaceApplicationIdURI,
+ }),
+ disabled: props.componentAction === ComponentAction.Lock,
},
{
- key: 'disable',
- text: props.resource.isEnabled ? 'Disable' : 'Enable',
- iconProps: { iconName: props.resource.isEnabled ? 'CirclePause' : 'PlayResume' },
+ key: "disable",
+ text: props.resource.isEnabled ? "Disable" : "Enable",
+ iconProps: {
+ iconName: props.resource.isEnabled ? "CirclePause" : "PlayResume",
+ },
onClick: () => setShowDisable(true),
- disabled: (props.componentAction === ComponentAction.Lock)
+ disabled: props.componentAction === ComponentAction.Lock,
},
{
- key: 'delete',
- text: 'Delete',
- title: props.resource.isEnabled ? 'Resource must be disabled before deleting' : 'Delete this resource',
- iconProps: { iconName: 'Delete' },
+ key: "delete",
+ text: "Delete",
+ title: props.resource.isEnabled
+ ? "Resource must be disabled before deleting"
+ : "Delete this resource",
+ iconProps: { iconName: "Delete" },
onClick: () => setShowDelete(true),
- disabled: (props.resource.isEnabled || props.componentAction === ComponentAction.Lock)
+ disabled:
+ props.resource.isEnabled ||
+ props.componentAction === ComponentAction.Lock,
},
];
const shouldDisableConnect = () => {
- return props.componentAction === ComponentAction.Lock
- || actionsDisabledStates.includes(props.resource.deploymentStatus)
- || !props.resource.isEnabled
- || (props.resource.azureStatus?.powerState && props.resource.azureStatus.powerState !== VMPowerStates.Running);
- }
+ return (
+ props.componentAction === ComponentAction.Lock ||
+ actionsDisabledStates.includes(props.resource.deploymentStatus) ||
+ !props.resource.isEnabled ||
+ (props.resource.azureStatus?.powerState &&
+ props.resource.azureStatus.powerState !== VMPowerStates.Running)
+ );
+ };
// add 'connect' button if we have a URL to connect to
- if(props.resource.properties.connection_uri){
+ if (props.resource.properties.connection_uri) {
if (props.resource.properties.is_exposed_externally === true) {
menuItems.push({
- key: 'connect',
- text: 'Connect',
- title: shouldDisableConnect() ? 'Resource must be deployed, enabled & powered on to connect' : 'Connect to resource',
- iconProps: { iconName: 'PlugConnected' },
- onClick: () => { window.open(props.resource.properties.connection_uri, '_blank') },
- disabled: shouldDisableConnect()
- })
- }
- else if (props.resource.properties.is_exposed_externally === false) {
+ key: "connect",
+ text: "Connect",
+ title: shouldDisableConnect()
+ ? "Resource must be deployed, enabled & powered on to connect"
+ : "Connect to resource",
+ iconProps: { iconName: "PlugConnected" },
+ onClick: () => {
+ window.open(props.resource.properties.connection_uri, "_blank");
+ },
+ disabled: shouldDisableConnect(),
+ });
+ } else if (props.resource.properties.is_exposed_externally === false) {
menuItems.push({
- key: 'connect',
- text: 'Connect',
- title: shouldDisableConnect() ? 'Resource must be deployed, enabled & powered on to connect' : 'Connect to resource',
- iconProps: { iconName: 'PlugConnected' },
+ key: "connect",
+ text: "Connect",
+ title: shouldDisableConnect()
+ ? "Resource must be deployed, enabled & powered on to connect"
+ : "Connect to resource",
+ iconProps: { iconName: "PlugConnected" },
onClick: () => setShowCopyUrl(true),
- disabled: shouldDisableConnect()
- })
+ disabled: shouldDisableConnect(),
+ });
}
}
-
const shouldDisableActions = () => {
- return props.componentAction === ComponentAction.Lock
- || actionsDisabledStates.includes(props.resource.deploymentStatus)
- || !props.resource.isEnabled;
- }
+ return (
+ props.componentAction === ComponentAction.Lock ||
+ actionsDisabledStates.includes(props.resource.deploymentStatus) ||
+ !props.resource.isEnabled
+ );
+ };
// add custom actions if we have any
- if (resourceTemplate && resourceTemplate.customActions && resourceTemplate.customActions.length > 0) {
+ if (
+ resourceTemplate &&
+ resourceTemplate.customActions &&
+ resourceTemplate.customActions.length > 0
+ ) {
let customActions: Array = [];
resourceTemplate.customActions.forEach((a: TemplateAction) => {
- customActions.push(
- {
- key: a.name,
- text: a.name,
- title: a.description,
- iconProps: { iconName: getActionIcon(a.name) },
- className: 'tre-context-menu',
- onClick: () => { doAction(a.name) }
- }
- );
+ customActions.push({
+ key: a.name,
+ text: a.name,
+ title: a.description,
+ iconProps: { iconName: getActionIcon(a.name) },
+ className: "tre-context-menu",
+ onClick: () => {
+ doAction(a.name);
+ },
+ });
});
menuItems.push({
- key: 'custom-actions',
- text: 'Actions',
- title: shouldDisableActions() ? 'Resource must be deployed and enabled to perform actions': 'Custom Actions',
- iconProps: { iconName: 'Asterisk' },
+ key: "custom-actions",
+ text: "Actions",
+ title: shouldDisableActions()
+ ? "Resource must be deployed and enabled to perform actions"
+ : "Custom Actions",
+ iconProps: { iconName: "Asterisk" },
disabled: shouldDisableActions(),
- subMenuProps: { items: customActions }
+ subMenuProps: { items: customActions },
});
}
// add 'upgrade' button if we have available template upgrades
- const nonMajorUpgrades = props.resource.availableUpgrades?.filter(upgrade => !upgrade.forceUpdateRequired)
+ const nonMajorUpgrades = props.resource.availableUpgrades?.filter(
+ (upgrade) => !upgrade.forceUpdateRequired,
+ );
if (nonMajorUpgrades?.length > 0) {
menuItems.push({
- key: 'upgrade',
- text: 'Upgrade',
- title: 'Upgrade this resource template version',
- iconProps: { iconName: 'Refresh' },
+ key: "upgrade",
+ text: "Upgrade",
+ title: "Upgrade this resource template version",
+ iconProps: { iconName: "Refresh" },
onClick: () => setShowUpgrade(true),
- disabled: (props.componentAction === ComponentAction.Lock)
- })
+ disabled: props.componentAction === ComponentAction.Lock,
+ });
}
const menuProps: IContextualMenuProps = {
shouldFocusOnMount: true,
- items: menuItems
+ items: menuItems,
};
return (
<>
-
+ ) : (
+
+ )
+ }
+ />
+ {showDisable && (
+ setShowDisable(false)}
+ resource={props.resource}
+ isEnabled={!props.resource.isEnabled}
/>
- :
-
- } />
- {
- showDisable &&
- setShowDisable(false)} resource={props.resource} isEnabled={!props.resource.isEnabled} />
- }
- {
- showDelete &&
- setShowDelete(false)} resource={props.resource} />
- }
- {
- showCopyUrl &&
- setShowCopyUrl(false)} resource={props.resource} />
- }
- {
- showUpgrade &&
- setShowUpgrade(false)} resource={props.resource} />
- }
+ )}
+ {showDelete && (
+ setShowDelete(false)}
+ resource={props.resource}
+ />
+ )}
+ {showCopyUrl && (
+ setShowCopyUrl(false)}
+ resource={props.resource}
+ />
+ )}
+ {showUpgrade && (
+ setShowUpgrade(false)}
+ resource={props.resource}
+ />
+ )}
>
- )
+ );
};
diff --git a/ui/app/src/components/shared/ResourceDebug.tsx b/ui/app/src/components/shared/ResourceDebug.tsx
index 79024a6e77..581b52e306 100644
--- a/ui/app/src/components/shared/ResourceDebug.tsx
+++ b/ui/app/src/components/shared/ResourceDebug.tsx
@@ -1,35 +1,35 @@
-import React from 'react';
-import { Resource } from '../../models/resource';
-import config from '../../config.json';
+import React from "react";
+import { Resource } from "../../models/resource";
+import config from "../../config.json";
interface ResourceDebugProps {
- resource: Resource
+ resource: Resource;
}
-export const ResourceDebug: React.FunctionComponent = (props: ResourceDebugProps) => {
-
- return (
- config.debug === true ?
+export const ResourceDebug: React.FunctionComponent = (
+ props: ResourceDebugProps,
+) => {
+ return config.debug === true ? (
<>
-
- Debug details:
+
+ Debug details:
- {
- Object.keys(props.resource).map((key, i) => {
- let val = typeof ((props.resource as any)[key]) === 'object' ?
- JSON.stringify((props.resource as any)[key]) :
- (props.resource as any)[key].toString()
+ {Object.keys(props.resource).map((key, i) => {
+ let val =
+ typeof (props.resource as any)[key] === "object"
+ ? JSON.stringify((props.resource as any)[key])
+ : (props.resource as any)[key].toString();
- return (
-
- {key}: {val}
-
- )
- })
- }
+ return (
+
+ {key}:
+ {val}
+
+ );
+ })}
- > :
- <>>
- )
+ >
+ ) : (
+ <>>
+ );
};
-
diff --git a/ui/app/src/components/shared/ResourceHeader.tsx b/ui/app/src/components/shared/ResourceHeader.tsx
index 59c392901d..0194b965da 100644
--- a/ui/app/src/components/shared/ResourceHeader.tsx
+++ b/ui/app/src/components/shared/ResourceHeader.tsx
@@ -1,38 +1,53 @@
-import React from 'react';
-import { ProgressIndicator, Stack } from '@fluentui/react';
-import { ResourceContextMenu } from '../shared/ResourceContextMenu';
-import { ComponentAction, Resource, ResourceUpdate } from '../../models/resource';
-import { StatusBadge } from './StatusBadge';
-import { PowerStateBadge } from './PowerStateBadge';
+import React from "react";
+import { ProgressIndicator, Stack } from "@fluentui/react";
+import { ResourceContextMenu } from "../shared/ResourceContextMenu";
+import {
+ ComponentAction,
+ Resource,
+ ResourceUpdate,
+} from "../../models/resource";
+import { StatusBadge } from "./StatusBadge";
+import { PowerStateBadge } from "./PowerStateBadge";
interface ResourceHeaderProps {
- resource: Resource,
- latestUpdate: ResourceUpdate,
- readonly?: boolean
+ resource: Resource;
+ latestUpdate: ResourceUpdate;
+ readonly?: boolean;
}
-export const ResourceHeader: React.FunctionComponent = (props: ResourceHeaderProps) => {
-
+export const ResourceHeader: React.FunctionComponent = (
+ props: ResourceHeaderProps,
+) => {
return (
<>
- {props.resource && props.resource.id &&
+ {props.resource && props.resource.id && (
-
+
-
-
+
+
{props.resource.properties?.display_name}
- {
- (props.resource.azureStatus?.powerState) &&
-
- }
+ {props.resource.azureStatus?.powerState && (
+
+ )}
- {
- (props.latestUpdate.operation || props.resource.deploymentStatus) &&
+ {(props.latestUpdate.operation ||
+ props.resource.deploymentStatus) && (
= (pro
}
/>
- }
+ )}
- {
- !props.readonly &&
+ {!props.readonly && (
= (pro
componentAction={props.latestUpdate.componentAction}
/>
- }
+ )}
- {
- props.latestUpdate.componentAction === ComponentAction.Lock &&
+ {props.latestUpdate.componentAction === ComponentAction.Lock && (
- }
+ )}
- }
+ )}
>
);
};
diff --git a/ui/app/src/components/shared/ResourceHistoryList.tsx b/ui/app/src/components/shared/ResourceHistoryList.tsx
index b0e42e6d84..b4a377c8c9 100644
--- a/ui/app/src/components/shared/ResourceHistoryList.tsx
+++ b/ui/app/src/components/shared/ResourceHistoryList.tsx
@@ -1,87 +1,130 @@
import { IStackStyles, Spinner, SpinnerSize, Stack } from "@fluentui/react";
-import React, { useEffect, useContext, useState } from 'react';
-import { useParams } from 'react-router-dom';
-import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { HistoryItem, Resource } from '../../models/resource';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { ResourceHistoryListItem } from './ResourceHistoryListItem';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import config from '../../config.json';
+import React, { useEffect, useContext, useState } from "react";
+import { useParams } from "react-router-dom";
+import { HttpMethod, useAuthApiCall } from "../../hooks/useAuthApiCall";
+import { HistoryItem, Resource } from "../../models/resource";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { ResourceHistoryListItem } from "./ResourceHistoryListItem";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import config from "../../config.json";
import moment from "moment";
import { APIError } from "../../models/exceptions";
import { LoadingState } from "../../models/loadingState";
import { ExceptionLayout } from "./ExceptionLayout";
-
interface ResourceHistoryListProps {
- resource: Resource
+ resource: Resource;
}
-export const ResourceHistoryList: React.FunctionComponent = (props: ResourceHistoryListProps) => {
+export const ResourceHistoryList: React.FunctionComponent<
+ ResourceHistoryListProps
+> = (props: ResourceHistoryListProps) => {
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const workspaceCtx = useContext(WorkspaceContext);
const { resourceId } = useParams();
- const [resourceHistory, setResourceHistory] = useState([] as Array)
- const [loadingState, setLoadingState] = useState('loading');
+ const [resourceHistory, setResourceHistory] = useState(
+ [] as Array,
+ );
+ const [loadingState, setLoadingState] = useState("loading");
useEffect(() => {
const getResourceHistory = async () => {
try {
// get resource operations
- const scopeId = workspaceCtx.roles.length > 0 ? workspaceCtx.workspaceApplicationIdURI : "";
- const history = await apiCall(`${props.resource.resourcePath}/${ApiEndpoint.History}`, HttpMethod.Get, scopeId);
- config.debug && console.log(`Got resource history, for resource:${props.resource.id}: ${history.resource_history}`);
+ const scopeId =
+ workspaceCtx.roles.length > 0
+ ? workspaceCtx.workspaceApplicationIdURI
+ : "";
+ const history = await apiCall(
+ `${props.resource.resourcePath}/${ApiEndpoint.History}`,
+ HttpMethod.Get,
+ scopeId,
+ );
+ config.debug &&
+ console.log(
+ `Got resource history, for resource:${props.resource.id}: ${history.resource_history}`,
+ );
setResourceHistory(history.resource_history.reverse());
setLoadingState(history ? LoadingState.Ok : LoadingState.Error);
} catch (err: any) {
- err.userMessage = "Error retrieving resource history"
+ err.userMessage = "Error retrieving resource history";
setApiError(err);
setLoadingState(LoadingState.Error);
}
};
getResourceHistory();
- }, [apiCall, props.resource, resourceId, workspaceCtx.workspaceApplicationIdURI, workspaceCtx.roles]);
-
+ }, [
+ apiCall,
+ props.resource,
+ resourceId,
+ workspaceCtx.workspaceApplicationIdURI,
+ workspaceCtx.roles,
+ ]);
const stackStyles: IStackStyles = {
root: {
padding: 0,
- minWidth: 300
- }
+ minWidth: 300,
+ },
};
switch (loadingState) {
case LoadingState.Ok:
return (
<>
- {
- resourceHistory && resourceHistory.map((history: HistoryItem, i: number) => {
+ {resourceHistory &&
+ resourceHistory.map((history: HistoryItem, i: number) => {
return (
-
+
-
-
-
-
-
-
+
+
+
+
+
+
- )
- })
- }
+ );
+ })}
>
);
case LoadingState.Error:
- return (
-
- )
+ return ;
default:
return (
-
-
+
+
- )
+ );
}
};
diff --git a/ui/app/src/components/shared/ResourceHistoryListItem.tsx b/ui/app/src/components/shared/ResourceHistoryListItem.tsx
index 7b110c8a49..08b4b747f5 100644
--- a/ui/app/src/components/shared/ResourceHistoryListItem.tsx
+++ b/ui/app/src/components/shared/ResourceHistoryListItem.tsx
@@ -1,28 +1,29 @@
import { DefaultPalette, IStackItemStyles, Stack } from "@fluentui/react";
interface ResourceHistoryListItemProps {
- header: String,
- val: String
+ header: String;
+ val: String;
}
-export const ResourceHistoryListItem: React.FunctionComponent
= (props: ResourceHistoryListItemProps) => {
-
- const stackItemStyles: IStackItemStyles = {
- root: {
- padding: '5px 0',
- color: DefaultPalette.neutralSecondary
- }
- }
- return(
- <>
-
-
- {props.header}
-
-
- : {props.val}
-
-
- >
- );
-}
+export const ResourceHistoryListItem: React.FunctionComponent<
+ ResourceHistoryListItemProps
+> = (props: ResourceHistoryListItemProps) => {
+ const stackItemStyles: IStackItemStyles = {
+ root: {
+ padding: "5px 0",
+ color: DefaultPalette.neutralSecondary,
+ },
+ };
+ return (
+ <>
+
+
+ {props.header}
+
+
+ : {props.val}
+
+
+ >
+ );
+};
diff --git a/ui/app/src/components/shared/ResourceOperationListItem.tsx b/ui/app/src/components/shared/ResourceOperationListItem.tsx
index c7f439a37f..45c830a07c 100644
--- a/ui/app/src/components/shared/ResourceOperationListItem.tsx
+++ b/ui/app/src/components/shared/ResourceOperationListItem.tsx
@@ -1,28 +1,29 @@
import { DefaultPalette, IStackItemStyles, Stack } from "@fluentui/react";
interface ResourceOperationListItemProps {
- header: String,
- val: String
+ header: String;
+ val: String;
}
-export const ResourceOperationListItem: React.FunctionComponent = (props: ResourceOperationListItemProps) => {
-
- const stackItemStyles: IStackItemStyles = {
- root: {
- padding: '5px 0',
- color: DefaultPalette.neutralSecondary
- }
- }
- return(
- <>
-
-
- {props.header}
-
-
- : {props.val}
-
-
- >
- );
-}
+export const ResourceOperationListItem: React.FunctionComponent<
+ ResourceOperationListItemProps
+> = (props: ResourceOperationListItemProps) => {
+ const stackItemStyles: IStackItemStyles = {
+ root: {
+ padding: "5px 0",
+ color: DefaultPalette.neutralSecondary,
+ },
+ };
+ return (
+ <>
+
+
+ {props.header}
+
+
+ : {props.val}
+
+
+ >
+ );
+};
diff --git a/ui/app/src/components/shared/ResourceOperationStepsList.tsx b/ui/app/src/components/shared/ResourceOperationStepsList.tsx
index cacc8e4e5b..0659a3c3d8 100644
--- a/ui/app/src/components/shared/ResourceOperationStepsList.tsx
+++ b/ui/app/src/components/shared/ResourceOperationStepsList.tsx
@@ -2,38 +2,40 @@ import { DefaultPalette, IStackItemStyles, Stack } from "@fluentui/react";
import { OperationStep } from "../../models/operation";
interface ResourceOperationStepsListProps {
- header: String,
- val?: OperationStep[]
+ header: String;
+ val?: OperationStep[];
}
-export const ResourceOperationStepsList: React.FunctionComponent = (props: ResourceOperationStepsListProps) => {
-
+export const ResourceOperationStepsList: React.FunctionComponent<
+ ResourceOperationStepsListProps
+> = (props: ResourceOperationStepsListProps) => {
const stackItemStyles: IStackItemStyles = {
root: {
- padding: '5px 0',
- color: DefaultPalette.neutralSecondary
- }
- }
+ padding: "5px 0",
+ color: DefaultPalette.neutralSecondary,
+ },
+ };
return (
-
+
{props.header}
-
+
{props.val?.map((step: OperationStep, i: number) => {
return (
-
- {i + 1}{')'} {step.stepTitle}
+
+ {i + 1}
+ {")"} {step.stepTitle}
{step.message}
- )
+ );
})}
);
-}
+};
diff --git a/ui/app/src/components/shared/ResourceOperationsList.tsx b/ui/app/src/components/shared/ResourceOperationsList.tsx
index a0ee6695c3..5d3605f6e6 100644
--- a/ui/app/src/components/shared/ResourceOperationsList.tsx
+++ b/ui/app/src/components/shared/ResourceOperationsList.tsx
@@ -1,13 +1,13 @@
import { IStackStyles, Spinner, SpinnerSize, Stack } from "@fluentui/react";
-import React, { useEffect, useContext, useState } from 'react';
-import { useParams } from 'react-router-dom';
-import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { Operation } from '../../models/operation';
-import { Resource } from '../../models/resource';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { ResourceOperationListItem } from './ResourceOperationListItem';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import config from '../../config.json';
+import React, { useEffect, useContext, useState } from "react";
+import { useParams } from "react-router-dom";
+import { HttpMethod, useAuthApiCall } from "../../hooks/useAuthApiCall";
+import { Operation } from "../../models/operation";
+import { Resource } from "../../models/resource";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { ResourceOperationListItem } from "./ResourceOperationListItem";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import config from "../../config.json";
import moment from "moment";
import { APIError } from "../../models/exceptions";
import { LoadingState } from "../../models/loadingState";
@@ -15,78 +15,138 @@ import { ExceptionLayout } from "./ExceptionLayout";
import { ResourceOperationStepsList } from "./ResourceOperationStepsList";
interface ResourceOperationsListProps {
- resource: Resource
+ resource: Resource;
}
-export const ResourceOperationsList: React.FunctionComponent = (props: ResourceOperationsListProps) => {
+export const ResourceOperationsList: React.FunctionComponent<
+ ResourceOperationsListProps
+> = (props: ResourceOperationsListProps) => {
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const workspaceCtx = useContext(WorkspaceContext);
const { resourceId } = useParams();
- const [resourceOperations, setResourceOperations] = useState([] as Array)
- const [loadingState, setLoadingState] = useState('loading');
+ const [resourceOperations, setResourceOperations] = useState(
+ [] as Array,
+ );
+ const [loadingState, setLoadingState] = useState("loading");
useEffect(() => {
const getOperations = async () => {
try {
// get resource operations
- const scopeId = workspaceCtx.roles && workspaceCtx.roles.length > 0 ? workspaceCtx.workspaceApplicationIdURI : "";
- const ops = await apiCall(`${props.resource.resourcePath}/${ApiEndpoint.Operations}`, HttpMethod.Get, scopeId);
- config.debug && console.log(`Got resource operations, for resource:${props.resource.id}: ${ops.operations}`);
+ const scopeId =
+ workspaceCtx.roles && workspaceCtx.roles.length > 0
+ ? workspaceCtx.workspaceApplicationIdURI
+ : "";
+ const ops = await apiCall(
+ `${props.resource.resourcePath}/${ApiEndpoint.Operations}`,
+ HttpMethod.Get,
+ scopeId,
+ );
+ config.debug &&
+ console.log(
+ `Got resource operations, for resource:${props.resource.id}: ${ops.operations}`,
+ );
setResourceOperations(ops.operations.reverse());
- setLoadingState(ops && ops.operations.length > 0 ? LoadingState.Ok : LoadingState.Error);
+ setLoadingState(
+ ops && ops.operations.length > 0
+ ? LoadingState.Ok
+ : LoadingState.Error,
+ );
} catch (err: any) {
- err.userMessage = "Error retrieving resource operations"
+ err.userMessage = "Error retrieving resource operations";
setApiError(err);
setLoadingState(LoadingState.Error);
}
};
getOperations();
- }, [apiCall, props.resource, resourceId, workspaceCtx.roles, workspaceCtx.workspaceApplicationIdURI]);
-
+ }, [
+ apiCall,
+ props.resource,
+ resourceId,
+ workspaceCtx.roles,
+ workspaceCtx.workspaceApplicationIdURI,
+ ]);
const stackStyles: IStackStyles = {
root: {
padding: 0,
- minWidth: 300
- }
+ minWidth: 300,
+ },
};
switch (loadingState) {
case LoadingState.Ok:
return (
<>
- {
- resourceOperations && resourceOperations.map((op: Operation, i: number) => {
+ {resourceOperations &&
+ resourceOperations.map((op: Operation, i: number) => {
return (
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
- )
- })
- }
+ );
+ })}
>
);
case LoadingState.Error:
- return (
-
- )
+ return ;
default:
return (
-
-
+
+
- )
+ );
}
};
diff --git a/ui/app/src/components/shared/ResourcePropertyPanel.tsx b/ui/app/src/components/shared/ResourcePropertyPanel.tsx
index 9169d36103..82b12faf7f 100644
--- a/ui/app/src/components/shared/ResourcePropertyPanel.tsx
+++ b/ui/app/src/components/shared/ResourcePropertyPanel.tsx
@@ -1,41 +1,51 @@
-import { DefaultPalette, IStackItemStyles, IStackStyles, Stack } from "@fluentui/react";
+import {
+ DefaultPalette,
+ IStackItemStyles,
+ IStackStyles,
+ Stack,
+} from "@fluentui/react";
import moment from "moment";
import React from "react";
import { Resource } from "../../models/resource";
import { ComplexPropertyModal } from "./ComplexItemDisplay";
interface ResourcePropertyPanelProps {
- resource: Resource
+ resource: Resource;
}
interface ResourcePropertyPanelItemProps {
- header: string,
- val: any
+ header: string;
+ val: any;
}
-export const ResourcePropertyPanelItem: React.FunctionComponent
= (props: ResourcePropertyPanelItemProps) => {
-
+export const ResourcePropertyPanelItem: React.FunctionComponent<
+ ResourcePropertyPanelItemProps
+> = (props: ResourcePropertyPanelItemProps) => {
const stackItemStyles: IStackItemStyles = {
root: {
padding: 5,
width: 150,
color: DefaultPalette.neutralSecondary,
- wordBreak: 'break-all'
- }
- }
+ wordBreak: "break-all",
+ },
+ };
function renderValue(val: any, title: string) {
- if (typeof (val) === "string") {
- if (val && val.startsWith('https://')) {
- return ({val} )
+ if (typeof val === "string") {
+ if (val && val.startsWith("https://")) {
+ return (
+
+ {val}
+
+ );
}
- return val
+ return val;
}
- if (typeof (val) === "object")
- return
+ if (typeof val === "object")
+ return ;
- return val.toString()
+ return val.toString();
}
return (
@@ -50,47 +60,80 @@ export const ResourcePropertyPanelItem: React.FunctionComponent
>
);
-}
-
-export const ResourcePropertyPanel: React.FunctionComponent = (props: ResourcePropertyPanelProps) => {
+};
+export const ResourcePropertyPanel: React.FunctionComponent<
+ ResourcePropertyPanelProps
+> = (props: ResourcePropertyPanelProps) => {
const stackStyles: IStackStyles = {
root: {
padding: 0,
- minWidth: 300
- }
+ minWidth: 300,
+ },
};
function userFriendlyKey(key: String) {
- let friendlyKey = key.replaceAll('_', ' ');
- return friendlyKey.charAt(0).toUpperCase() + friendlyKey.slice(1).toLowerCase();
+ let friendlyKey = key.replaceAll("_", " ");
+ return (
+ friendlyKey.charAt(0).toUpperCase() + friendlyKey.slice(1).toLowerCase()
+ );
}
- return (
- props.resource && props.resource.id ?
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- {
- Object.keys(props.resource.properties).map((key) => {
- let val = (props.resource.properties as any)[key];
- return (
-
- )
- })
- }
-
+ return props.resource && props.resource.id ? (
+ <>
+
+
+
+
+
+
+
+
+
+
- > : <>>
+
+ {Object.keys(props.resource.properties).map((key) => {
+ let val = (props.resource.properties as any)[key];
+ return (
+
+ );
+ })}
+
+
+ >
+ ) : (
+ <>>
);
};
diff --git a/ui/app/src/components/shared/SecuredByRole.tsx b/ui/app/src/components/shared/SecuredByRole.tsx
index 2c1e8cb343..67ce82e953 100644
--- a/ui/app/src/components/shared/SecuredByRole.tsx
+++ b/ui/app/src/components/shared/SecuredByRole.tsx
@@ -1,20 +1,26 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { AppRolesContext } from '../../contexts/AppRolesContext';
-import { MessageBar, MessageBarType } from '@fluentui/react';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
+import React, { useContext, useEffect, useState } from "react";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { AppRolesContext } from "../../contexts/AppRolesContext";
+import { MessageBar, MessageBarType } from "@fluentui/react";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import {
+ HttpMethod,
+ ResultType,
+ useAuthApiCall,
+} from "../../hooks/useAuthApiCall";
interface SecuredByRoleProps {
- element: JSX.Element,
- allowedAppRoles?: Array,
- allowedWorkspaceRoles?: Array,
- workspaceId?: string,
+ element: JSX.Element;
+ allowedAppRoles?: Array;
+ allowedWorkspaceRoles?: Array;
+ workspaceId?: string;
errorString?: String;
}
// Check if the user roles match any of the roles we are given - if they do, show the element, if not, don't
-export const SecuredByRole: React.FunctionComponent = (props: SecuredByRoleProps) => {
+export const SecuredByRole: React.FunctionComponent = (
+ props: SecuredByRoleProps,
+) => {
const apiCall = useAuthApiCall();
const appRoles = useContext(AppRolesContext);
@@ -26,12 +32,24 @@ export const SecuredByRole: React.FunctionComponent = (props
if (!workspaceCtx.workspace.id && props.workspaceId !== "") {
let r = [] as Array;
- let workspaceAuth = (await apiCall(`${ApiEndpoint.Workspaces}/${props.workspaceId}/scopeid`, HttpMethod.Get)).workspaceAuth;
+ let workspaceAuth = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${props.workspaceId}/scopeid`,
+ HttpMethod.Get,
+ )
+ ).workspaceAuth;
if (workspaceAuth) {
- await apiCall(`${ApiEndpoint.Workspaces}/${props.workspaceId}`, HttpMethod.Get, workspaceAuth.scopeId,
- undefined, ResultType.JSON, (roles: Array) => {
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${props.workspaceId}`,
+ HttpMethod.Get,
+ workspaceAuth.scopeId,
+ undefined,
+ ResultType.JSON,
+ (roles: Array) => {
r = roles;
- }, true);
+ },
+ true,
+ );
}
setRoles(r);
}
@@ -39,21 +57,25 @@ export const SecuredByRole: React.FunctionComponent = (props
if (workspaceCtx.roles.length === 0 && props.workspaceId !== undefined) {
getWorkspaceRoles();
- }
- else {
+ } else {
setRoles(workspaceCtx.roles);
}
+ }, [
+ apiCall,
+ workspaceCtx.workspace.id,
+ props.workspaceId,
+ workspaceCtx.roles,
+ ]);
- }, [apiCall, workspaceCtx.workspace.id, props.workspaceId, workspaceCtx.roles]);
-
- return (
- (workspaceRoles?.some(x => props.allowedWorkspaceRoles?.includes(x)) || appRoles?.roles?.some(x => props.allowedAppRoles?.includes(x)))
- ? props.element
- : (props.errorString && (workspaceRoles.length > 0 || appRoles.roles.length > 0)
- ?
- Access Denied
- {props.errorString}
-
- : null)
- );
+ return workspaceRoles?.some((x) =>
+ props.allowedWorkspaceRoles?.includes(x),
+ ) || appRoles?.roles?.some((x) => props.allowedAppRoles?.includes(x)) ? (
+ props.element
+ ) : props.errorString &&
+ (workspaceRoles.length > 0 || appRoles.roles.length > 0) ? (
+
+ Access Denied
+ {props.errorString}
+
+ ) : null;
};
diff --git a/ui/app/src/components/shared/SharedServiceItem.tsx b/ui/app/src/components/shared/SharedServiceItem.tsx
index 7e2a0bdd2e..018b7cdf3c 100644
--- a/ui/app/src/components/shared/SharedServiceItem.tsx
+++ b/ui/app/src/components/shared/SharedServiceItem.tsx
@@ -1,22 +1,24 @@
-import React, { useEffect, useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { useAuthApiCall, HttpMethod } from '../../hooks/useAuthApiCall';
-import { Spinner, SpinnerSize } from '@fluentui/react';
-import { LoadingState } from '../../models/loadingState';
-import { SharedService } from '../../models/sharedService';
-import { ResourceHeader } from './ResourceHeader';
-import { useComponentManager } from '../../hooks/useComponentManager';
-import { Resource } from '../../models/resource';
-import { ResourceBody } from './ResourceBody';
-import { APIError } from '../../models/exceptions';
-import { ExceptionLayout } from './ExceptionLayout';
+import React, { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { useAuthApiCall, HttpMethod } from "../../hooks/useAuthApiCall";
+import { Spinner, SpinnerSize } from "@fluentui/react";
+import { LoadingState } from "../../models/loadingState";
+import { SharedService } from "../../models/sharedService";
+import { ResourceHeader } from "./ResourceHeader";
+import { useComponentManager } from "../../hooks/useComponentManager";
+import { Resource } from "../../models/resource";
+import { ResourceBody } from "./ResourceBody";
+import { APIError } from "../../models/exceptions";
+import { ExceptionLayout } from "./ExceptionLayout";
interface SharedServiceItemProps {
- readonly?: boolean
+ readonly?: boolean;
}
-export const SharedServiceItem: React.FunctionComponent = (props: SharedServiceItemProps) => {
+export const SharedServiceItem: React.FunctionComponent<
+ SharedServiceItemProps
+> = (props: SharedServiceItemProps) => {
const { sharedServiceId } = useParams();
const [sharedService, setSharedService] = useState({} as SharedService);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
@@ -27,19 +29,22 @@ export const SharedServiceItem: React.FunctionComponent
const latestUpdate = useComponentManager(
sharedService,
(r: Resource) => setSharedService(r as SharedService),
- (r: Resource) => navigate(`/${ApiEndpoint.SharedServices}`)
+ (r: Resource) => navigate(`/${ApiEndpoint.SharedServices}`),
);
useEffect(() => {
const getData = async () => {
try {
- let ss = await apiCall(`${ApiEndpoint.SharedServices}/${sharedServiceId}`, HttpMethod.Get);
+ let ss = await apiCall(
+ `${ApiEndpoint.SharedServices}/${sharedServiceId}`,
+ HttpMethod.Get,
+ );
setSharedService(ss.sharedService);
setLoadingState(LoadingState.Ok);
- } catch (err:any) {
+ } catch (err: any) {
err.userMessage = "Error retrieving shared service";
setApiError(err);
- setLoadingState(LoadingState.Error)
+ setLoadingState(LoadingState.Error);
}
};
getData();
@@ -49,19 +54,26 @@ export const SharedServiceItem: React.FunctionComponent
case LoadingState.Ok:
return (
<>
-
+
>
);
case LoadingState.Error:
- return (
-
- );
+ return ;
default:
return (
-
-
+
+
- )
+ );
}
};
diff --git a/ui/app/src/components/shared/SharedServices.tsx b/ui/app/src/components/shared/SharedServices.tsx
index 74a516cf77..a642237897 100644
--- a/ui/app/src/components/shared/SharedServices.tsx
+++ b/ui/app/src/components/shared/SharedServices.tsx
@@ -1,29 +1,34 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { Resource } from '../../models/resource';
-import { ResourceCardList } from '../shared/ResourceCardList';
-import { PrimaryButton, Stack } from '@fluentui/react';
-import { ResourceType } from '../../models/resourceType';
-import { SharedService } from '../../models/sharedService';
-import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
-import { RoleName } from '../../models/roleNames';
-import { SecuredByRole } from './SecuredByRole';
+import React, { useContext, useEffect, useState } from "react";
+import { Resource } from "../../models/resource";
+import { ResourceCardList } from "../shared/ResourceCardList";
+import { PrimaryButton, Stack } from "@fluentui/react";
+import { ResourceType } from "../../models/resourceType";
+import { SharedService } from "../../models/sharedService";
+import { HttpMethod, useAuthApiCall } from "../../hooks/useAuthApiCall";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { CreateUpdateResourceContext } from "../../contexts/CreateUpdateResourceContext";
+import { RoleName } from "../../models/roleNames";
+import { SecuredByRole } from "./SecuredByRole";
-interface SharedServiceProps{
- readonly?: boolean
+interface SharedServiceProps {
+ readonly?: boolean;
}
-export const SharedServices: React.FunctionComponent
= (props: SharedServiceProps) => {
+export const SharedServices: React.FunctionComponent = (
+ props: SharedServiceProps,
+) => {
const createFormCtx = useContext(CreateUpdateResourceContext);
- const [sharedServices, setSharedServices] = useState([] as Array);
+ const [sharedServices, setSharedServices] = useState(
+ [] as Array,
+ );
const apiCall = useAuthApiCall();
useEffect(() => {
const getSharedServices = async () => {
- const ss = (await apiCall(ApiEndpoint.SharedServices, HttpMethod.Get)).sharedServices;
+ const ss = (await apiCall(ApiEndpoint.SharedServices, HttpMethod.Get))
+ .sharedServices;
setSharedServices(ss);
- }
+ };
getSharedServices();
}, [apiCall]);
@@ -45,7 +50,7 @@ export const SharedServices: React.FunctionComponent = (prop
let ssList = [...sharedServices];
ssList.push(ss);
setSharedServices(ssList);
- }
+ };
return (
<>
@@ -53,26 +58,38 @@ export const SharedServices: React.FunctionComponent = (prop
Shared Services
- {
- !props.readonly &&
- {
- createFormCtx.openCreateForm({
- resourceType: ResourceType.SharedService,
- onAdd: (r: Resource) => addSharedService(r as SharedService)
- })
- }} />
- } />
- }
+ {!props.readonly && (
+ {
+ createFormCtx.openCreateForm({
+ resourceType: ResourceType.SharedService,
+ onAdd: (r: Resource) =>
+ addSharedService(r as SharedService),
+ });
+ }}
+ />
+ }
+ />
+ )}
updateSharedService(r as SharedService)}
- removeResource={(r: Resource) => removeSharedService(r as SharedService)}
+ updateResource={(r: Resource) =>
+ updateSharedService(r as SharedService)
+ }
+ removeResource={(r: Resource) =>
+ removeSharedService(r as SharedService)
+ }
emptyText="This TRE has no shared services."
- readonly={props.readonly} />
+ readonly={props.readonly}
+ />
>
diff --git a/ui/app/src/components/shared/StatusBadge.tsx b/ui/app/src/components/shared/StatusBadge.tsx
index 624869a72d..b1f64655e0 100644
--- a/ui/app/src/components/shared/StatusBadge.tsx
+++ b/ui/app/src/components/shared/StatusBadge.tsx
@@ -1,14 +1,31 @@
-import { Stack, FontWeights, Text, Spinner, FontIcon, mergeStyles, getTheme, SpinnerSize, TooltipHost, ITooltipProps } from '@fluentui/react';
-import React from 'react';
-import { awaitingStates, failedStates, inProgressStates } from '../../models/operation';
-import { Resource } from '../../models/resource';
+import {
+ Stack,
+ FontWeights,
+ Text,
+ Spinner,
+ FontIcon,
+ mergeStyles,
+ getTheme,
+ SpinnerSize,
+ TooltipHost,
+ ITooltipProps,
+} from "@fluentui/react";
+import React from "react";
+import {
+ awaitingStates,
+ failedStates,
+ inProgressStates,
+} from "../../models/operation";
+import { Resource } from "../../models/resource";
interface StatusBadgeProps {
- status: string
- resource?: Resource
+ status: string;
+ resource?: Resource;
}
-export const StatusBadge: React.FunctionComponent = (props: StatusBadgeProps) => {
+export const StatusBadge: React.FunctionComponent = (
+ props: StatusBadgeProps,
+) => {
let badgeType;
if (props.status && inProgressStates.indexOf(props.status) !== -1) {
badgeType = "inProgress";
@@ -20,17 +37,22 @@ export const StatusBadge: React.FunctionComponent = (props: St
const failedTooltipProps: ITooltipProps = {
onRenderContent: () => (
-
-
+
+
{props.status.replace("_", " ")}
-
+
- There was an issue with the latest deployment or update for this resource.
- Please see the Operations panel within the resource for details.
+ There was an issue with the latest deployment or update for
+ this resource. Please see the Operations panel within the
+ resource for details.
@@ -42,11 +64,24 @@ export const StatusBadge: React.FunctionComponent = (props: St
switch (badgeType) {
case "inProgress":
- let label = awaitingStates.includes(props.status) ? 'pending' : props.status.replace("_", " ");
- return
+ let label = awaitingStates.includes(props.status)
+ ? "pending"
+ : props.status.replace("_", " ");
+ return (
+
+ );
case "failed":
return (
-
+
= (props: St
/>
>
- )
+ );
default:
- return <>>
+ return <>>;
}
};
@@ -80,10 +115,10 @@ const { palette } = getTheme();
const errorIcon = mergeStyles({
color: palette.red,
fontSize: 18,
- margin: 8
+ margin: 8,
});
const disabledIcon = mergeStyles({
color: palette.blackTranslucent40,
fontSize: 18,
- margin: 8
+ margin: 8,
});
diff --git a/ui/app/src/components/shared/TopNav.tsx b/ui/app/src/components/shared/TopNav.tsx
index d3b93d2ac1..d3dd54ca8b 100644
--- a/ui/app/src/components/shared/TopNav.tsx
+++ b/ui/app/src/components/shared/TopNav.tsx
@@ -1,8 +1,8 @@
-import React from 'react';
-import { getTheme, Icon, mergeStyles, Stack } from '@fluentui/react';
-import { Link } from 'react-router-dom';
-import { UserMenu } from './UserMenu';
-import { NotificationPanel } from './notifications/NotificationPanel';
+import React from "react";
+import { getTheme, Icon, mergeStyles, Stack } from "@fluentui/react";
+import { Link } from "react-router-dom";
+import { UserMenu } from "./UserMenu";
+import { NotificationPanel } from "./notifications/NotificationPanel";
export const TopNav: React.FunctionComponent = () => {
return (
@@ -10,9 +10,16 @@ export const TopNav: React.FunctionComponent = () => {
-
-
- Azure TRE
+
+
+ Azure TRE
@@ -32,7 +39,7 @@ const contentClass = mergeStyles([
{
backgroundColor: theme.palette.themeDark,
color: theme.palette.white,
- lineHeight: '50px',
- padding: '0 10px 0 10px'
- }
+ lineHeight: "50px",
+ padding: "0 10px 0 10px",
+ },
]);
diff --git a/ui/app/src/components/shared/UserMenu.tsx b/ui/app/src/components/shared/UserMenu.tsx
index 66f79050a2..7cc4c0235b 100644
--- a/ui/app/src/components/shared/UserMenu.tsx
+++ b/ui/app/src/components/shared/UserMenu.tsx
@@ -1,6 +1,11 @@
-import React from 'react';
-import { IContextualMenuProps, Persona, PersonaSize, PrimaryButton } from '@fluentui/react';
-import { useAccount, useMsal } from '@azure/msal-react';
+import React from "react";
+import {
+ IContextualMenuProps,
+ Persona,
+ PersonaSize,
+ PrimaryButton,
+} from "@fluentui/react";
+import { useAccount, useMsal } from "@azure/msal-react";
export const UserMenu: React.FunctionComponent = () => {
const { instance, accounts } = useMsal();
@@ -11,28 +16,28 @@ export const UserMenu: React.FunctionComponent = () => {
directionalHint: 6, // bottom right edge
items: [
{
- key: 'logout',
- text: 'Logout',
- iconProps: { iconName: 'SignOut' },
+ key: "logout",
+ text: "Logout",
+ iconProps: { iconName: "SignOut" },
onClick: () => {
instance.logout(); // will use MSAL to logout and redirect to the /logout page
- }
- }
- ]
+ },
+ },
+ ],
};
return (
-
-
+
);
};
-
-
diff --git a/ui/app/src/components/shared/airlock/Airlock.tsx b/ui/app/src/components/shared/airlock/Airlock.tsx
index 89e3f3709f..e8660beb9d 100644
--- a/ui/app/src/components/shared/airlock/Airlock.tsx
+++ b/ui/app/src/components/shared/airlock/Airlock.tsx
@@ -1,28 +1,53 @@
-import React, { useCallback, useContext, useEffect, useState } from 'react';
-import { ColumnActionsMode, CommandBar, CommandBarButton, ContextualMenu, DirectionalHint, getTheme, IColumn, ICommandBarItemProps, Icon, IContextualMenuItem, IContextualMenuProps, Persona, PersonaSize, SelectionMode, ShimmeredDetailsList, Stack } from '@fluentui/react';
-import { HttpMethod, useAuthApiCall } from '../../../hooks/useAuthApiCall';
-import { ApiEndpoint } from '../../../models/apiEndpoints';
-import { WorkspaceContext } from '../../../contexts/WorkspaceContext';
-import { AirlockRequest, AirlockRequestAction, AirlockRequestStatus, AirlockRequestType } from '../../../models/airlock';
-import moment from 'moment';
-import { Route, Routes, useNavigate } from 'react-router-dom';
-import { AirlockViewRequest } from './AirlockViewRequest';
-import { LoadingState } from '../../../models/loadingState';
-import { APIError } from '../../../models/exceptions';
-import { ExceptionLayout } from '../ExceptionLayout';
-import { AirlockNewRequest } from './AirlockNewRequest';
-import { WorkspaceRoleName } from '../../../models/roleNames';
-import { useAccount, useMsal } from '@azure/msal-react';
-import { getFileTypeIconProps } from '@fluentui/react-file-type-icons';
+import React, { useCallback, useContext, useEffect, useState } from "react";
+import {
+ ColumnActionsMode,
+ CommandBar,
+ CommandBarButton,
+ ContextualMenu,
+ DirectionalHint,
+ getTheme,
+ IColumn,
+ ICommandBarItemProps,
+ Icon,
+ IContextualMenuItem,
+ IContextualMenuProps,
+ Persona,
+ PersonaSize,
+ SelectionMode,
+ ShimmeredDetailsList,
+ Stack,
+} from "@fluentui/react";
+import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
+import { ApiEndpoint } from "../../../models/apiEndpoints";
+import { WorkspaceContext } from "../../../contexts/WorkspaceContext";
+import {
+ AirlockRequest,
+ AirlockRequestAction,
+ AirlockRequestStatus,
+ AirlockRequestType,
+} from "../../../models/airlock";
+import moment from "moment";
+import { Route, Routes, useNavigate } from "react-router-dom";
+import { AirlockViewRequest } from "./AirlockViewRequest";
+import { LoadingState } from "../../../models/loadingState";
+import { APIError } from "../../../models/exceptions";
+import { ExceptionLayout } from "../ExceptionLayout";
+import { AirlockNewRequest } from "./AirlockNewRequest";
+import { WorkspaceRoleName } from "../../../models/roleNames";
+import { useAccount, useMsal } from "@azure/msal-react";
+import { getFileTypeIconProps } from "@fluentui/react-file-type-icons";
export const Airlock: React.FunctionComponent = () => {
- const [airlockRequests, setAirlockRequests] = useState([] as AirlockRequest[]);
+ const [airlockRequests, setAirlockRequests] = useState(
+ [] as AirlockRequest[],
+ );
const [requestColumns, setRequestColumns] = useState([] as IColumn[]);
- const [orderBy, setOrderBy] = useState('updatedWhen');
+ const [orderBy, setOrderBy] = useState("updatedWhen");
const [orderAscending, setOrderAscending] = useState(false);
const [filters, setFilters] = useState(new Map());
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
- const [contextMenuProps, setContextMenuProps] = useState();
+ const [contextMenuProps, setContextMenuProps] =
+ useState();
const [apiError, setApiError] = useState();
const workspaceCtx = useContext(WorkspaceContext);
const apiCall = useAuthApiCall();
@@ -39,9 +64,8 @@ export const Airlock: React.FunctionComponent = () => {
try {
let requests: AirlockRequest[];
if (workspaceCtx.workspace) {
-
// Add any selected filters and orderBy
- let query = '?';
+ let query = "?";
filters.forEach((value, key) => {
query += `${key}=${value}&`;
});
@@ -53,18 +77,20 @@ export const Airlock: React.FunctionComponent = () => {
const result = await apiCall(
`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.AirlockRequests}${query.slice(0, -1)}`,
HttpMethod.Get,
- workspaceCtx.workspaceApplicationIdURI
+ workspaceCtx.workspaceApplicationIdURI,
);
// Map the inner requests and the allowed user actions to state
- requests = result.airlockRequests.map((r: {
- airlockRequest: AirlockRequest,
- allowedUserActions: Array
- }) => {
- const request = r.airlockRequest;
- request.allowedUserActions = r.allowedUserActions;
- return request;
- });
+ requests = result.airlockRequests.map(
+ (r: {
+ airlockRequest: AirlockRequest;
+ allowedUserActions: Array;
+ }) => {
+ const request = r.airlockRequest;
+ request.allowedUserActions = r.allowedUserActions;
+ return request;
+ },
+ );
} else {
// TODO: Get all requests across workspaces
requests = [];
@@ -73,11 +99,18 @@ export const Airlock: React.FunctionComponent = () => {
setAirlockRequests(requests);
setLoadingState(LoadingState.Ok);
} catch (err: any) {
- err.userMessage = 'Error fetching airlock requests';
+ err.userMessage = "Error fetching airlock requests";
setApiError(err);
setLoadingState(LoadingState.Error);
}
- }, [apiCall, workspaceCtx.workspace, workspaceCtx.workspaceApplicationIdURI, filters, orderBy, orderAscending]);
+ }, [
+ apiCall,
+ workspaceCtx.workspace,
+ workspaceCtx.workspaceApplicationIdURI,
+ filters,
+ orderBy,
+ orderAscending,
+ ]);
// Fetch new requests on first load and whenever filters/orderBy selection changes
useEffect(() => {
@@ -96,159 +129,195 @@ export const Airlock: React.FunctionComponent = () => {
};
// Open a context menu in the requests list for filtering and sorting
- const openContextMenu = useCallback((column: IColumn, ev: React.MouseEvent, options: Array) => {
- const filterOptions = options.map(option => {
- return {
- key: option,
- name: option,
- canCheck: true,
- checked: filters?.has(column.key) && filters.get(column.key) === option,
- onClick: () => {
- // Set filter or unset if already selected
- setFilters((f) => {
- if (f.get(column.key) === option) {
- f.delete(column.key);
- } else {
- f.set(column.key, option);
- }
- // Return as a new map to trigger re-rendering
- return new Map(f);
- });
- }
- }
- });
+ const openContextMenu = useCallback(
+ (
+ column: IColumn,
+ ev: React.MouseEvent,
+ options: Array,
+ ) => {
+ const filterOptions = options.map((option) => {
+ return {
+ key: option,
+ name: option,
+ canCheck: true,
+ checked:
+ filters?.has(column.key) && filters.get(column.key) === option,
+ onClick: () => {
+ // Set filter or unset if already selected
+ setFilters((f) => {
+ if (f.get(column.key) === option) {
+ f.delete(column.key);
+ } else {
+ f.set(column.key, option);
+ }
+ // Return as a new map to trigger re-rendering
+ return new Map(f);
+ });
+ },
+ };
+ });
- const items: IContextualMenuItem[] = [
- {
- key: 'sort',
- name: 'Sort',
- iconProps: { iconName: 'Sort' },
- onClick: () => orderRequests(column)
- },
- {
- key: 'filter',
- name: 'Filter',
- iconProps: { iconName: 'Filter' },
- subMenuProps: {
- items: filterOptions,
- }
- }
- ];
+ const items: IContextualMenuItem[] = [
+ {
+ key: "sort",
+ name: "Sort",
+ iconProps: { iconName: "Sort" },
+ onClick: () => orderRequests(column),
+ },
+ {
+ key: "filter",
+ name: "Filter",
+ iconProps: { iconName: "Filter" },
+ subMenuProps: {
+ items: filterOptions,
+ },
+ },
+ ];
- setContextMenuProps({
+ setContextMenuProps({
items: items,
target: ev.currentTarget as HTMLElement,
directionalHint: DirectionalHint.bottomCenter,
gapSpace: 0,
onDismiss: () => setContextMenuProps(undefined),
- });
- }, [filters]);
+ });
+ },
+ [filters],
+ );
// Set the columns on initial render
useEffect(() => {
- const orderByColumn = (ev: React.MouseEvent, column: IColumn) => {
+ const orderByColumn = (
+ ev: React.MouseEvent,
+ column: IColumn,
+ ) => {
orderRequests(column);
};
const columns: IColumn[] = [
{
- key: 'fileIcon',
- name: 'fileIcon',
+ key: "fileIcon",
+ name: "fileIcon",
minWidth: 16,
maxWidth: 16,
isIconOnly: true,
onRender: (request: AirlockRequest) => {
if (request.status === AirlockRequestStatus.Draft) {
- return
+ return (
+
+ );
} else if (request.files?.length > 0 && request.files[0].name) {
- const fileType = request.files[0].name.split('.').pop();
- return
+ const fileType = request.files[0].name.split(".").pop();
+ return (
+
+ );
} else {
- return
+ return (
+
+ );
}
- }
+ },
},
{
- key: 'title',
- name: 'Title',
- ariaLabel: 'Title of the airlock request',
+ key: "title",
+ name: "Title",
+ ariaLabel: "Title of the airlock request",
minWidth: 150,
maxWidth: 300,
isResizable: true,
- fieldName: 'title'
+ fieldName: "title",
},
{
- key: 'createdBy',
- name: 'Creator',
- ariaLabel: 'Creator of the airlock request',
+ key: "createdBy",
+ name: "Creator",
+ ariaLabel: "Creator of the airlock request",
minWidth: 150,
maxWidth: 200,
isResizable: true,
- onRender: (request: AirlockRequest) => ,
- isFiltered: filters.has('creator_user_id')
+ onRender: (request: AirlockRequest) => (
+
+ ),
+ isFiltered: filters.has("creator_user_id"),
},
{
- key: 'type',
- name: 'Type',
- ariaLabel: 'Whether the request is import or export',
+ key: "type",
+ name: "Type",
+ ariaLabel: "Whether the request is import or export",
minWidth: 70,
maxWidth: 100,
isResizable: true,
- fieldName: 'type',
+ fieldName: "type",
columnActionsMode: ColumnActionsMode.hasDropdown,
- isSorted: orderBy === 'type',
+ isSorted: orderBy === "type",
isSortedDescending: !orderAscending,
- onColumnClick: (ev, column) => openContextMenu(column, ev, Object.values(AirlockRequestType)),
+ onColumnClick: (ev, column) =>
+ openContextMenu(column, ev, Object.values(AirlockRequestType)),
onColumnContextMenu: (column, ev) =>
- (column && ev) && openContextMenu(column, ev, Object.values(AirlockRequestType)),
- isFiltered: filters.has('type')
+ column &&
+ ev &&
+ openContextMenu(column, ev, Object.values(AirlockRequestType)),
+ isFiltered: filters.has("type"),
},
{
- key: 'status',
- name: 'Status',
- ariaLabel: 'Status of the request',
+ key: "status",
+ name: "Status",
+ ariaLabel: "Status of the request",
minWidth: 70,
isResizable: true,
- fieldName: 'status',
+ fieldName: "status",
columnActionsMode: ColumnActionsMode.hasDropdown,
- isSorted: orderBy === 'status',
+ isSorted: orderBy === "status",
isSortedDescending: !orderAscending,
- onColumnClick: (ev, column) => openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
+ onColumnClick: (ev, column) =>
+ openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
onColumnContextMenu: (column, ev) =>
- (column && ev) && openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
- isFiltered: filters.has('status'),
- onRender: (request: AirlockRequest) => request.status.replace("_", " ")
+ column &&
+ ev &&
+ openContextMenu(column, ev, Object.values(AirlockRequestStatus)),
+ isFiltered: filters.has("status"),
+ onRender: (request: AirlockRequest) => request.status.replace("_", " "),
},
{
- key: 'createdTime',
- name: 'Created',
- ariaLabel: 'When the request was created',
+ key: "createdTime",
+ name: "Created",
+ ariaLabel: "When the request was created",
minWidth: 120,
- data: 'number',
+ data: "number",
isResizable: true,
- fieldName: 'createdTime',
- isSorted: orderBy === 'createdTime',
+ fieldName: "createdTime",
+ isSorted: orderBy === "createdTime",
isSortedDescending: !orderAscending,
onRender: (request: AirlockRequest) => {
- return { moment.unix(request.createdWhen).format('DD/MM/YYYY') } ;
+ return (
+ {moment.unix(request.createdWhen).format("DD/MM/YYYY")}
+ );
},
- onColumnClick: orderByColumn
+ onColumnClick: orderByColumn,
},
{
- key: 'updatedWhen',
- name: 'Updated',
- ariaLabel: 'When the request was last updated',
+ key: "updatedWhen",
+ name: "Updated",
+ ariaLabel: "When the request was last updated",
minWidth: 120,
- data: 'number',
+ data: "number",
isResizable: true,
- fieldName: 'updatedWhen',
- isSorted: orderBy === 'updatedWhen',
+ fieldName: "updatedWhen",
+ isSorted: orderBy === "updatedWhen",
isSortedDescending: !orderAscending,
onRender: (request: AirlockRequest) => {
- return { moment.unix(request.updatedWhen).fromNow() } ;
+ return {moment.unix(request.updatedWhen).fromNow()} ;
},
- onColumnClick: orderByColumn
- }
+ onColumnClick: orderByColumn,
+ },
];
setRequestColumns(columns);
}, [openContextMenu, filters, orderAscending, orderBy]);
@@ -260,34 +329,34 @@ export const Airlock: React.FunctionComponent = () => {
const quickFilters: ICommandBarItemProps[] = [
{
- key: 'reset',
- text: 'Clear filters',
- iconProps: { iconName: 'ClearFilter' },
- onClick: () => setFilters(new Map())
- }
+ key: "reset",
+ text: "Clear filters",
+ iconProps: { iconName: "ClearFilter" },
+ onClick: () => setFilters(new Map()),
+ },
];
// If we can access the user's msal account, give option to filter by their user id
if (account) {
quickFilters.unshift({
- key: 'myRequests',
- text: 'My requests',
- iconProps: { iconName: 'EditContact' },
+ key: "myRequests",
+ text: "My requests",
+ iconProps: { iconName: "EditContact" },
onClick: () => {
- const userId = account.localAccountId.split('.')[0];
- setFilters(new Map([['creator_user_id', userId]]));
- }
+ const userId = account.localAccountId.split(".")[0];
+ setFilters(new Map([["creator_user_id", userId]]));
+ },
});
}
// Only show "Awaiting my review" filter if user in airlock manager role
if (workspaceCtx.roles?.includes(WorkspaceRoleName.AirlockManager)) {
quickFilters.unshift({
- key: 'awaitingMyReview',
- text: 'Awaiting my review',
- iconProps: { iconName: 'TemporaryUser' },
+ key: "awaitingMyReview",
+ text: "Awaiting my review",
+ iconProps: { iconName: "TemporaryUser" },
// Currently we don't have assigned reviewers so this will be all requests in review status
- onClick: () => setFilters(new Map([['status', 'in_review']]))
+ onClick: () => setFilters(new Map([["status", "in_review"]])),
});
}
@@ -296,29 +365,27 @@ export const Airlock: React.FunctionComponent = () => {
- Airlock
+ Airlock
getAirlockRequests()}
/>
navigate('new')}
+ style={{ background: "none", color: theme.palette.themePrimary }}
+ onClick={() => navigate("new")}
/>
- {
- apiError &&
- }
-
+ {apiError &&
}
+
{
className="tre-table"
enableShimmer={loadingState === LoadingState.Loading}
/>
- {
- contextMenuProps &&
- }
- {
- airlockRequests.length === 0 && loadingState !== LoadingState.Loading &&
-
No requests found
- {
- filters.size > 0
- ? There are no requests matching your selected filter(s).
- : Looks like there are no airlock requests yet. Create a new request to get started.
- }
-
- }
+ {contextMenuProps && }
+ {airlockRequests.length === 0 &&
+ loadingState !== LoadingState.Loading && (
+
+
No requests found
+ {filters.size > 0 ? (
+
+ There are no requests matching your selected filter(s).
+
+ ) : (
+
+ Looks like there are no airlock requests yet. Create a new
+ request to get started.
+
+ )}
+
+ )}
-
- } />
-
- } />
+ }
+ />
+
+ }
+ />
>
);
};
-
diff --git a/ui/app/src/components/shared/airlock/AirlockNewRequest.tsx b/ui/app/src/components/shared/airlock/AirlockNewRequest.tsx
index 4b774e73d8..bd1b2ba644 100644
--- a/ui/app/src/components/shared/airlock/AirlockNewRequest.tsx
+++ b/ui/app/src/components/shared/airlock/AirlockNewRequest.tsx
@@ -1,9 +1,33 @@
-import { DefaultButton, Dialog, DialogFooter, DocumentCard, DocumentCardDetails, DocumentCardPreview, DocumentCardTitle, DocumentCardType, getTheme, Icon, IDocumentCardPreviewProps, IStackTokens, Panel, PanelType, PrimaryButton, Spinner, SpinnerSize, Stack, TextField } from "@fluentui/react";
+import {
+ DefaultButton,
+ Dialog,
+ DialogFooter,
+ DocumentCard,
+ DocumentCardDetails,
+ DocumentCardPreview,
+ DocumentCardTitle,
+ DocumentCardType,
+ getTheme,
+ Icon,
+ IDocumentCardPreviewProps,
+ IStackTokens,
+ Panel,
+ PanelType,
+ PrimaryButton,
+ Spinner,
+ SpinnerSize,
+ Stack,
+ TextField,
+} from "@fluentui/react";
import { useCallback, useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { WorkspaceContext } from "../../../contexts/WorkspaceContext";
import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
-import { AirlockRequest, AirlockRequestType, NewAirlockRequest } from "../../../models/airlock";
+import {
+ AirlockRequest,
+ AirlockRequestType,
+ NewAirlockRequest,
+} from "../../../models/airlock";
import { ApiEndpoint } from "../../../models/apiEndpoints";
import { APIError } from "../../../models/exceptions";
import { ExceptionLayout } from "../ExceptionLayout";
@@ -12,8 +36,12 @@ interface AirlockNewRequestProps {
onCreateRequest: (request: AirlockRequest) => void;
}
-export const AirlockNewRequest: React.FunctionComponent
= (props: AirlockNewRequestProps) => {
- const [newRequest, setNewRequest] = useState({} as NewAirlockRequest);
+export const AirlockNewRequest: React.FunctionComponent<
+ AirlockNewRequestProps
+> = (props: AirlockNewRequestProps) => {
+ const [newRequest, setNewRequest] = useState(
+ {} as NewAirlockRequest,
+ );
const [requestValid, setRequestValid] = useState(false);
const [hideCreateDialog, setHideCreateDialog] = useState(true);
const [creating, setCreating] = useState(false);
@@ -24,35 +52,42 @@ export const AirlockNewRequest: React.FunctionComponent
const apiCall = useAuthApiCall();
const onChangetitle = useCallback(
- (event: React.FormEvent, newValue?: string) => {
- setNewRequest(request => {
+ (
+ event: React.FormEvent,
+ newValue?: string,
+ ) => {
+ setNewRequest((request) => {
return {
...request,
- title: newValue || ''
- }
+ title: newValue || "",
+ };
});
},
- [setNewRequest]
+ [setNewRequest],
);
const onChangeBusinessJustification = useCallback(
- (event: React.FormEvent, newValue?: string) => {
- setNewRequest(request => {
+ (
+ event: React.FormEvent,
+ newValue?: string,
+ ) => {
+ setNewRequest((request) => {
return {
...request,
- businessJustification: newValue || ''
- }
+ businessJustification: newValue || "",
+ };
});
},
- [setNewRequest]
+ [setNewRequest],
);
useEffect(
- () => setRequestValid(
- newRequest.title?.length > 0 &&
- newRequest.businessJustification?.length > 0
- ),
- [newRequest, setRequestValid]
+ () =>
+ setRequestValid(
+ newRequest.title?.length > 0 &&
+ newRequest.businessJustification?.length > 0,
+ ),
+ [newRequest, setRequestValid],
);
// Submit Airlock request to API
@@ -65,12 +100,12 @@ export const AirlockNewRequest: React.FunctionComponent
`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.AirlockRequests}`,
HttpMethod.Post,
workspaceCtx.workspaceApplicationIdURI,
- newRequest
+ newRequest,
);
props.onCreateRequest(response.airlockRequest);
setHideCreateDialog(true);
} catch (err: any) {
- err.userMessage = 'Error submitting airlock request';
+ err.userMessage = "Error submitting airlock request";
setApiSubmitError(err);
setCreateError(true);
}
@@ -78,17 +113,29 @@ export const AirlockNewRequest: React.FunctionComponent
}
}, [apiCall, newRequest, props, workspaceCtx, requestValid]);
- const dismissPanel = useCallback(() => navigate('../'), [navigate]);
+ const dismissPanel = useCallback(() => navigate("../"), [navigate]);
const renderFooter = useCallback(() => {
- let footer = <>>
+ let footer = <>>;
if (newRequest.type) {
- footer = <>
-
-
setNewRequest({} as NewAirlockRequest)} styles={{root:{marginRight: 8}}}>Back
-
setHideCreateDialog(false)} disabled={!requestValid}>Create
-
- >
+ footer = (
+ <>
+
+
setNewRequest({} as NewAirlockRequest)}
+ styles={{ root: { marginRight: 8 } }}
+ >
+ Back
+
+
setHideCreateDialog(false)}
+ disabled={!requestValid}
+ >
+ Create
+
+
+ >
+ );
}
return footer;
}, [newRequest, setNewRequest, setHideCreateDialog, requestValid]);
@@ -99,58 +146,72 @@ export const AirlockNewRequest: React.FunctionComponent
// Render current step depending on whether type has been selected
if (!newRequest.type) {
title = "New airlock request";
- currentStep =
- setNewRequest({ type: AirlockRequestType.Import } as NewAirlockRequest)}>
-
-
-
-
-
-
-
- setNewRequest({ type: AirlockRequestType.Export } as NewAirlockRequest)}>
-
-
-
-
-
-
- ;
+ currentStep = (
+
+
+ setNewRequest({
+ type: AirlockRequestType.Import,
+ } as NewAirlockRequest)
+ }
+ >
+
+
+
+
+
+
+
+
+ setNewRequest({
+ type: AirlockRequestType.Export,
+ } as NewAirlockRequest)
+ }
+ >
+
+
+
+
+
+
+
+ );
} else {
title = `New airlock ${newRequest.type} request`;
- currentStep =
-
-
- ;
+ currentStep = (
+
+
+
+
+ );
}
return (
@@ -165,34 +226,47 @@ export const AirlockNewRequest: React.FunctionComponent
type={PanelType.custom}
customWidth="450px"
>
-
-
+
+
{workspaceCtx.workspace?.properties?.display_name}
- { currentStep }
- setHideCreateDialog(true)}
- dialogContentProps={{
- title: 'Create request?',
- subText: 'Are you sure you want to create this request?',
- }}
- >
- {
- createError &&
- }
- {
- creating
- ?
- :
-
- setHideCreateDialog(true)} text="Cancel" />
-
- }
-
+ {currentStep}
+ setHideCreateDialog(true)}
+ dialogContentProps={{
+ title: "Create request?",
+ subText: "Are you sure you want to create this request?",
+ }}
+ >
+ {createError && }
+ {creating ? (
+
+ ) : (
+
+
+ setHideCreateDialog(true)}
+ text="Cancel"
+ />
+
+ )}
+
- )
-}
+ );
+};
const stackTokens: IStackTokens = { childrenGap: 20 };
const { palette, fonts } = getTheme();
@@ -201,11 +275,11 @@ const importPreviewGraphic: IDocumentCardPreviewProps = {
previewImages: [
{
previewIconProps: {
- iconName: 'ReleaseGate',
+ iconName: "ReleaseGate",
styles: {
root: {
fontSize: fonts.superLarge.fontSize,
- color: '#0078d7',
+ color: "#0078d7",
backgroundColor: palette.neutralLighterAlt,
},
},
@@ -222,11 +296,11 @@ const exportPreviewGraphic: IDocumentCardPreviewProps = {
previewImages: [
{
previewIconProps: {
- iconName: 'Leave',
+ iconName: "Leave",
styles: {
root: {
fontSize: fonts.superLarge.fontSize,
- color: '#0078d7',
+ color: "#0078d7",
backgroundColor: palette.neutralLighterAlt,
},
},
@@ -239,4 +313,4 @@ const exportPreviewGraphic: IDocumentCardPreviewProps = {
},
};
-const cardTitleStyles = { root: { fontWeight: '600', paddingTop: 15 } };
+const cardTitleStyles = { root: { fontWeight: "600", paddingTop: 15 } };
diff --git a/ui/app/src/components/shared/airlock/AirlockRequestFilesSection.tsx b/ui/app/src/components/shared/airlock/AirlockRequestFilesSection.tsx
index 8f19cf4520..cb7c2ff680 100644
--- a/ui/app/src/components/shared/airlock/AirlockRequestFilesSection.tsx
+++ b/ui/app/src/components/shared/airlock/AirlockRequestFilesSection.tsx
@@ -1,4 +1,13 @@
-import { MessageBar, MessageBarType, Pivot, PivotItem, PrimaryButton, Stack, TextField, TooltipHost } from "@fluentui/react";
+import {
+ MessageBar,
+ MessageBarType,
+ Pivot,
+ PivotItem,
+ PrimaryButton,
+ Stack,
+ TextField,
+ TooltipHost,
+} from "@fluentui/react";
import React, { useCallback, useEffect, useState } from "react";
import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
import { AirlockRequest, AirlockRequestStatus } from "../../../models/airlock";
@@ -12,11 +21,14 @@ interface AirlockRequestFilesSectionProps {
workspaceApplicationIdURI: string;
}
-export const AirlockRequestFilesSection: React.FunctionComponent = (props: AirlockRequestFilesSectionProps) => {
+export const AirlockRequestFilesSection: React.FunctionComponent<
+ AirlockRequestFilesSectionProps
+> = (props: AirlockRequestFilesSectionProps) => {
+ const COPY_TOOL_TIP_DEFAULT_MESSAGE = "Copy to clipboard";
- const COPY_TOOL_TIP_DEFAULT_MESSAGE = "Copy to clipboard"
-
- const [copyToolTipMessage, setCopyToolTipMessage] = useState(COPY_TOOL_TIP_DEFAULT_MESSAGE);
+ const [copyToolTipMessage, setCopyToolTipMessage] = useState(
+ COPY_TOOL_TIP_DEFAULT_MESSAGE,
+ );
const [sasUrl, setSasUrl] = useState();
const [sasUrlError, setSasUrlError] = useState(false);
@@ -30,11 +42,11 @@ export const AirlockRequestFilesSection: React.FunctionComponent {
- const match = sasUrl.match(/https:\/\/(.*?).blob.core.windows.net\/(.*)\?(.*)$/);
+ const match = sasUrl.match(
+ /https:\/\/(.*?).blob.core.windows.net\/(.*)\?(.*)$/,
+ );
if (!match) {
- return
+ return;
}
return {
StorageAccountName: match[1],
containerName: match[2],
- sasToken: match[3]
- }
+ sasToken: match[3],
+ };
};
const handleCopySasUrl = () => {
@@ -59,28 +73,31 @@ export const AirlockRequestFilesSection: React.FunctionComponent setCopyToolTipMessage(COPY_TOOL_TIP_DEFAULT_MESSAGE), 3000);
- }
+ setCopyToolTipMessage("Copied");
+ setTimeout(
+ () => setCopyToolTipMessage(COPY_TOOL_TIP_DEFAULT_MESSAGE),
+ 3000,
+ );
+ };
const getAzureCliCommand = (sasUrl: string) => {
- let containerDetails = parseSasUrl(sasUrl)
+ let containerDetails = parseSasUrl(sasUrl);
if (!containerDetails) {
- return '';
+ return "";
}
let cliCommand = "";
if (props.request.status === AirlockRequestStatus.Draft) {
- cliCommand = `az storage blob upload --file --name --account-name ${containerDetails.StorageAccountName} --type block --container-name ${containerDetails.containerName} --sas-token "${containerDetails.sasToken}"`
+ cliCommand = `az storage blob upload --file --name --account-name ${containerDetails.StorageAccountName} --type block --container-name ${containerDetails.containerName} --sas-token "${containerDetails.sasToken}"`;
} else {
- cliCommand = `az storage blob download-batch --destination --source ${containerDetails.containerName} --account-name ${containerDetails.StorageAccountName} --sas-token "${containerDetails.sasToken}"`
+ cliCommand = `az storage blob download-batch --destination --source ${containerDetails.containerName} --account-name ${containerDetails.StorageAccountName} --sas-token "${containerDetails.sasToken}"`;
}
return cliCommand;
};
useEffect(() => {
- generateSasUrl()
+ generateSasUrl();
}, [generateSasUrl]);
return (
@@ -88,51 +105,68 @@ export const AirlockRequestFilesSection: React.FunctionComponent
-
- {
- props.request.status === AirlockRequestStatus.Draft
- ? Use the storage container SAS URL to upload your request file.
- : Use the storage container SAS URL to view the request file.
- }
-
+
+ {props.request.status === AirlockRequestStatus.Draft ? (
+
+ Use the storage container SAS URL to upload your request file.
+
+ ) : (
+
+ Use the storage container SAS URL to view the request file.
+
+ )}
+
{ handleCopySasUrl() }}
+ iconProps={{ iconName: "copy" }}
+ styles={{ root: { minWidth: "40px" } }}
+ onClick={() => {
+ handleCopySasUrl();
+ }}
/>
- {
- props.request.status === AirlockRequestStatus.Draft &&
- Please upload a single file. Only single-file imports (including zip files) are supported.
+ {props.request.status === AirlockRequestStatus.Draft && (
+
+ Please upload a single file. Only single-file imports (including
+ zip files) are supported.
- }
+ )}
-
- Use Azure command-line interface (Azure CLI) to interact with the storage container.
-
+
+
+ Use Azure command-line interface (Azure CLI) to interact with
+ the storage container.
+
+
-
+
- {
- sasUrlError &&
- }
+ {sasUrlError && }
);
};
diff --git a/ui/app/src/components/shared/airlock/AirlockReviewRequest.tsx b/ui/app/src/components/shared/airlock/AirlockReviewRequest.tsx
index a5e80069b5..4dc62213b1 100644
--- a/ui/app/src/components/shared/airlock/AirlockReviewRequest.tsx
+++ b/ui/app/src/components/shared/airlock/AirlockReviewRequest.tsx
@@ -1,4 +1,23 @@
-import { DefaultButton, DialogFooter, FontWeights, getTheme, IButtonStyles, IconButton, IIconProps, IStackItemStyles, IStackStyles, mergeStyleSets, MessageBar, MessageBarType, PrimaryButton, Shimmer, Spinner, SpinnerSize, Stack, TextField } from "@fluentui/react";
+import {
+ DefaultButton,
+ DialogFooter,
+ FontWeights,
+ getTheme,
+ IButtonStyles,
+ IconButton,
+ IIconProps,
+ IStackItemStyles,
+ IStackStyles,
+ mergeStyleSets,
+ MessageBar,
+ MessageBarType,
+ PrimaryButton,
+ Shimmer,
+ Spinner,
+ SpinnerSize,
+ Stack,
+ TextField,
+} from "@fluentui/react";
import { useCallback, useContext, useEffect, useState } from "react";
import { WorkspaceContext } from "../../../contexts/WorkspaceContext";
import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
@@ -7,11 +26,20 @@ import { ApiEndpoint } from "../../../models/apiEndpoints";
import { APIError } from "../../../models/exceptions";
import { destructiveButtonStyles, successButtonStyles } from "../../../styles";
import { ExceptionLayout } from "../ExceptionLayout";
-import { UserResource } from '../../../models/userResource';
+import { UserResource } from "../../../models/userResource";
import { PowerStateBadge } from "../PowerStateBadge";
import { useComponentManager } from "../../../hooks/useComponentManager";
-import { ComponentAction, Resource, VMPowerStates } from "../../../models/resource";
-import { actionsDisabledStates, failedStates, inProgressStates, successStates } from "../../../models/operation";
+import {
+ ComponentAction,
+ Resource,
+ VMPowerStates,
+} from "../../../models/resource";
+import {
+ actionsDisabledStates,
+ failedStates,
+ inProgressStates,
+ successStates,
+} from "../../../models/operation";
import { useAppDispatch } from "../../../hooks/customReduxHooks";
import { addUpdateOperation } from "../notifications/operationsSlice";
import { StatusBadge } from "../StatusBadge";
@@ -19,19 +47,24 @@ import vmImage from "../../../assets/virtual_machine.svg";
import { useAccount, useMsal } from "@azure/msal-react";
interface AirlockReviewRequestProps {
- request: AirlockRequest | undefined,
- onUpdateRequest: (request: AirlockRequest) => void,
- onReviewRequest: (request: AirlockRequest) => void,
- onClose: () => void
+ request: AirlockRequest | undefined;
+ onUpdateRequest: (request: AirlockRequest) => void;
+ onReviewRequest: (request: AirlockRequest) => void;
+ onClose: () => void;
}
-export const AirlockReviewRequest: React.FunctionComponent = (props: AirlockReviewRequestProps) => {
+export const AirlockReviewRequest: React.FunctionComponent<
+ AirlockReviewRequestProps
+> = (props: AirlockReviewRequestProps) => {
const [request, setRequest] = useState();
- const [reviewExplanation, setReviewExplanation] = useState('');
+ const [reviewExplanation, setReviewExplanation] = useState("");
const [reviewing, setReviewing] = useState(false);
const [reviewError, setReviewError] = useState(false);
- const [reviewResourcesConfigured, setReviewResourcesConfigured] = useState(false);
- const [reviewResourceStatus, setReviewResourceStatus] = useState<'notCreated' | 'creating' | 'created' | 'failed'>();
+ const [reviewResourcesConfigured, setReviewResourcesConfigured] =
+ useState(false);
+ const [reviewResourceStatus, setReviewResourceStatus] = useState<
+ "notCreated" | "creating" | "created" | "failed"
+ >();
const [reviewResourceError, setReviewResourceError] = useState(false);
const [apiError, setApiError] = useState({} as APIError);
const [proceedToReview, setProceedToReview] = useState(false);
@@ -49,9 +82,9 @@ export const AirlockReviewRequest: React.FunctionComponent {
if (
- request
- && workspaceCtx.workspace?.properties.airlock_review_config
- && workspaceCtx.workspace?.properties.airlock_review_config[request.type]
+ request &&
+ workspaceCtx.workspace?.properties.airlock_review_config &&
+ workspaceCtx.workspace?.properties.airlock_review_config[request.type]
) {
setReviewResourcesConfigured(true);
} else {
@@ -65,16 +98,26 @@ export const AirlockReviewRequest: React.FunctionComponent id !== userId);
+ const otherReviewers = Object.keys(request.reviewUserResources).filter(
+ (id) => id !== userId,
+ );
setOtherReviewers(otherReviewers);
}
- }, [apiCall, request, workspaceCtx.workspace.id, workspaceCtx.workspaceApplicationIdURI, reviewResourcesConfigured, account]);
+ }, [
+ apiCall,
+ request,
+ workspaceCtx.workspace.id,
+ workspaceCtx.workspaceApplicationIdURI,
+ reviewResourcesConfigured,
+ account,
+ ]);
// Get the latest updates to the review resource to track deployment
const latestUpdate = useComponentManager(
reviewResource,
- (r: Resource) => { setReviewResource(r as UserResource) },
- () => { setReviewResource({} as UserResource) },
- reviewWorkspaceScope // Pass this so component manager knows it might be different to the workspace context
+ (r: Resource) => {
+ setReviewResource(r as UserResource);
+ },
+ () => {
+ setReviewResource({} as UserResource);
+ },
+ reviewWorkspaceScope, // Pass this so component manager knows it might be different to the workspace context
);
// Set the review resource status
useEffect(() => {
if (reviewResource && latestUpdate) {
- if (inProgressStates.includes(latestUpdate.operation?.status) || inProgressStates.includes(reviewResource.deploymentStatus)) {
- setReviewResourceStatus('creating');
- } else if (failedStates.includes(latestUpdate.operation?.status) || failedStates.includes(reviewResource.deploymentStatus)) {
- setReviewResourceStatus('failed');
+ if (
+ inProgressStates.includes(latestUpdate.operation?.status) ||
+ inProgressStates.includes(reviewResource.deploymentStatus)
+ ) {
+ setReviewResourceStatus("creating");
+ } else if (
+ failedStates.includes(latestUpdate.operation?.status) ||
+ failedStates.includes(reviewResource.deploymentStatus)
+ ) {
+ setReviewResourceStatus("failed");
const err = new Error(latestUpdate.operation?.message) as any;
- err.userMessage = 'An issue occurred while deploying the review resource.'
+ err.userMessage =
+ "An issue occurred while deploying the review resource.";
setApiError(new Error(latestUpdate.operation?.message));
setReviewResourceError(true);
- } else if (successStates.includes(latestUpdate.operation?.status) || successStates.includes(reviewResource.deploymentStatus)) {
- setReviewResourceStatus('created');
+ } else if (
+ successStates.includes(latestUpdate.operation?.status) ||
+ successStates.includes(reviewResource.deploymentStatus)
+ ) {
+ setReviewResourceStatus("created");
}
}
- }, [latestUpdate, reviewResource, request])
+ }, [latestUpdate, reviewResource, request]);
// Create a review resource
const createReviewResource = useCallback(async () => {
setReviewResourceError(false);
- setReviewResourceStatus('creating');
+ setReviewResourceStatus("creating");
try {
const response = await apiCall(
`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.AirlockRequests}/${request?.id}/${ApiEndpoint.AirlockCreateReviewResource}`,
HttpMethod.Post,
- workspaceCtx.workspaceApplicationIdURI
+ workspaceCtx.workspaceApplicationIdURI,
);
dispatch(addUpdateOperation(response.operation));
props.onUpdateRequest(response.airlockRequest);
@@ -147,41 +215,57 @@ export const AirlockReviewRequest: React.FunctionComponent {
- if (request && reviewExplanation) {
- setReviewing(true);
- setReviewError(false);
- try {
- const review = {
- approval: isApproved,
- decisionExplanation: reviewExplanation
- };
- const response = await apiCall(
- `${ApiEndpoint.Workspaces}/${request.workspaceId}/${ApiEndpoint.AirlockRequests}/${request.id}/${ApiEndpoint.AirlockReview}`,
- HttpMethod.Post,
- workspaceCtx.workspaceApplicationIdURI,
- review
- );
- props.onReviewRequest(response.airlockRequest);
- } catch (err: any) {
- err.userMessage = 'Error reviewing airlock request';
- setApiError(err);
- setReviewError(true);
+ const reviewRequest = useCallback(
+ async (isApproved: boolean) => {
+ if (request && reviewExplanation) {
+ setReviewing(true);
+ setReviewError(false);
+ try {
+ const review = {
+ approval: isApproved,
+ decisionExplanation: reviewExplanation,
+ };
+ const response = await apiCall(
+ `${ApiEndpoint.Workspaces}/${request.workspaceId}/${ApiEndpoint.AirlockRequests}/${request.id}/${ApiEndpoint.AirlockReview}`,
+ HttpMethod.Post,
+ workspaceCtx.workspaceApplicationIdURI,
+ review,
+ );
+ props.onReviewRequest(response.airlockRequest);
+ } catch (err: any) {
+ err.userMessage = "Error reviewing airlock request";
+ setApiError(err);
+ setReviewError(true);
+ }
+ setReviewing(false);
}
- setReviewing(false);
- }
- }, [apiCall, request, workspaceCtx.workspaceApplicationIdURI, reviewExplanation, props]);
+ },
+ [
+ apiCall,
+ request,
+ workspaceCtx.workspaceApplicationIdURI,
+ reviewExplanation,
+ props,
+ ],
+ );
let statusBadge = ;
- let action = ;
+ let action = ;
// Get connection property for review userResource
- let connectUri = '';
+ let connectUri = "";
if (reviewResource?.properties && reviewResource.properties.connection_uri) {
connectUri = reviewResource.properties.connection_uri;
}
@@ -189,128 +273,165 @@ export const AirlockReviewRequest: React.FunctionComponent ;
+ case "creating":
+ statusBadge = (
+
+ );
break;
- case 'notCreated':
+ case "notCreated":
statusBadge = Not created ;
action = ;
break;
- case 'failed':
- statusBadge = ;
+ case "failed":
+ statusBadge = (
+
+ );
action = ;
break;
- case 'created':
- statusBadge = ;
+ case "created":
+ statusBadge = (
+
+ );
if (resourceNotConnectable) {
- action = ;
+ action = (
+
+ );
} else {
- action = window.open(connectUri)}
- text="View data"
- title="Connect to resource"
- />;
+ action = (
+ window.open(connectUri)}
+ text="View data"
+ title="Connect to resource"
+ />
+ );
}
break;
}
- const currentStep = !proceedToReview ? <>
-
- To securely review the request's data, you need to create a review VM. Click "Create" and a VM will be created with the data
- automatically downloaded onto it. Once you've viewed the data, click "Proceed to review" to make your decision.
-
- {
- reviewResourcesConfigured ? <>
-
-
-
-
-
Review VM
- { statusBadge }
-
-
-
- { action }
-
-
- {
- otherReviewers && otherReviewers.length > 0 &&
- {
- otherReviewers.length === 1
- ? <>1 other person is reviewing this request.>
- : <>{otherReviewers.length} other people are reviewing this request.>
- }
+ const currentStep = !proceedToReview ? (
+ <>
+
+ To securely review the request's data, you need to create a review VM.
+ Click "Create" and a VM will be created with the data automatically
+ downloaded onto it. Once you've viewed the data, click "Proceed to
+ review" to make your decision.
+
+ {reviewResourcesConfigured ? (
+ <>
+
+
+
+
+
Review VM
+ {statusBadge}
+
+
+ {action}
+
+ {otherReviewers && otherReviewers.length > 0 && (
+
+ {otherReviewers.length === 1 ? (
+ <>
+ 1 other person is reviewing this request.
+ >
+ ) : (
+ <>
+ {otherReviewers.length} other people are reviewing this
+ request.
+ >
+ )}
+
+ )}
+ {reviewResourceError && }
+ >
+ ) : (
+ <>
+
+ It looks like review VMs aren't set up in your workspace. Please
+ contact your Workspace Owner.
- }
- { reviewResourceError && }
- > : <>
-
- It looks like review VMs aren't set up in your workspace. Please contact your Workspace Owner.
-
- >
- }
-
-
- setProceedToReview(true)} text="Proceed to review" />
-
- > : <>
- setReviewExplanation(newValue || '')}
- multiline
- rows={6}
- required
- />
- {
- reviewError &&
- }
- {
- reviewing
- ?
- :
- setProceedToReview(false)}
- text="Back"
- styles={{root:{float:'left'}}}
- />
- reviewRequest(false)}
- text="Reject"
- styles={destructiveButtonStyles}
- disabled={reviewExplanation.length <= 0}
- />
- reviewRequest(true)}
- text="Approve"
- styles={successButtonStyles}
- disabled={reviewExplanation.length <= 0}
+ >
+ )}
+
+
+ setProceedToReview(true)}
+ text="Proceed to review"
/>
- }
- >
+ >
+ ) : (
+ <>
+
+ setReviewExplanation(newValue || "")
+ }
+ multiline
+ rows={6}
+ required
+ />
+ {reviewError && }
+ {reviewing ? (
+
+ ) : (
+
+ setProceedToReview(false)}
+ text="Back"
+ styles={{ root: { float: "left" } }}
+ />
+ reviewRequest(false)}
+ text="Reject"
+ styles={destructiveButtonStyles}
+ disabled={reviewExplanation.length <= 0}
+ />
+ reviewRequest(true)}
+ text="Approve"
+ styles={successButtonStyles}
+ disabled={reviewExplanation.length <= 0}
+ />
+
+ )}
+ >
+ );
return (
<>
@@ -323,66 +444,64 @@ export const AirlockReviewRequest: React.FunctionComponent
-
- { currentStep }
-
+ {currentStep}
>
- )
-}
+ );
+};
const theme = getTheme();
const contentStyles = mergeStyleSets({
header: [
theme.fonts.xLarge,
{
- flex: '1 1 auto',
+ flex: "1 1 auto",
borderTop: `4px solid ${theme.palette.themePrimary}`,
color: theme.palette.neutralPrimary,
- display: 'flex',
- alignItems: 'center',
+ display: "flex",
+ alignItems: "center",
fontWeight: FontWeights.semibold,
- padding: '12px 12px 14px 24px',
+ padding: "12px 12px 14px 24px",
},
],
body: {
- flex: '4 4 auto',
- padding: '0 24px 24px 24px',
- overflowY: 'hidden',
+ flex: "4 4 auto",
+ padding: "0 24px 24px 24px",
+ overflowY: "hidden",
selectors: {
- p: { margin: '14px 0' },
- 'p:first-child': { marginTop: 0 },
- 'p:last-child': { marginBottom: 0 },
+ p: { margin: "14px 0" },
+ "p:first-child": { marginTop: 0 },
+ "p:last-child": { marginBottom: 0 },
},
- width: 600
+ width: 600,
},
});
const iconButtonStyles: Partial = {
root: {
color: theme.palette.neutralPrimary,
- marginLeft: 'auto',
- marginTop: '4px',
- marginRight: '2px',
+ marginLeft: "auto",
+ marginTop: "4px",
+ marginRight: "2px",
},
rootHovered: {
color: theme.palette.neutralDark,
},
};
-const cancelIcon: IIconProps = { iconName: 'Cancel' };
+const cancelIcon: IIconProps = { iconName: "Cancel" };
const reviewVMStyles: IStackStyles = {
- root:{
+ root: {
marginTop: 20,
marginBottom: 20,
padding: 20,
- backgroundColor: theme.palette.neutralLighter
- }
+ backgroundColor: theme.palette.neutralLighter,
+ },
};
const reviewVMItemStyles: IStackItemStyles = {
root: {
- display:'flex',
- alignItems:'center'
- }
-}
+ display: "flex",
+ alignItems: "center",
+ },
+};
diff --git a/ui/app/src/components/shared/airlock/AirlockViewRequest.tsx b/ui/app/src/components/shared/airlock/AirlockViewRequest.tsx
index 6631f2ae08..e31e3ec7b7 100644
--- a/ui/app/src/components/shared/airlock/AirlockViewRequest.tsx
+++ b/ui/app/src/components/shared/airlock/AirlockViewRequest.tsx
@@ -1,10 +1,42 @@
-import { DefaultButton, Dialog, DialogFooter, DocumentCard, DocumentCardActivity, DocumentCardDetails, DocumentCardTitle, DocumentCardType, FontIcon, getTheme, IStackItemStyles, IStackStyles, IStackTokens, mergeStyles, MessageBar, MessageBarType, Modal, Panel, PanelType, Persona, PersonaSize, PrimaryButton, Spinner, SpinnerSize, Stack} from "@fluentui/react";
+import {
+ DefaultButton,
+ Dialog,
+ DialogFooter,
+ DocumentCard,
+ DocumentCardActivity,
+ DocumentCardDetails,
+ DocumentCardTitle,
+ DocumentCardType,
+ FontIcon,
+ getTheme,
+ IStackItemStyles,
+ IStackStyles,
+ IStackTokens,
+ mergeStyles,
+ MessageBar,
+ MessageBarType,
+ Modal,
+ Panel,
+ PanelType,
+ Persona,
+ PersonaSize,
+ PrimaryButton,
+ Spinner,
+ SpinnerSize,
+ Stack,
+} from "@fluentui/react";
import moment from "moment";
import React, { useCallback, useContext, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { WorkspaceContext } from "../../../contexts/WorkspaceContext";
import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
-import { AirlockFilesLinkValidStatus, AirlockRequest, AirlockRequestAction, AirlockRequestStatus, AirlockReviewDecision } from "../../../models/airlock";
+import {
+ AirlockFilesLinkValidStatus,
+ AirlockRequest,
+ AirlockRequestAction,
+ AirlockRequestStatus,
+ AirlockReviewDecision,
+} from "../../../models/airlock";
import { ApiEndpoint } from "../../../models/apiEndpoints";
import { APIError } from "../../../models/exceptions";
import { destructiveButtonStyles } from "../../../styles";
@@ -17,8 +49,10 @@ interface AirlockViewRequestProps {
onUpdateRequest: (requests: AirlockRequest) => void;
}
-export const AirlockViewRequest: React.FunctionComponent = (props: AirlockViewRequestProps) => {
- const {requestId} = useParams();
+export const AirlockViewRequest: React.FunctionComponent<
+ AirlockViewRequestProps
+> = (props: AirlockViewRequestProps) => {
+ const { requestId } = useParams();
const [request, setRequest] = useState();
const [hideSubmitDialog, setHideSubmitDialog] = useState(true);
const [reviewIsOpen, setReviewIsOpen] = useState(false);
@@ -32,14 +66,14 @@ export const AirlockViewRequest: React.FunctionComponent {
// Get the selected request from the router param and find in the requests prop
- let req = props.requests.find(r => r.id === requestId) as AirlockRequest;
+ let req = props.requests.find((r) => r.id === requestId) as AirlockRequest;
// If not found, fetch it from the API
if (!req) {
apiCall(
`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.AirlockRequests}/${requestId}`,
HttpMethod.Get,
- workspaceCtx.workspaceApplicationIdURI
+ workspaceCtx.workspaceApplicationIdURI,
).then((result) => {
const request = result.airlockRequest as AirlockRequest;
request.allowedUserActions = result.allowedUserActions;
@@ -49,9 +83,15 @@ export const AirlockViewRequest: React.FunctionComponent navigate('../'), [navigate]);
+ const dismissPanel = useCallback(() => navigate("../"), [navigate]);
// Submit an airlock request
const submitRequest = useCallback(async () => {
@@ -62,12 +102,12 @@ export const AirlockViewRequest: React.FunctionComponent {
- let footer = <>>
+ let footer = <>>;
if (request) {
- footer = <>
- {
- request.status === AirlockRequestStatus.Draft &&
-
- This request is currently in draft. Add a file to the request's storage container and submit when ready.
-
+ footer = (
+ <>
+ {request.status === AirlockRequestStatus.Draft && (
+
+
+ This request is currently in draft. Add a file to the request's
+ storage container and submit when ready.
+
+
+ )}
+ {request.statusMessage && (
+
+
+ {request.statusMessage}
+
+
+ )}
+
+ {request.allowedUserActions?.includes(
+ AirlockRequestAction.Cancel,
+ ) && (
+
{
+ setSubmitError(false);
+ setHideCancelDialog(false);
+ }}
+ styles={destructiveButtonStyles}
+ >
+ Cancel request
+
+ )}
+ {request.allowedUserActions?.includes(
+ AirlockRequestAction.Submit,
+ ) && (
+
{
+ setSubmitError(false);
+ setHideSubmitDialog(false);
+ }}
+ >
+ Submit
+
+ )}
+ {request.allowedUserActions?.includes(
+ AirlockRequestAction.Review,
+ ) && (
+
setReviewIsOpen(true)}>
+ Review
+
+ )}
- }
- {
- request.statusMessage &&
- {request.statusMessage}
-
- }
-
- {
- request.allowedUserActions?.includes(AirlockRequestAction.Cancel) &&
-
{setSubmitError(false); setHideCancelDialog(false)}} styles={destructiveButtonStyles}>Cancel request
- }
- {
- request.allowedUserActions?.includes(AirlockRequestAction.Submit) &&
-
{setSubmitError(false); setHideSubmitDialog(false)}}>Submit
- }
- {
- request.allowedUserActions?.includes(AirlockRequestAction.Review) &&
-
setReviewIsOpen(true)}>Review
- }
-
- >
+ >
+ );
}
return footer;
}, [request]);
@@ -136,7 +201,9 @@ export const AirlockViewRequest: React.FunctionComponent
{
- request ? <>
-
-
- Id
-
-
- {request.id}
-
-
+ >
+ {" "}
+ {request ? (
+ <>
+
+
+ Id
+
+
+ {request.id}
+
+
-
-
- Creator
-
-
-
-
-
+
+
+ Creator
+
+
+
+
+
-
-
- Type
-
-
- {request.type}
-
-
+
+
+ Type
+
+
+ {request.type}
+
+
-
-
- Status
-
-
- {request.status.replace("_", " ")}
-
-
+
+
+ Status
+
+
+ {request.status.replace("_", " ")}
+
+
-
-
- Workspace
-
-
- {workspaceCtx.workspace?.properties?.display_name}
-
-
+
+
+ Workspace
+
+
+ {workspaceCtx.workspace?.properties?.display_name}
+
+
-
-
- Created
-
-
- {moment.unix(request.createdWhen).format('DD/MM/YYYY')}
-
-
+
+
+ Created
+
+
+ {moment.unix(request.createdWhen).format("DD/MM/YYYY")}
+
+
-
-
- Updated
-
-
- {moment.unix(request.updatedWhen).fromNow()}
-
-
+
+
+ Updated
+
+
+ {moment.unix(request.updatedWhen).fromNow()}
+
+
-
-
- Business Justification
-
-
-
-
- {request.businessJustification}
-
-
- {
- AirlockFilesLinkValidStatus.includes(request.status) && <>
-
-
- Files
-
-
-
- >
- }
- {
- request.reviews && request.reviews.length > 0 && <>
-
-
- Reviews
-
-
-
- {
- request.reviews.map((review, i) => {
- return
-
-
-
-
-
- {
- review.reviewDecision === AirlockReviewDecision.Approved && <>
-
- Approved
- >
- }
- {
- review.reviewDecision === AirlockReviewDecision.Rejected && <>
-
- Rejected
- >
- }
-
-
- })
- }
-
- >
- }
- >
- :
-
-
- }
+
+
+ Business Justification
+
+
+
+
+ {request.businessJustification}
+
+
+ {AirlockFilesLinkValidStatus.includes(request.status) && (
+ <>
+
+
+ Files
+
+
+
+ >
+ )}
+ {request.reviews && request.reviews.length > 0 && (
+ <>
+
+
+ Reviews
+
+
+
+ {request.reviews.map((review, i) => {
+ return (
+
+
+
+
+
+
+ {review.reviewDecision ===
+ AirlockReviewDecision.Approved && (
+ <>
+
+ Approved
+ >
+ )}
+ {review.reviewDecision ===
+ AirlockReviewDecision.Rejected && (
+ <>
+
+ Rejected
+ >
+ )}
+
+
+ );
+ })}
+
+ >
+ )}
+ >
+ ) : (
+
+
+
+ )}
{setHideSubmitDialog(true); setSubmitError(false)}}
+ onDismiss={() => {
+ setHideSubmitDialog(true);
+ setSubmitError(false);
+ }}
dialogContentProps={{
- title: 'Submit request?',
- subText: 'Make sure you have uploaded your file to the request\'s storage account before submitting.',
+ title: "Submit request?",
+ subText:
+ "Make sure you have uploaded your file to the request's storage account before submitting.",
}}
>
- {
- submitError &&
- }
- {
- submitting
- ?
- :
- {setHideSubmitDialog(true); setSubmitError(false)}} text="Cancel" />
+ {submitError && }
+ {submitting ? (
+
+ ) : (
+
+ {
+ setHideSubmitDialog(true);
+ setSubmitError(false);
+ }}
+ text="Cancel"
+ />
- }
+ )}
-
{setHideCancelDialog(true); setSubmitError(false)}}
+ onDismiss={() => {
+ setHideCancelDialog(true);
+ setSubmitError(false);
+ }}
dialogContentProps={{
- title: 'Cancel Airlock Request?',
- subText: 'Are you sure you want to cancel this airlock request?',
+ title: "Cancel Airlock Request?",
+ subText: "Are you sure you want to cancel this airlock request?",
}}
>
- {
- submitError &&
- }
- {
- submitting
- ?
- :
-
- {setHideCancelDialog(true); setSubmitError(false)}} text="Back" />
+ {submitError && }
+ {submitting ? (
+
+ ) : (
+
+
+ {
+ setHideCancelDialog(true);
+ setSubmitError(false);
+ }}
+ text="Back"
+ />
- }
+ )}
-
{props.onUpdateRequest(request); setReviewIsOpen(false)}}
+ onReviewRequest={(request) => {
+ props.onUpdateRequest(request);
+ setReviewIsOpen(false);
+ }}
onClose={() => setReviewIsOpen(false)}
/>
>
- )
-}
+ );
+};
const { palette } = getTheme();
const stackTokens: IStackTokens = { childrenGap: 20 };
const underlineStackStyles: IStackStyles = {
root: {
- borderBottom: '#f2f2f2 solid 1px'
+ borderBottom: "#f2f2f2 solid 1px",
},
};
const stackItemStyles: IStackItemStyles = {
root: {
- alignItems: 'center',
- display: 'flex',
+ alignItems: "center",
+ display: "flex",
height: 50,
- margin: '0px 5px'
+ margin: "0px 5px",
},
};
const approvedIcon = mergeStyles({
color: palette.green,
marginRight: 5,
- fontSize: 12
+ fontSize: 12,
});
const rejectedIcon = mergeStyles({
color: palette.red,
marginRight: 5,
- fontSize: 12
+ fontSize: 12,
});
const modalStyles = mergeStyles({
- display: 'flex',
- flexFlow: 'column nowrap',
- alignItems: 'stretch',
+ display: "flex",
+ flexFlow: "column nowrap",
+ alignItems: "stretch",
});
diff --git a/ui/app/src/components/shared/create-update-resource/CreateUpdateResource.tsx b/ui/app/src/components/shared/create-update-resource/CreateUpdateResource.tsx
index bf6af53639..a67b806053 100644
--- a/ui/app/src/components/shared/create-update-resource/CreateUpdateResource.tsx
+++ b/ui/app/src/components/shared/create-update-resource/CreateUpdateResource.tsx
@@ -1,46 +1,56 @@
-import { Icon, mergeStyles, Panel, PanelType, PrimaryButton } from '@fluentui/react';
-import React, { useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { ApiEndpoint } from '../../../models/apiEndpoints';
-import { Operation } from '../../../models/operation';
-import { ResourceType } from '../../../models/resourceType';
-import { Workspace } from '../../../models/workspace';
-import { WorkspaceService } from '../../../models/workspaceService';
-import { ResourceForm } from './ResourceForm';
-import { SelectTemplate } from './SelectTemplate';
-import { getResourceFromResult, Resource } from '../../../models/resource';
-import { HttpMethod, useAuthApiCall } from '../../../hooks/useAuthApiCall';
-import { useAppDispatch } from '../../../hooks/customReduxHooks';
-import { addUpdateOperation } from '../../shared/notifications/operationsSlice';
+import {
+ Icon,
+ mergeStyles,
+ Panel,
+ PanelType,
+ PrimaryButton,
+} from "@fluentui/react";
+import React, { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { ApiEndpoint } from "../../../models/apiEndpoints";
+import { Operation } from "../../../models/operation";
+import { ResourceType } from "../../../models/resourceType";
+import { Workspace } from "../../../models/workspace";
+import { WorkspaceService } from "../../../models/workspaceService";
+import { ResourceForm } from "./ResourceForm";
+import { SelectTemplate } from "./SelectTemplate";
+import { getResourceFromResult, Resource } from "../../../models/resource";
+import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
+import { useAppDispatch } from "../../../hooks/customReduxHooks";
+import { addUpdateOperation } from "../../shared/notifications/operationsSlice";
interface CreateUpdateResourceProps {
- isOpen: boolean,
- onClose: () => void,
- workspaceApplicationIdURI?: string,
- resourceType: ResourceType,
- parentResource?: Workspace | WorkspaceService,
- onAddResource?: (r: Resource) => void,
- updateResource?: Resource
+ isOpen: boolean;
+ onClose: () => void;
+ workspaceApplicationIdURI?: string;
+ resourceType: ResourceType;
+ parentResource?: Workspace | WorkspaceService;
+ onAddResource?: (r: Resource) => void;
+ updateResource?: Resource;
}
interface PageTitle {
- selectTemplate: string,
- resourceForm: string,
- creating: string
+ selectTemplate: string;
+ resourceForm: string;
+ creating: string;
}
const creatingIconClass = mergeStyles({
fontSize: 100,
height: 100,
width: 100,
- margin: '0 25px',
- color: 'deepskyblue',
- padding: 20
+ margin: "0 25px",
+ color: "deepskyblue",
+ padding: 20,
});
-export const CreateUpdateResource: React.FunctionComponent = (props: CreateUpdateResourceProps) => {
- const [page, setPage] = useState('selectTemplate' as keyof PageTitle);
- const [selectedTemplate, setTemplate] = useState(props.updateResource?.templateName || '');
+export const CreateUpdateResource: React.FunctionComponent<
+ CreateUpdateResourceProps
+> = (props: CreateUpdateResourceProps) => {
+ const [page, setPage] = useState("selectTemplate" as keyof PageTitle);
+ const [selectedTemplate, setTemplate] = useState(
+ props.updateResource?.templateName || "",
+ );
const [deployOperation, setDeployOperation] = useState({} as Operation);
const navigate = useNavigate();
const apiCall = useAuthApiCall();
@@ -48,103 +58,147 @@ export const CreateUpdateResource: React.FunctionComponent {
const clearState = () => {
- setPage('selectTemplate');
+ setPage("selectTemplate");
setDeployOperation({} as Operation);
- setTemplate('');
- }
+ setTemplate("");
+ };
!props.isOpen && clearState();
- props.isOpen && props.updateResource && props.updateResource.templateName && selectTemplate(props.updateResource.templateName);
+ props.isOpen &&
+ props.updateResource &&
+ props.updateResource.templateName &&
+ selectTemplate(props.updateResource.templateName);
}, [props.isOpen, props.updateResource]);
// Render a panel title depending on sub-page
const pageTitles: PageTitle = {
- selectTemplate: 'Choose a template',
- resourceForm: 'Create / Update a ' + props.resourceType,
- creating: ''
- }
+ selectTemplate: "Choose a template",
+ resourceForm: "Create / Update a " + props.resourceType,
+ creating: "",
+ };
// Construct API paths for templates of specified resourceType
let templateListPath;
// Usually, the GET path would be `${templateGetPath}/${selectedTemplate}`, but there's an exception for user resources
let templateGetPath;
- let workspaceApplicationIdURI = undefined
+ let workspaceApplicationIdURI = undefined;
switch (props.resourceType) {
case ResourceType.Workspace:
- templateListPath = ApiEndpoint.WorkspaceTemplates; templateGetPath = templateListPath; break;
+ templateListPath = ApiEndpoint.WorkspaceTemplates;
+ templateGetPath = templateListPath;
+ break;
case ResourceType.WorkspaceService:
- templateListPath = ApiEndpoint.WorkspaceServiceTemplates; templateGetPath = templateListPath; break;
+ templateListPath = ApiEndpoint.WorkspaceServiceTemplates;
+ templateGetPath = templateListPath;
+ break;
case ResourceType.SharedService:
- templateListPath = ApiEndpoint.SharedServiceTemplates; templateGetPath = templateListPath; break;
+ templateListPath = ApiEndpoint.SharedServiceTemplates;
+ templateGetPath = templateListPath;
+ break;
case ResourceType.UserResource:
if (props.parentResource) {
// If we are creating a user resource, parent resource must have a workspaceId
- const workspaceId = (props.parentResource as WorkspaceService).workspaceId
+ const workspaceId = (props.parentResource as WorkspaceService)
+ .workspaceId;
templateListPath = `${ApiEndpoint.Workspaces}/${workspaceId}/${ApiEndpoint.WorkspaceServiceTemplates}/${props.parentResource.templateName}/${ApiEndpoint.UserResourceTemplates}`;
- templateGetPath = `${ApiEndpoint.WorkspaceServiceTemplates}/${props.parentResource.templateName}/${ApiEndpoint.UserResourceTemplates}`
- workspaceApplicationIdURI = props.workspaceApplicationIdURI
+ templateGetPath = `${ApiEndpoint.WorkspaceServiceTemplates}/${props.parentResource.templateName}/${ApiEndpoint.UserResourceTemplates}`;
+ workspaceApplicationIdURI = props.workspaceApplicationIdURI;
break;
} else {
- throw Error('Parent workspace service must be passed as prop when creating user resource.');
+ throw Error(
+ "Parent workspace service must be passed as prop when creating user resource.",
+ );
}
default:
- throw Error('Unsupported resource type.');
+ throw Error("Unsupported resource type.");
}
// Construct API path for resource creation
let resourcePath;
switch (props.resourceType) {
case ResourceType.Workspace:
- resourcePath = ApiEndpoint.Workspaces; break;
+ resourcePath = ApiEndpoint.Workspaces;
+ break;
case ResourceType.SharedService:
- resourcePath = ApiEndpoint.SharedServices; break;
+ resourcePath = ApiEndpoint.SharedServices;
+ break;
default:
if (!props.parentResource) {
- throw Error('A parentResource must be passed as prop if creating a workspace-service or user-resource');
+ throw Error(
+ "A parentResource must be passed as prop if creating a workspace-service or user-resource",
+ );
}
resourcePath = `${props.parentResource.resourcePath}/${props.resourceType}s`;
}
const selectTemplate = (templateName: string) => {
setTemplate(templateName);
- setPage('resourceForm');
- }
+ setPage("resourceForm");
+ };
const resourceCreating = async (operation: Operation) => {
setDeployOperation(operation);
- setPage('creating');
+ setPage("creating");
// Add deployment operation to notifications operation poller
dispatch(addUpdateOperation(operation));
// if an onAdd callback has been given, get the resource we just created and pass it back
if (props.onAddResource) {
- let resource = getResourceFromResult(await apiCall(operation.resourcePath, HttpMethod.Get, props.workspaceApplicationIdURI));
+ let resource = getResourceFromResult(
+ await apiCall(
+ operation.resourcePath,
+ HttpMethod.Get,
+ props.workspaceApplicationIdURI,
+ ),
+ );
props.onAddResource(resource);
}
- }
+ };
// Render the current panel sub-page
let currentPage;
switch (page) {
- case 'selectTemplate':
- currentPage = ; break;
- case 'resourceForm':
- currentPage = ; break;
- case 'creating':
- currentPage =
-
-
{props.updateResource?.id ? 'Updating' : 'Creating'} {props.resourceType}...
-
Check the notifications panel for deployment progress.
-
{navigate(deployOperation.resourcePath); props.onClose();}} />
- ; break;
+ case "selectTemplate":
+ currentPage = (
+
+ );
+ break;
+ case "resourceForm":
+ currentPage = (
+
+ );
+ break;
+ case "creating":
+ currentPage = (
+
+
+
+ {props.updateResource?.id ? "Updating" : "Creating"}{" "}
+ {props.resourceType}...
+
+
Check the notifications panel for deployment progress.
+
{
+ navigate(deployOperation.resourcePath);
+ props.onClose();
+ }}
+ />
+
+ );
+ break;
}
return (
@@ -157,9 +211,7 @@ export const CreateUpdateResource: React.FunctionComponent
-
- {currentPage}
-
+ {currentPage}
>
);
diff --git a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx
index 60cb48edfa..85f68337e3 100644
--- a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx
+++ b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx
@@ -1,26 +1,35 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import { useEffect, useState } from "react";
import { LoadingState } from "../../../models/loadingState";
-import { HttpMethod, ResultType, useAuthApiCall } from "../../../hooks/useAuthApiCall";
+import {
+ HttpMethod,
+ ResultType,
+ useAuthApiCall,
+} from "../../../hooks/useAuthApiCall";
import Form from "@rjsf/fluent-ui";
import { Operation } from "../../../models/operation";
import { Resource } from "../../../models/resource";
import { ResourceType } from "../../../models/resourceType";
import { APIError } from "../../../models/exceptions";
import { ExceptionLayout } from "../ExceptionLayout";
-import { ResourceTemplate, sanitiseTemplateForRJSF } from "../../../models/resourceTemplate";
+import {
+ ResourceTemplate,
+ sanitiseTemplateForRJSF,
+} from "../../../models/resourceTemplate";
import validator from "@rjsf/validator-ajv8";
interface ResourceFormProps {
- templateName: string,
- templatePath: string,
- resourcePath: string,
- updateResource?: Resource,
- onCreateResource: (operation: Operation) => void,
- workspaceApplicationIdURI?: string
+ templateName: string;
+ templatePath: string;
+ resourcePath: string;
+ updateResource?: Resource;
+ onCreateResource: (operation: Operation) => void;
+ workspaceApplicationIdURI?: string;
}
-export const ResourceForm: React.FunctionComponent = (props: ResourceFormProps) => {
+export const ResourceForm: React.FunctionComponent = (
+ props: ResourceFormProps,
+) => {
const [template, setTemplate] = useState(null);
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(LoadingState.Loading as LoadingState);
@@ -32,7 +41,12 @@ export const ResourceForm: React.FunctionComponent = (props:
const getFullTemplate = async () => {
try {
// Get the full resource template containing the required parameters
- const templateResponse = (await apiCall(props.updateResource ? `${props.templatePath}?is_update=true&version=${props.updateResource.templateVersion}` : props.templatePath, HttpMethod.Get)) as ResourceTemplate;
+ const templateResponse = (await apiCall(
+ props.updateResource
+ ? `${props.templatePath}?is_update=true&version=${props.updateResource.templateVersion}`
+ : props.templatePath,
+ HttpMethod.Get,
+ )) as ResourceTemplate;
// if it's an update, populate the form with the props that are available in the template
if (props.updateResource) {
@@ -56,7 +70,6 @@ export const ResourceForm: React.FunctionComponent = (props:
}, [apiCall, props.templatePath, template, props.updateResource]);
const removeReadOnlyProps = (data: any, template: ResourceTemplate): any => {
-
// flatten all the nested properties from across the template into a basic array we can iterate easily
let allProps = {} as any;
@@ -67,11 +80,11 @@ export const ResourceForm: React.FunctionComponent = (props:
allProps[prop] = templateFragment[key][prop];
});
}
- if (typeof (templateFragment[key]) === "object" && key !== "if") {
+ if (typeof templateFragment[key] === "object" && key !== "if") {
recurseTemplate(templateFragment[key]);
}
- })
- }
+ });
+ };
recurseTemplate(template);
@@ -84,7 +97,7 @@ export const ResourceForm: React.FunctionComponent = (props:
}
return data;
- }
+ };
const createUpdateResource = async (formData: any) => {
const data = removeReadOnlyProps(formData, template);
@@ -95,15 +108,18 @@ export const ResourceForm: React.FunctionComponent = (props:
try {
if (props.updateResource) {
const wsAuth =
- props.updateResource.resourceType === ResourceType.WorkspaceService
- || props.updateResource.resourceType === ResourceType.UserResource;
+ props.updateResource.resourceType === ResourceType.WorkspaceService ||
+ props.updateResource.resourceType === ResourceType.UserResource;
response = await apiCall(
props.updateResource.resourcePath,
HttpMethod.Patch,
wsAuth ? props.workspaceApplicationIdURI : undefined,
{ properties: data },
ResultType.JSON,
- undefined, undefined, props.updateResource._etag);
+ undefined,
+ undefined,
+ props.updateResource._etag,
+ );
} else {
const resource = { templateName: props.templateName, properties: data };
response = await apiCall(
@@ -111,57 +127,70 @@ export const ResourceForm: React.FunctionComponent = (props:
HttpMethod.Post,
props.workspaceApplicationIdURI,
resource,
- ResultType.JSON);
+ ResultType.JSON,
+ );
}
setSendingData(false);
props.onCreateResource(response.operation);
} catch (err: any) {
- err.userMessage = 'Error sending create / update request';
+ err.userMessage = "Error sending create / update request";
setApiError(err);
setLoading(LoadingState.Error);
setSendingData(false);
}
- }
+ };
// use the supplied uiSchema or create a blank one, and set the overview field to textarea manually.
const uiSchema = (template && template.uiSchema) || {};
uiSchema.overview = {
- "ui:widget": "textarea"
- }
+ "ui:widget": "textarea",
+ };
// if no specific order has been set, set a generic one with the primary fields at the top
if (!uiSchema["ui:order"] || uiSchema["ui:order"].length === 0) {
- uiSchema["ui:order"] = [
- "display_name",
- "description",
- "overview",
- "*"
- ]
+ uiSchema["ui:order"] = ["display_name", "description", "overview", "*"];
}
switch (loading) {
case LoadingState.Ok:
return (
- template &&
-
- {
- sendingData ?
-
- :
-
- )
- case LoadingState.Error:
- return (
-
+ template && (
+
+ {sendingData ? (
+
+ ) : (
+
+ )
);
+ case LoadingState.Error:
+ return ;
+ case LoadingState.Error:
+ return ;
default:
return (
-
+
- )
+ );
}
-}
+};
diff --git a/ui/app/src/components/shared/create-update-resource/SelectTemplate.tsx b/ui/app/src/components/shared/create-update-resource/SelectTemplate.tsx
index 42428c0c7b..f8a5c8b374 100644
--- a/ui/app/src/components/shared/create-update-resource/SelectTemplate.tsx
+++ b/ui/app/src/components/shared/create-update-resource/SelectTemplate.tsx
@@ -1,4 +1,11 @@
-import { DefaultButton, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack } from "@fluentui/react";
+import {
+ DefaultButton,
+ MessageBar,
+ MessageBarType,
+ Spinner,
+ SpinnerSize,
+ Stack,
+} from "@fluentui/react";
import { useEffect, useState } from "react";
import { LoadingState } from "../../../models/loadingState";
import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
@@ -6,68 +13,85 @@ import { APIError } from "../../../models/exceptions";
import { ExceptionLayout } from "../ExceptionLayout";
interface SelectTemplateProps {
- templatesPath: string,
- workspaceApplicationIdURI?: string | undefined,
- onSelectTemplate: (templateName: string) => void
+ templatesPath: string;
+ workspaceApplicationIdURI?: string | undefined;
+ onSelectTemplate: (templateName: string) => void;
}
-export const SelectTemplate: React.FunctionComponent = (props: SelectTemplateProps) => {
- const [templates, setTemplates] = useState(null);
- const [loading, setLoading] = useState(LoadingState.Loading as LoadingState);
- const apiCall = useAuthApiCall();
- const [apiError, setApiError] = useState({} as APIError);
+export const SelectTemplate: React.FunctionComponent = (
+ props: SelectTemplateProps,
+) => {
+ const [templates, setTemplates] = useState(null);
+ const [loading, setLoading] = useState(LoadingState.Loading as LoadingState);
+ const apiCall = useAuthApiCall();
+ const [apiError, setApiError] = useState({} as APIError);
- useEffect(() => {
- const getTemplates = async () => {
- try {
- const templatesResponse = await apiCall(props.templatesPath, HttpMethod.Get, props.workspaceApplicationIdURI);
- setTemplates(templatesResponse.templates);
- setLoading(LoadingState.Ok);
- } catch (err: any){
- err.userMessage = 'Error retrieving templates';
- setApiError(err);
- setLoading(LoadingState.Error);
- }
- };
+ useEffect(() => {
+ const getTemplates = async () => {
+ try {
+ const templatesResponse = await apiCall(
+ props.templatesPath,
+ HttpMethod.Get,
+ props.workspaceApplicationIdURI,
+ );
+ setTemplates(templatesResponse.templates);
+ setLoading(LoadingState.Ok);
+ } catch (err: any) {
+ err.userMessage = "Error retrieving templates";
+ setApiError(err);
+ setLoading(LoadingState.Error);
+ }
+ };
- // Fetch resource templates only if not already fetched
- if (!templates) {
- getTemplates();
- }
- }, [apiCall, props.templatesPath, templates, props.workspaceApplicationIdURI]);
+ // Fetch resource templates only if not already fetched
+ if (!templates) {
+ getTemplates();
+ }
+ }, [
+ apiCall,
+ props.templatesPath,
+ templates,
+ props.workspaceApplicationIdURI,
+ ]);
- switch (loading) {
- case LoadingState.Ok:
- return (
- templates && templates.length > 0 ?
- {
- templates.map((template: any, i) => {
- return (
-
-
{template.title}
-
{template.description}
-
props.onSelectTemplate(template.name)}/>
-
- )
- })
- }
- :
- No templates found
- Looks like there aren't any templates registered for this resource type.
-
- )
- case LoadingState.Error:
+ switch (loading) {
+ case LoadingState.Ok:
+ return templates && templates.length > 0 ? (
+
+ {templates.map((template: any, i) => {
return (
-
+
+
{template.title}
+
{template.description}
+
props.onSelectTemplate(template.name)}
+ />
+
);
- default:
- return (
-
-
-
- )
- }
-}
+ })}
+
+ ) : (
+
+ No templates found
+
+ Looks like there aren't any templates registered for this resource
+ type.
+
+
+ );
+ case LoadingState.Error:
+ return ;
+ default:
+ return (
+
+
+
+ );
+ }
+};
diff --git a/ui/app/src/components/shared/notifications/NotificationItem.tsx b/ui/app/src/components/shared/notifications/NotificationItem.tsx
index 9a44d412c8..4d81507ac7 100644
--- a/ui/app/src/components/shared/notifications/NotificationItem.tsx
+++ b/ui/app/src/components/shared/notifications/NotificationItem.tsx
@@ -1,25 +1,42 @@
-import React, { useEffect, useState } from 'react';
-import { Icon, ProgressIndicator, Link as FluentLink, Stack, DefaultPalette, Shimmer, ShimmerElementType } from '@fluentui/react';
-import { TRENotification } from '../../../models/treNotification';
-import { awaitingStates, completedStates, failedStates, inProgressStates, Operation, OperationStep } from '../../../models/operation';
-import { Link } from 'react-router-dom';
-import moment from 'moment';
-import { useInterval } from './useInterval';
-import { HttpMethod, useAuthApiCall } from '../../../hooks/useAuthApiCall';
-import { ApiEndpoint } from '../../../models/apiEndpoints';
-import { getResourceFromResult, Resource } from '../../../models/resource';
-import { NotificationPoller } from './NotificationPoller';
-import { APIError } from '../../../models/exceptions';
-import { ExceptionLayout } from '../ExceptionLayout';
-import { addUpdateOperation } from './operationsSlice';
-import { useAppDispatch } from '../../../hooks/customReduxHooks';
+import React, { useEffect, useState } from "react";
+import {
+ Icon,
+ ProgressIndicator,
+ Link as FluentLink,
+ Stack,
+ DefaultPalette,
+ Shimmer,
+ ShimmerElementType,
+} from "@fluentui/react";
+import { TRENotification } from "../../../models/treNotification";
+import {
+ awaitingStates,
+ completedStates,
+ failedStates,
+ inProgressStates,
+ Operation,
+ OperationStep,
+} from "../../../models/operation";
+import { Link } from "react-router-dom";
+import moment from "moment";
+import { useInterval } from "./useInterval";
+import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
+import { ApiEndpoint } from "../../../models/apiEndpoints";
+import { getResourceFromResult, Resource } from "../../../models/resource";
+import { NotificationPoller } from "./NotificationPoller";
+import { APIError } from "../../../models/exceptions";
+import { ExceptionLayout } from "../ExceptionLayout";
+import { addUpdateOperation } from "./operationsSlice";
+import { useAppDispatch } from "../../../hooks/customReduxHooks";
interface NotificationItemProps {
- operation: Operation,
+ operation: Operation;
showCallout: (o: Operation, r: Resource) => void;
}
-export const NotificationItem: React.FunctionComponent = (props: NotificationItemProps) => {
+export const NotificationItem: React.FunctionComponent<
+ NotificationItemProps
+> = (props: NotificationItemProps) => {
const [now, setNow] = useState(moment.utc());
const [isExpanded, setIsExpanded] = useState(false);
const [notification, setNotification] = useState({} as TRENotification);
@@ -31,7 +48,7 @@ export const NotificationItem: React.FunctionComponent =
const [apiError, setApiError] = useState({} as APIError);
const getRelativeTime = (createdWhen: number) => {
- return (moment.utc(moment.unix(createdWhen))).from(now);
+ return moment.utc(moment.unix(createdWhen)).from(now);
};
useEffect(() => {
@@ -45,15 +62,26 @@ export const NotificationItem: React.FunctionComponent =
try {
// is this a workspace, or workspace child resource operation?
if (op.resourcePath.indexOf(ApiEndpoint.Workspaces) !== -1) {
- const wsId = op.resourcePath.split('/')[2];
- let scopeId = (await apiCall(`${ApiEndpoint.Workspaces}/${wsId}/scopeid`, HttpMethod.Get)).workspaceAuth.scopeId;
- ws = (await apiCall(`${ApiEndpoint.Workspaces}/${wsId}`, HttpMethod.Get, scopeId)).workspace;
+ const wsId = op.resourcePath.split("/")[2];
+ let scopeId = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${wsId}/scopeid`,
+ HttpMethod.Get,
+ )
+ ).workspaceAuth.scopeId;
+ ws = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${wsId}`,
+ HttpMethod.Get,
+ scopeId,
+ )
+ ).workspace;
// is a workspace child resource operation
- if (op.resourcePath.split('/').length >= 3) {
+ if (op.resourcePath.split("/").length >= 3) {
let r = await apiCall(op.resourcePath, HttpMethod.Get, scopeId);
resource = getResourceFromResult(r);
- // is a workspace operation
+ // is a workspace operation
} else {
resource = ws;
}
@@ -71,7 +99,6 @@ export const NotificationItem: React.FunctionComponent =
};
setupNotification(props.operation);
-
}, [props.operation, apiCall, notification.resource]);
// update the 'now' time for comparison
@@ -80,10 +107,10 @@ export const NotificationItem: React.FunctionComponent =
}, 10000);
const getIconAndColourForStatus = (status: string) => {
- if (failedStates.includes(status)) return ['ErrorBadge', 'red'];
- if (completedStates.includes(status)) return ['SkypeCheck', 'green'];
- if (awaitingStates.includes(status)) return ['Clock', '#cccccc'];
- return ['ProgressLoopInner', DefaultPalette.themePrimary];
+ if (failedStates.includes(status)) return ["ErrorBadge", "red"];
+ if (completedStates.includes(status)) return ["SkypeCheck", "green"];
+ if (awaitingStates.includes(status)) return ["Clock", "#cccccc"];
+ return ["ProgressLoopInner", DefaultPalette.themePrimary];
};
const updateOperation = (operation: Operation) => {
@@ -95,71 +122,140 @@ export const NotificationItem: React.FunctionComponent =
return (
<>
- {
- props.operation.dismiss ? <>> :
- loadingNotification ?
-
-
-
-
-
-
-
-
- :
- errorNotification ?
-
-
-
- :
-
-
- {
- inProgressStates.indexOf(props.operation.status) !== -1 &&
- updateOperation(operation)} />
- }
-
-
-
- {notification.resource.properties.display_name}: {props.operation.action}
- }
- description={`${notification.resource.resourceType} is ${props.operation.status}`} />
-
-
-
- {
- props.operation.steps && props.operation.steps.length > 0 && !(props.operation.steps.length === 1 && props.operation.steps[0].templateStepId === 'main') ?
- { setIsExpanded(!isExpanded); }} style={{ position: 'relative', top: '2px' }}>{isExpanded ? : }
- :
- ' '
- }
-
- {getRelativeTime(props.operation.createdWhen)}
-
-
- {
- isExpanded &&
- <>
-
- {props.operation.steps && props.operation.steps.map((s: OperationStep, i: number) => {
- return (
-
-
- {
- s.templateStepId === "main" ?
- <>{notification.resource.properties.display_name}: {props.operation.action}> :
- s.stepTitle
- }
- );
- })
- }
-
- >
- }
-
- }
- >);
+ {props.operation.dismiss ? (
+ <>>
+ ) : loadingNotification ? (
+
+
+
+
+
+
+
+
+ ) : errorNotification ? (
+
+
+
+ ) : (
+
+ {inProgressStates.indexOf(props.operation.status) !== -1 && (
+
+ updateOperation(operation)
+ }
+ />
+ )}
+
+
+
+ {notification.resource.properties.display_name}:{" "}
+ {props.operation.action}
+
+ }
+ description={`${notification.resource.resourceType} is ${props.operation.status}`}
+ />
+
+
+
+ {props.operation.steps &&
+ props.operation.steps.length > 0 &&
+ !(
+ props.operation.steps.length === 1 &&
+ props.operation.steps[0].templateStepId === "main"
+ ) ? (
+ {
+ setIsExpanded(!isExpanded);
+ }}
+ style={{ position: "relative", top: "2px" }}
+ >
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ ) : (
+ " "
+ )}
+
+
+ {" "}
+
+ {getRelativeTime(props.operation.createdWhen)}
+
+
+
+
+ {isExpanded && (
+ <>
+
+ {props.operation.steps &&
+ props.operation.steps.map((s: OperationStep, i: number) => {
+ return (
+
+
+ {s.templateStepId === "main" ? (
+ <>
+ {notification.resource.properties.display_name}:{" "}
+ {props.operation.action}
+ >
+ ) : (
+ s.stepTitle
+ )}
+
+ );
+ })}
+
+ >
+ )}
+
+ )}
+ >
+ );
};
diff --git a/ui/app/src/components/shared/notifications/NotificationPanel.tsx b/ui/app/src/components/shared/notifications/NotificationPanel.tsx
index 3d7c03b92f..4d0e85d742 100644
--- a/ui/app/src/components/shared/notifications/NotificationPanel.tsx
+++ b/ui/app/src/components/shared/notifications/NotificationPanel.tsx
@@ -1,105 +1,173 @@
-import { Callout, DirectionalHint, FontWeights, Icon, Link, mergeStyleSets, MessageBar, MessageBarType, Panel, ProgressIndicator, Text } from '@fluentui/react';
-import React, { useEffect, useState } from 'react';
-import { completedStates, inProgressStates, Operation, successStates } from '../../../models/operation';
-import { NotificationItem } from './NotificationItem';
-import { IconButton } from '@fluentui/react/lib/Button';
-import { HttpMethod, useAuthApiCall } from '../../../hooks/useAuthApiCall';
-import { ApiEndpoint } from '../../../models/apiEndpoints';
-import { Resource } from '../../../models/resource';
-import { useAppDispatch, useAppSelector } from '../../../hooks/customReduxHooks';
-import { setInitialOperations, dismissCompleted } from '../../shared/notifications/operationsSlice';
+import {
+ Callout,
+ DirectionalHint,
+ FontWeights,
+ Icon,
+ Link,
+ mergeStyleSets,
+ MessageBar,
+ MessageBarType,
+ Panel,
+ ProgressIndicator,
+ Text,
+} from "@fluentui/react";
+import React, { useEffect, useState } from "react";
+import {
+ completedStates,
+ inProgressStates,
+ Operation,
+ successStates,
+} from "../../../models/operation";
+import { NotificationItem } from "./NotificationItem";
+import { IconButton } from "@fluentui/react/lib/Button";
+import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
+import { ApiEndpoint } from "../../../models/apiEndpoints";
+import { Resource } from "../../../models/resource";
+import {
+ useAppDispatch,
+ useAppSelector,
+} from "../../../hooks/customReduxHooks";
+import {
+ setInitialOperations,
+ dismissCompleted,
+} from "../../shared/notifications/operationsSlice";
export const NotificationPanel: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [showCallout, setShowCallout] = useState(false);
- const [calloutDetails, setCalloutDetails] = useState({ title: '', text: '', success: true });
+ const [calloutDetails, setCalloutDetails] = useState({
+ title: "",
+ text: "",
+ success: true,
+ });
const apiCall = useAuthApiCall();
const operations = useAppSelector((state) => state.operations);
const dispatch = useAppDispatch();
useEffect(() => {
const loadAllOps = async () => {
- let opsToAdd = (await apiCall(`${ApiEndpoint.Operations}`, HttpMethod.Get)).operations as Array;
+ let opsToAdd = (
+ await apiCall(`${ApiEndpoint.Operations}`, HttpMethod.Get)
+ ).operations as Array;
dispatch(setInitialOperations(opsToAdd));
};
loadAllOps();
- }, [apiCall, dispatch])
+ }, [apiCall, dispatch]);
const callout = (o: Operation, r: Resource) => {
if (successStates.includes(o.status)) {
setCalloutDetails({
title: "Operation Succeeded",
text: `${o.action} for ${r.properties.display_name} completed successfully`,
- success: true
+ success: true,
});
} else {
setCalloutDetails({
title: "Operation Failed",
text: `${o.action} for ${r.properties.display_name} completed with status ${o.status}`,
- success: false
+ success: false,
});
}
setShowCallout(true);
- }
+ };
return (
<>
- setIsOpen(true)} title="Notifications" ariaLabel="Notifications" />
+ setIsOpen(true)}
+ title="Notifications"
+ ariaLabel="Notifications"
+ />
- {
- operations.items && operations.items.filter((o: Operation) => inProgressStates.includes(o.status)).length > 0 &&
-
-
-
- }
+ {operations.items &&
+ operations.items.filter((o: Operation) =>
+ inProgressStates.includes(o.status),
+ ).length > 0 && (
+
+
+
+ )}
- {
- showCallout && !isOpen &&
+ {showCallout && !isOpen && (
{ setShowCallout(false) }}
+ onDismiss={() => {
+ setShowCallout(false);
+ }}
directionalHint={DirectionalHint.bottomLeftEdge}
setInitialFocus
>
-
- {calloutDetails.success ?
-
- :
-
- }
+
+ {calloutDetails.success ? (
+
+ ) : (
+
+ )}
{calloutDetails.title}
-
+
{calloutDetails.text}
- }
+ )}
{ setIsOpen(false) }}
+ onDismiss={() => {
+ setIsOpen(false);
+ }}
closeButtonAriaLabel="Close Notifications"
>
- { dispatch(dismissCompleted()); return false; }} disabled={
- operations.items.filter((o: Operation) => o.dismiss !== true && completedStates.includes(o.status)).length === 0
- }>Dismiss Completed
+ {
+ dispatch(dismissCompleted());
+ return false;
+ }}
+ disabled={
+ operations.items.filter(
+ (o: Operation) =>
+ o.dismiss !== true && completedStates.includes(o.status),
+ ).length === 0
+ }
+ >
+ Dismiss Completed
+
- {
- operations.items.length === 0 &&
-
+ {operations.items.length === 0 && (
+
{
No notifications to display
- }
+ )}
- {
- operations.items.map((o: Operation, i: number) => {
- return (
- callout(o, r)} />
- )
- })
- }
+ {operations.items.map((o: Operation, i: number) => {
+ return (
+ callout(o, r)}
+ />
+ );
+ })}
>
@@ -124,30 +194,30 @@ export const NotificationPanel: React.FunctionComponent = () => {
const styles = mergeStyleSets({
buttonArea: {
- verticalAlign: 'top',
- display: 'inline-block',
- textAlign: 'center',
- margin: '0 100px',
+ verticalAlign: "top",
+ display: "inline-block",
+ textAlign: "center",
+ margin: "0 100px",
minWidth: 130,
height: 32,
},
configArea: {
width: 300,
- display: 'inline-block',
+ display: "inline-block",
},
button: {
width: 130,
},
callout: {
width: 320,
- padding: '20px 24px',
+ padding: "20px 24px",
},
title: {
marginBottom: 12,
fontWeight: FontWeights.semilight,
},
link: {
- display: 'block',
+ display: "block",
marginTop: 20,
},
});
diff --git a/ui/app/src/components/shared/notifications/NotificationPoller.tsx b/ui/app/src/components/shared/notifications/NotificationPoller.tsx
index 9a70c984a2..c88b1e1b7d 100644
--- a/ui/app/src/components/shared/notifications/NotificationPoller.tsx
+++ b/ui/app/src/components/shared/notifications/NotificationPoller.tsx
@@ -1,24 +1,32 @@
-import React from 'react';
-import { useInterval } from './useInterval';
-import { HttpMethod, useAuthApiCall } from '../../../hooks/useAuthApiCall';
-import { ApiEndpoint } from '../../../models/apiEndpoints';
-import { TRENotification } from '../../../models/treNotification';
-import { Operation } from '../../../models/operation';
-import config from '../../../config.json';
+import React from "react";
+import { useInterval } from "./useInterval";
+import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
+import { ApiEndpoint } from "../../../models/apiEndpoints";
+import { TRENotification } from "../../../models/treNotification";
+import { Operation } from "../../../models/operation";
+import config from "../../../config.json";
interface NotificationPollerProps {
- notification: TRENotification,
- updateOperation: (n: Operation) => void
+ notification: TRENotification;
+ updateOperation: (n: Operation) => void;
}
-export const NotificationPoller: React.FunctionComponent
= (props: NotificationPollerProps) => {
+export const NotificationPoller: React.FunctionComponent<
+ NotificationPollerProps
+> = (props: NotificationPollerProps) => {
const apiCall = useAuthApiCall();
useInterval(async () => {
-
try {
- let op = (await apiCall(`${props.notification.operation.resourcePath}/${ApiEndpoint.Operations}/${props.notification.operation.id}`,
- HttpMethod.Get, props.notification.workspace ? props.notification.workspace.properties.scope_id: null)).operation as Operation;
+ let op = (
+ await apiCall(
+ `${props.notification.operation.resourcePath}/${ApiEndpoint.Operations}/${props.notification.operation.id}`,
+ HttpMethod.Get,
+ props.notification.workspace
+ ? props.notification.workspace.properties.scope_id
+ : null,
+ )
+ ).operation as Operation;
// check if any fields have changed - ie the json is any different. we don't care _what_ has changed, just that something has
if (JSON.stringify(op) !== JSON.stringify(props.notification.operation)) {
@@ -27,9 +35,12 @@ export const NotificationPoller: React.FunctionComponent>);
+ return <>>;
};
diff --git a/ui/app/src/components/shared/notifications/dummyOp.json b/ui/app/src/components/shared/notifications/dummyOp.json
index ddd3e847e3..0473a6117d 100644
--- a/ui/app/src/components/shared/notifications/dummyOp.json
+++ b/ui/app/src/components/shared/notifications/dummyOp.json
@@ -1,20 +1,18 @@
{
- "id": "36847de7-aa82-40a8-bbe7-d211bd677467",
- "resourceId": "8c70974a-5f66-4ae9-9502-7a54e9e0bb86",
- "resourcePath": "/workspaces/1e800001-7385-46a1-9f6d-490a6201ea01/workspace-services/8c70974a-5f66-4ae9-9502-7a54e9e0bb86",
- "resourceVersion": 0,
- "status": "deploying",
- "action": "install",
- "message": "8c70974a-5f66-4ae9-9502-7a54e9e0bb86: install action completed successfully.",
- "createdWhen": 1650653543.343581,
- "updatedWhen": 1650653543.343581,
- "user": {
- "id": "7f9756c3-7925-4b78-a10b-83927ab9c008",
- "name": "joalmeid@microsoft.com Lopes de Almeida",
- "email": "",
- "roles": [
- "WorkspaceOwner"
- ],
- "roleAssignments": []
- }
+ "id": "36847de7-aa82-40a8-bbe7-d211bd677467",
+ "resourceId": "8c70974a-5f66-4ae9-9502-7a54e9e0bb86",
+ "resourcePath": "/workspaces/1e800001-7385-46a1-9f6d-490a6201ea01/workspace-services/8c70974a-5f66-4ae9-9502-7a54e9e0bb86",
+ "resourceVersion": 0,
+ "status": "deploying",
+ "action": "install",
+ "message": "8c70974a-5f66-4ae9-9502-7a54e9e0bb86: install action completed successfully.",
+ "createdWhen": 1650653543.343581,
+ "updatedWhen": 1650653543.343581,
+ "user": {
+ "id": "7f9756c3-7925-4b78-a10b-83927ab9c008",
+ "name": "joalmeid@microsoft.com Lopes de Almeida",
+ "email": "",
+ "roles": ["WorkspaceOwner"],
+ "roleAssignments": []
+ }
}
diff --git a/ui/app/src/components/shared/notifications/dummyOpSteps.json b/ui/app/src/components/shared/notifications/dummyOpSteps.json
index 111bcbd898..7ffdd043a1 100644
--- a/ui/app/src/components/shared/notifications/dummyOpSteps.json
+++ b/ui/app/src/components/shared/notifications/dummyOpSteps.json
@@ -1,55 +1,53 @@
{
- "id": "ba02d504-47d5-412d-b192-b75d17c65009",
- "resourceId": "8b6e42a0-e236-46ae-9541-01b462e4b468",
- "resourcePath": "/workspaces/1e800001-7385-46a1-9f6d-490a6201ea01/workspace-services/8c70974a-5f66-4ae9-9502-7a54e9e0bb86/user-resources/8b6e42a0-e236-46ae-9541-01b462e4b468",
- "resourceVersion": 0,
- "status": "pipeline_failed",
- "action": "install",
- "message": "Pipeline deployment completed successfully",
- "createdWhen": 1650653543.343581,
- "updatedWhen": 1650653543.343581,
- "user": {
- "id": "7f9756c3-7925-4b78-a10b-83927ab9c008",
- "name": "joalmeid@microsoft.com Lopes de Almeida",
- "email": "",
- "roles": [
- "WorkspaceOwner"
- ],
- "roleAssignments": []
+ "id": "ba02d504-47d5-412d-b192-b75d17c65009",
+ "resourceId": "8b6e42a0-e236-46ae-9541-01b462e4b468",
+ "resourcePath": "/workspaces/1e800001-7385-46a1-9f6d-490a6201ea01/workspace-services/8c70974a-5f66-4ae9-9502-7a54e9e0bb86/user-resources/8b6e42a0-e236-46ae-9541-01b462e4b468",
+ "resourceVersion": 0,
+ "status": "pipeline_failed",
+ "action": "install",
+ "message": "Pipeline deployment completed successfully",
+ "createdWhen": 1650653543.343581,
+ "updatedWhen": 1650653543.343581,
+ "user": {
+ "id": "7f9756c3-7925-4b78-a10b-83927ab9c008",
+ "name": "joalmeid@microsoft.com Lopes de Almeida",
+ "email": "",
+ "roles": ["WorkspaceOwner"],
+ "roleAssignments": []
+ },
+ "steps": [
+ {
+ "stepId": "6d2d7eb7-984e-4330-bd3c-c7ec98658402",
+ "stepTitle": "Update the firewall the first time",
+ "resourceId": "ea079a34-ec53-43e0-9454-84531c96599e",
+ "resourceTemplateName": "tre-shared-service-firewall",
+ "resourceType": "shared-service",
+ "resourceAction": "upgrade",
+ "status": "action_succeeded",
+ "message": "ea079a34-ec53-43e0-9454-84531c96599e: upgrade action completed successfully.",
+ "updatedWhen": 1650898800.932936
},
- "steps": [
- {
- "stepId": "6d2d7eb7-984e-4330-bd3c-c7ec98658402",
- "stepTitle": "Update the firewall the first time",
- "resourceId": "ea079a34-ec53-43e0-9454-84531c96599e",
- "resourceTemplateName": "tre-shared-service-firewall",
- "resourceType": "shared-service",
- "resourceAction": "upgrade",
- "status": "action_succeeded",
- "message": "ea079a34-ec53-43e0-9454-84531c96599e: upgrade action completed successfully.",
- "updatedWhen": 1650898800.932936
- },
- {
- "stepId": "main",
- "stepTitle": "Main step for 8b6e42a0-e236-46ae-9541-01b462e4b468",
- "resourceId": "8b6e42a0-e236-46ae-9541-01b462e4b4686",
- "resourceTemplateName": "tre-service-dev-vm",
- "resourceType": "user-resource",
- "resourceAction": "install",
- "status": "deployment_failed",
- "message": "8b6e42a0-e236-46ae-9541-01b462e4b468: install action completed successfully.",
- "updatedWhen": 1650899841.316169
- },
- {
- "stepId": "2fe8a6a7-2c27-4c49-8773-127df8a48b4e",
- "stepTitle": "Update the firewall the second time",
- "resourceId": "ea079a34-ec53-43e0-9454-84531c96599e",
- "resourceTemplateName": "tre-shared-service-firewall",
- "resourceType": "shared-service",
- "resourceAction": "upgrade",
- "status": "awaiting_deployment",
- "message": "ea079a34-ec53-43e0-9454-84531c96599e: upgrade action in progress",
- "updatedWhen": 1650899911.042068
- }
- ]
+ {
+ "stepId": "main",
+ "stepTitle": "Main step for 8b6e42a0-e236-46ae-9541-01b462e4b468",
+ "resourceId": "8b6e42a0-e236-46ae-9541-01b462e4b4686",
+ "resourceTemplateName": "tre-service-dev-vm",
+ "resourceType": "user-resource",
+ "resourceAction": "install",
+ "status": "deployment_failed",
+ "message": "8b6e42a0-e236-46ae-9541-01b462e4b468: install action completed successfully.",
+ "updatedWhen": 1650899841.316169
+ },
+ {
+ "stepId": "2fe8a6a7-2c27-4c49-8773-127df8a48b4e",
+ "stepTitle": "Update the firewall the second time",
+ "resourceId": "ea079a34-ec53-43e0-9454-84531c96599e",
+ "resourceTemplateName": "tre-shared-service-firewall",
+ "resourceType": "shared-service",
+ "resourceAction": "upgrade",
+ "status": "awaiting_deployment",
+ "message": "ea079a34-ec53-43e0-9454-84531c96599e: upgrade action in progress",
+ "updatedWhen": 1650899911.042068
+ }
+ ]
}
diff --git a/ui/app/src/components/shared/notifications/operationsSlice.ts b/ui/app/src/components/shared/notifications/operationsSlice.ts
index d8095345e6..98ae538e64 100644
--- a/ui/app/src/components/shared/notifications/operationsSlice.ts
+++ b/ui/app/src/components/shared/notifications/operationsSlice.ts
@@ -1,25 +1,27 @@
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { completedStates, Operation } from '../../../models/operation';
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { completedStates, Operation } from "../../../models/operation";
interface OperationsState {
- items: Array
+ items: Array;
}
const initialState: OperationsState = {
- items: []
+ items: [],
};
// note - we can write what looks like state mutations here because the redux toolkit uses
// Immer under the hood to make everything immutable
const operationsSlice = createSlice({
- name: 'operations',
+ name: "operations",
initialState,
reducers: {
setInitialOperations(state, action: PayloadAction>) {
state.items = action.payload;
},
addUpdateOperation(state, action: PayloadAction) {
- let i = state.items.findIndex((f: Operation) => f.id === action.payload.id);
+ let i = state.items.findIndex(
+ (f: Operation) => f.id === action.payload.id,
+ );
if (i !== -1) {
state.items.splice(i, 1, action.payload);
} else {
@@ -32,9 +34,10 @@ const operationsSlice = createSlice({
o.dismiss = true;
}
});
- }
- }
+ },
+ },
});
-export const { setInitialOperations, addUpdateOperation, dismissCompleted } = operationsSlice.actions;
+export const { setInitialOperations, addUpdateOperation, dismissCompleted } =
+ operationsSlice.actions;
export default operationsSlice.reducer;
diff --git a/ui/app/src/components/shared/notifications/useInterval.ts b/ui/app/src/components/shared/notifications/useInterval.ts
index 623ccbd813..47ebc1e895 100644
--- a/ui/app/src/components/shared/notifications/useInterval.ts
+++ b/ui/app/src/components/shared/notifications/useInterval.ts
@@ -2,19 +2,19 @@
import { useEffect, useRef } from "react";
export const useInterval = (callback: () => void, delay: number | null) => {
- const savedCallback = useRef(callback);
-
- useEffect(() => {
- savedCallback.current = callback;
- }, [callback]);
-
- useEffect(() => {
- const tick = () => {
- savedCallback.current && savedCallback.current();
- }
- if (delay !== null) {
- let id = setInterval(tick, delay);
- return () => clearInterval(id);
- }
- }, [delay]);
-};
\ No newline at end of file
+ const savedCallback = useRef(callback);
+
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+
+ useEffect(() => {
+ const tick = () => {
+ savedCallback.current && savedCallback.current();
+ };
+ if (delay !== null) {
+ let id = setInterval(tick, delay);
+ return () => clearInterval(id);
+ }
+ }, [delay]);
+};
diff --git a/ui/app/src/components/workspaces/UserResourceItem.tsx b/ui/app/src/components/workspaces/UserResourceItem.tsx
index f5e85c66f4..9507fdfd4b 100644
--- a/ui/app/src/components/workspaces/UserResourceItem.tsx
+++ b/ui/app/src/components/workspaces/UserResourceItem.tsx
@@ -1,13 +1,13 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { useAuthApiCall, HttpMethod } from '../../hooks/useAuthApiCall';
-import { UserResource } from '../../models/userResource';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { ResourceHeader } from '../shared/ResourceHeader';
-import { Resource } from '../../models/resource';
-import { useComponentManager } from '../../hooks/useComponentManager';
-import { ResourceBody } from '../shared/ResourceBody';
+import React, { useContext, useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { useAuthApiCall, HttpMethod } from "../../hooks/useAuthApiCall";
+import { UserResource } from "../../models/userResource";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { ResourceHeader } from "../shared/ResourceHeader";
+import { Resource } from "../../models/resource";
+import { useComponentManager } from "../../hooks/useComponentManager";
+import { ResourceBody } from "../shared/ResourceBody";
interface UserResourceItemProps {
userResource?: UserResource;
@@ -15,7 +15,9 @@ interface UserResourceItemProps {
removeUserResource: (u: UserResource) => void;
}
-export const UserResourceItem: React.FunctionComponent = (props: UserResourceItemProps) => {
+export const UserResourceItem: React.FunctionComponent<
+ UserResourceItemProps
+> = (props: UserResourceItemProps) => {
const { workspaceServiceId, userResourceId } = useParams();
const [userResource, setUserResource] = useState({} as UserResource);
const apiCall = useAuthApiCall();
@@ -24,12 +26,17 @@ export const UserResourceItem: React.FunctionComponent =
const latestUpdate = useComponentManager(
userResource,
- (r: Resource) => { props.updateUserResource(r as UserResource); setUserResource(r as UserResource); },
+ (r: Resource) => {
+ props.updateUserResource(r as UserResource);
+ setUserResource(r as UserResource);
+ },
(r: Resource) => {
props.removeUserResource(r as UserResource);
if (workspaceCtx.workspace.id)
- navigate(`/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}`);
- }
+ navigate(
+ `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}`,
+ );
+ },
);
useEffect(() => {
@@ -38,19 +45,30 @@ export const UserResourceItem: React.FunctionComponent =
if (props.userResource && props.userResource.id) {
setUserResource(props.userResource);
} else if (workspaceCtx.workspace.id) {
- let ur = await apiCall(`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}/${ApiEndpoint.UserResources}/${userResourceId}`, HttpMethod.Get, workspaceCtx.workspaceApplicationIdURI);
+ let ur = await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}/${ApiEndpoint.UserResources}/${userResourceId}`,
+ HttpMethod.Get,
+ workspaceCtx.workspaceApplicationIdURI,
+ );
setUserResource(ur.userResource);
}
};
getData();
- }, [apiCall, props.userResource, workspaceCtx.workspaceApplicationIdURI, userResourceId, workspaceServiceId, workspaceCtx.workspace.id]);
+ }, [
+ apiCall,
+ props.userResource,
+ workspaceCtx.workspaceApplicationIdURI,
+ userResourceId,
+ workspaceServiceId,
+ workspaceCtx.workspace.id,
+ ]);
- return (
- userResource && userResource.id ?
- <>
-
-
- >
- : <>>
+ return userResource && userResource.id ? (
+ <>
+
+
+ >
+ ) : (
+ <>>
);
};
diff --git a/ui/app/src/components/workspaces/WorkspaceHeader.tsx b/ui/app/src/components/workspaces/WorkspaceHeader.tsx
index 99feba78fb..96f468c73f 100644
--- a/ui/app/src/components/workspaces/WorkspaceHeader.tsx
+++ b/ui/app/src/components/workspaces/WorkspaceHeader.tsx
@@ -1,6 +1,6 @@
-import { getTheme, Icon, mergeStyles, Stack } from '@fluentui/react';
-import React, { useContext } from 'react';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
+import { getTheme, Icon, mergeStyles, Stack } from "@fluentui/react";
+import React, { useContext } from "react";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
export const WorkspaceHeader: React.FunctionComponent = () => {
const workspaceCtx = useContext(WorkspaceContext);
@@ -8,9 +8,16 @@ export const WorkspaceHeader: React.FunctionComponent = () => {
return (
<>
-
-
-
+
+
+
{workspaceCtx.workspace?.properties?.display_name}
@@ -24,8 +31,8 @@ const contentClass = mergeStyles([
{
backgroundColor: theme.palette.themeDarker,
color: theme.palette.white,
- lineHeight: '15px',
- padding: '0 20px',
- boxShadow: '0 1px 8px 0px #ccc'
- }
+ lineHeight: "15px",
+ padding: "0 20px",
+ boxShadow: "0 1px 8px 0px #ccc",
+ },
]);
diff --git a/ui/app/src/components/workspaces/WorkspaceItem.tsx b/ui/app/src/components/workspaces/WorkspaceItem.tsx
index 923ef42751..d71ed2906d 100644
--- a/ui/app/src/components/workspaces/WorkspaceItem.tsx
+++ b/ui/app/src/components/workspaces/WorkspaceItem.tsx
@@ -1,12 +1,11 @@
-import React, { useContext } from 'react';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { Resource } from '../../models/resource';
-import { Workspace } from '../../models/workspace';
-import { useComponentManager } from '../../hooks/useComponentManager';
-import { ResourceHeader } from '../shared/ResourceHeader';
-import { useNavigate } from 'react-router-dom';
-import { ResourceBody } from '../shared/ResourceBody';
-
+import React, { useContext } from "react";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { Resource } from "../../models/resource";
+import { Workspace } from "../../models/workspace";
+import { useComponentManager } from "../../hooks/useComponentManager";
+import { ResourceHeader } from "../shared/ResourceHeader";
+import { useNavigate } from "react-router-dom";
+import { ResourceBody } from "../shared/ResourceBody";
export const WorkspaceItem: React.FunctionComponent = () => {
const workspaceCtx = useContext(WorkspaceContext);
@@ -15,12 +14,15 @@ export const WorkspaceItem: React.FunctionComponent = () => {
const latestUpdate = useComponentManager(
workspaceCtx.workspace,
(r: Resource) => workspaceCtx.setWorkspace(r as Workspace),
- (r: Resource) => navigate(`/`)
+ (r: Resource) => navigate(`/`),
);
return (
<>
-
+
>
);
diff --git a/ui/app/src/components/workspaces/WorkspaceLeftNav.tsx b/ui/app/src/components/workspaces/WorkspaceLeftNav.tsx
index 0a03bd15d0..f5a9288ba2 100644
--- a/ui/app/src/components/workspaces/WorkspaceLeftNav.tsx
+++ b/ui/app/src/components/workspaces/WorkspaceLeftNav.tsx
@@ -1,22 +1,24 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { Nav, INavLinkGroup, INavStyles } from '@fluentui/react/lib/Nav';
-import { useNavigate } from 'react-router-dom';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { WorkspaceService } from '../../models/workspaceService';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { SharedService } from '../../models/sharedService';
+import React, { useContext, useEffect, useState } from "react";
+import { Nav, INavLinkGroup, INavStyles } from "@fluentui/react/lib/Nav";
+import { useNavigate } from "react-router-dom";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { WorkspaceService } from "../../models/workspaceService";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { SharedService } from "../../models/sharedService";
// TODO:
// - active item is sometimes lost
interface WorkspaceLeftNavProps {
- workspaceServices: Array,
- sharedServices: Array,
- setWorkspaceService: (workspaceService: WorkspaceService) => void,
- addWorkspaceService: (w: WorkspaceService) => void
+ workspaceServices: Array;
+ sharedServices: Array;
+ setWorkspaceService: (workspaceService: WorkspaceService) => void;
+ addWorkspaceService: (w: WorkspaceService) => void;
}
-export const WorkspaceLeftNav: React.FunctionComponent = (props: WorkspaceLeftNavProps) => {
+export const WorkspaceLeftNav: React.FunctionComponent<
+ WorkspaceLeftNavProps
+> = (props: WorkspaceLeftNavProps) => {
const navigate = useNavigate();
const emptyLinks: INavLinkGroup[] = [{ links: [] }];
const [serviceLinks, setServiceLinks] = useState(emptyLinks);
@@ -25,64 +27,65 @@ export const WorkspaceLeftNav: React.FunctionComponent =
useEffect(() => {
const getWorkspaceServices = async () => {
// get the workspace services
- if(!workspaceCtx.workspace.id) return;
+ if (!workspaceCtx.workspace.id) return;
let serviceLinkArray: Array = [];
props.workspaceServices.forEach((service: WorkspaceService) => {
- serviceLinkArray.push(
- {
- name: service.properties.display_name,
- url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${service.id}`,
- key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${service.id}`
- });
+ serviceLinkArray.push({
+ name: service.properties.display_name,
+ url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${service.id}`,
+ key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${service.id}`,
+ });
});
let sharedServiceLinkArray: Array = [];
props.sharedServices.forEach((service: SharedService) => {
- sharedServiceLinkArray.push(
- {
- name: service.properties.display_name,
- url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.SharedServices}/${service.id}`,
- key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.SharedServices}/${service.id}`
- });
+ sharedServiceLinkArray.push({
+ name: service.properties.display_name,
+ url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.SharedServices}/${service.id}`,
+ key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.SharedServices}/${service.id}`,
+ });
});
const serviceNavLinks: INavLinkGroup[] = [
{
links: [
{
- name: 'Overview',
+ name: "Overview",
key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}`,
url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}`,
- isExpanded: true
+ isExpanded: true,
},
{
- name: 'Services',
+ name: "Services",
key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}`,
url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}`,
isExpanded: true,
- links: serviceLinkArray
+ links: serviceLinkArray,
},
{
- name: 'Shared Services',
+ name: "Shared Services",
key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.SharedServices}`,
url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.SharedServices}`,
isExpanded: false,
- links: sharedServiceLinkArray
+ links: sharedServiceLinkArray,
},
{
- name: 'Users',
+ name: "Users",
key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.Users}`,
url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.Users}`,
- isExpanded: false
- }
- ]
- }
+ isExpanded: false,
+ },
+ ],
+ },
];
// Only show airlock link if enabled for workspace
- if (workspaceCtx.workspace.properties !== undefined && workspaceCtx.workspace.properties.enable_airlock) {
+ if (
+ workspaceCtx.workspace.properties !== undefined &&
+ workspaceCtx.workspace.properties.enable_airlock
+ ) {
serviceNavLinks[0].links.push({
- name: 'Airlock',
+ name: "Airlock",
key: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.AirlockRequests}`,
url: `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.AirlockRequests}`,
});
@@ -91,7 +94,12 @@ export const WorkspaceLeftNav: React.FunctionComponent =
setServiceLinks(serviceNavLinks);
};
getWorkspaceServices();
- }, [props.workspaceServices, props.sharedServices, workspaceCtx.workspace.id, workspaceCtx.workspace.properties]);
+ }, [
+ props.workspaceServices,
+ props.sharedServices,
+ workspaceCtx.workspace.id,
+ workspaceCtx.workspace.properties,
+ ]);
return (
<>
@@ -99,11 +107,13 @@ export const WorkspaceLeftNav: React.FunctionComponent =
onLinkClick={(e, item) => {
e?.preventDefault();
if (!item || !item.url) return;
- let selectedService = props.workspaceServices.find((w) => item.key?.indexOf(w.id.toString()) !== -1);
+ let selectedService = props.workspaceServices.find(
+ (w) => item.key?.indexOf(w.id.toString()) !== -1,
+ );
if (selectedService) {
props.setWorkspaceService(selectedService);
}
- navigate(item.url)
+ navigate(item.url);
}}
ariaLabel="TRE Workspace Left Navigation"
groups={serviceLinks}
@@ -115,13 +125,13 @@ export const WorkspaceLeftNav: React.FunctionComponent =
const navStyles: Partial = {
root: {
- boxSizing: 'border-box',
- border: '1px solid #eee',
- paddingBottom: 40
+ boxSizing: "border-box",
+ border: "1px solid #eee",
+ paddingBottom: 40,
},
// these link styles override the default truncation behavior
link: {
- whiteSpace: 'normal',
- lineHeight: 'inherit',
+ whiteSpace: "normal",
+ lineHeight: "inherit",
},
};
diff --git a/ui/app/src/components/workspaces/WorkspaceProvider.tsx b/ui/app/src/components/workspaces/WorkspaceProvider.tsx
index 0fe590a1da..05a72077b0 100644
--- a/ui/app/src/components/workspaces/WorkspaceProvider.tsx
+++ b/ui/app/src/components/workspaces/WorkspaceProvider.tsx
@@ -1,32 +1,49 @@
-import { FontIcon, Spinner, SpinnerSize, Stack, getTheme, mergeStyles } from '@fluentui/react';
-import React, { useContext, useEffect, useRef, useState } from 'react';
-import { Route, Routes, useParams } from 'react-router-dom';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { WorkspaceService } from '../../models/workspaceService';
-import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { WorkspaceHeader } from './WorkspaceHeader';
-import { WorkspaceItem } from './WorkspaceItem';
-import { WorkspaceLeftNav } from './WorkspaceLeftNav';
-import { WorkspaceServiceItem } from './WorkspaceServiceItem';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { WorkspaceServices } from './WorkspaceServices';
-import { WorkspaceUsers } from './WorkspaceUsers';
-import { Workspace } from '../../models/workspace';
-import { SharedService } from '../../models/sharedService';
-import { SharedServices } from '../shared/SharedServices';
-import { SharedServiceItem } from '../shared/SharedServiceItem';
-import { Airlock } from '../shared/airlock/Airlock';
-import { APIError } from '../../models/exceptions';
-import { LoadingState } from '../../models/loadingState';
-import { ExceptionLayout } from '../shared/ExceptionLayout';
-import { AppRolesContext } from '../../contexts/AppRolesContext';
-import { RoleName, WorkspaceRoleName } from '../../models/roleNames';
+import {
+ FontIcon,
+ Spinner,
+ SpinnerSize,
+ Stack,
+ getTheme,
+ mergeStyles,
+} from "@fluentui/react";
+import React, { useContext, useEffect, useRef, useState } from "react";
+import { Route, Routes, useParams } from "react-router-dom";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { WorkspaceService } from "../../models/workspaceService";
+import {
+ HttpMethod,
+ ResultType,
+ useAuthApiCall,
+} from "../../hooks/useAuthApiCall";
+import { WorkspaceHeader } from "./WorkspaceHeader";
+import { WorkspaceItem } from "./WorkspaceItem";
+import { WorkspaceLeftNav } from "./WorkspaceLeftNav";
+import { WorkspaceServiceItem } from "./WorkspaceServiceItem";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { WorkspaceServices } from "./WorkspaceServices";
+import { WorkspaceUsers } from "./WorkspaceUsers";
+import { Workspace } from "../../models/workspace";
+import { SharedService } from "../../models/sharedService";
+import { SharedServices } from "../shared/SharedServices";
+import { SharedServiceItem } from "../shared/SharedServiceItem";
+import { Airlock } from "../shared/airlock/Airlock";
+import { APIError } from "../../models/exceptions";
+import { LoadingState } from "../../models/loadingState";
+import { ExceptionLayout } from "../shared/ExceptionLayout";
+import { AppRolesContext } from "../../contexts/AppRolesContext";
+import { RoleName, WorkspaceRoleName } from "../../models/roleNames";
export const WorkspaceProvider: React.FunctionComponent = () => {
const apiCall = useAuthApiCall();
- const [selectedWorkspaceService, setSelectedWorkspaceService] = useState({} as WorkspaceService);
- const [workspaceServices, setWorkspaceServices] = useState([] as Array);
- const [sharedServices, setSharedServices] = useState([] as Array);
+ const [selectedWorkspaceService, setSelectedWorkspaceService] = useState(
+ {} as WorkspaceService,
+ );
+ const [workspaceServices, setWorkspaceServices] = useState(
+ [] as Array,
+ );
+ const [sharedServices, setSharedServices] = useState(
+ [] as Array,
+ );
const workspaceCtx = useRef(useContext(WorkspaceContext));
const [wsRoles, setWSRoles] = useState([] as Array);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
@@ -39,11 +56,15 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
// set workspace context from url
useEffect(() => {
-
const getWorkspace = async () => {
try {
// get the workspace - first we get the scope_id so we can auth against the right aad app
- let scopeId = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}/scopeid`, HttpMethod.Get)).workspaceAuth.scopeId;
+ let scopeId = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceId}/scopeid`,
+ HttpMethod.Get,
+ )
+ ).workspaceAuth.scopeId;
const authProvisioned = scopeId !== "";
@@ -52,45 +73,69 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
if (authProvisioned) {
// use the client ID to get a token against the workspace (tokenOnly), and set the workspace roles in the context
- await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get, scopeId,
- undefined, ResultType.JSON, (roles: Array) => {
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceId}`,
+ HttpMethod.Get,
+ scopeId,
+ undefined,
+ ResultType.JSON,
+ (roles: Array) => {
wsRoles = roles;
- }, true);
+ },
+ true,
+ );
}
if (wsRoles && wsRoles.length > 0) {
- ws = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get, scopeId)).workspace;
+ ws = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceId}`,
+ HttpMethod.Get,
+ scopeId,
+ )
+ ).workspace;
workspaceCtx.current.setWorkspace(ws);
workspaceCtx.current.setRoles(wsRoles);
setWSRoles(wsRoles);
// get workspace services to pass to nav + ws services page
- const workspaceServices = await apiCall(`${ApiEndpoint.Workspaces}/${ws.id}/${ApiEndpoint.WorkspaceServices}`,
- HttpMethod.Get, ws.properties.scope_id);
+ const workspaceServices = await apiCall(
+ `${ApiEndpoint.Workspaces}/${ws.id}/${ApiEndpoint.WorkspaceServices}`,
+ HttpMethod.Get,
+ ws.properties.scope_id,
+ );
setWorkspaceServices(workspaceServices.workspaceServices);
// get shared services to pass to nav shared services pages
- const sharedServices = await apiCall(ApiEndpoint.SharedServices, HttpMethod.Get);
+ const sharedServices = await apiCall(
+ ApiEndpoint.SharedServices,
+ HttpMethod.Get,
+ );
setSharedServices(sharedServices.sharedServices);
setLoadingState(LoadingState.Ok);
} else if (appRoles.roles.includes(RoleName.TREAdmin)) {
- ws = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get)).workspace;
+ ws = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceId}`,
+ HttpMethod.Get,
+ )
+ ).workspace;
workspaceCtx.current.setWorkspace(ws);
setLoadingState(LoadingState.Ok);
setIsTREAdminUser(true);
} else {
let e = new APIError();
e.status = 403;
- e.userMessage = "User does not have a role assigned in the workspace or the TRE Admin role assigned";
+ e.userMessage =
+ "User does not have a role assigned in the workspace or the TRE Admin role assigned";
e.endpoint = `${ApiEndpoint.Workspaces}/${workspaceId}`;
throw e;
}
-
} catch (e: any) {
if (e.status === 401 || e.status === 403) {
setApiError(e);
setLoadingState(LoadingState.AccessDenied);
} else {
- e.userMessage = 'Error retrieving workspace';
+ e.userMessage = "Error retrieving workspace";
setApiError(e);
setLoadingState(LoadingState.Error);
}
@@ -111,41 +156,51 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
const getWorkspaceCosts = async () => {
try {
// TODO: amend when costs enabled in API for WorkspaceRoleName.Researcher
- if(wsRoles.includes(WorkspaceRoleName.WorkspaceOwner)){
- let scopeId = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}/scopeid`, HttpMethod.Get)).workspaceAuth.scopeId;
- const r = await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}/${ApiEndpoint.Costs}`, HttpMethod.Get, scopeId, undefined, ResultType.JSON);
+ if (wsRoles.includes(WorkspaceRoleName.WorkspaceOwner)) {
+ let scopeId = (
+ await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceId}/scopeid`,
+ HttpMethod.Get,
+ )
+ ).workspaceAuth.scopeId;
+ const r = await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceId}/${ApiEndpoint.Costs}`,
+ HttpMethod.Get,
+ scopeId,
+ undefined,
+ ResultType.JSON,
+ );
const costs = [
...r.costs,
...r.workspace_services,
- ...r.workspace_services.flatMap((ws: { user_resources: any; }) => [
- ...ws.user_resources
- ])
+ ...r.workspace_services.flatMap((ws: { user_resources: any }) => [
+ ...ws.user_resources,
+ ]),
];
workspaceCtx.current.setCosts(costs);
}
- }
- catch (e: any) {
+ } catch (e: any) {
if (e instanceof APIError) {
if (e.status === 404 /*subscription not supported*/) {
- }
- else if (e.status === 429 /*too many requests*/ || e.status === 503 /*service unavaiable*/) {
+ } else if (
+ e.status === 429 /*too many requests*/ ||
+ e.status === 503 /*service unavaiable*/
+ ) {
let msg = JSON.parse(e.message);
let retryAfter = Number(msg.error["retry-after"]);
setTimeout(getWorkspaceCosts, retryAfter * 1000);
+ } else {
+ e.userMessage = "Error retrieving costs";
}
- else {
- e.userMessage = 'Error retrieving costs';
- }
- }
- else {
- e.userMessage = 'Error retrieving costs';
+ } else {
+ e.userMessage = "Error retrieving costs";
}
setCostApiError(e);
}
};
getWorkspaceCosts();
- },[apiCall, workspaceId, wsRoles]);
+ }, [apiCall, workspaceId, wsRoles]);
const addWorkspaceService = (w: WorkspaceService) => {
let ws = [...workspaceServices];
@@ -171,74 +226,112 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
case LoadingState.Ok:
return (
<>
- {
- costApiError.message &&
-
- }
+ {costApiError.message && }
-
-
+
+
{!isTREAdminUser && (
setSelectedWorkspaceService(ws)}
- addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)} />
+ setWorkspaceService={(ws: WorkspaceService) =>
+ setSelectedWorkspaceService(ws)
+ }
+ addWorkspaceService={(ws: WorkspaceService) =>
+ addWorkspaceService(ws)
+ }
+ />
)}
-
+
-
-
- {!isTREAdminUser ? (
- setSelectedWorkspaceService(ws)}
- addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)}
- updateWorkspaceService={(ws: WorkspaceService) => updateWorkspaceService(ws)}
- removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} />
- ) : (
-
-
-
+
+ {!isTREAdminUser ? (
+
+ setSelectedWorkspaceService(ws)
+ }
+ addWorkspaceService={(ws: WorkspaceService) =>
+ addWorkspaceService(ws)
+ }
+ updateWorkspaceService={(ws: WorkspaceService) =>
+ updateWorkspaceService(ws)
+ }
+ removeWorkspaceService={(ws: WorkspaceService) =>
+ removeWorkspaceService(ws)
+ }
/>
- You are currently accessing this workspace using the TRE Admin role. Additional functionality requires a workspace role, such as Workspace Owner.
-
-
- )}
- >}
+ ) : (
+
+
+
+ You are currently accessing this workspace using
+ the TRE Admin role. Additional functionality
+ requires a workspace role, such as Workspace
+ Owner.
+
+
+ )}
+ >
+ }
/>
{!isTREAdminUser && (
<>
- setSelectedWorkspaceService(ws)}
- addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)}
- updateWorkspaceService={(ws: WorkspaceService) => updateWorkspaceService(ws)}
- removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)}
- />
- } />
- updateWorkspaceService(ws)}
- removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} />
- } />
+
+ setSelectedWorkspaceService(ws)
+ }
+ addWorkspaceService={(ws: WorkspaceService) =>
+ addWorkspaceService(ws)
+ }
+ updateWorkspaceService={(ws: WorkspaceService) =>
+ updateWorkspaceService(ws)
+ }
+ removeWorkspaceService={(ws: WorkspaceService) =>
+ removeWorkspaceService(ws)
+ }
+ />
+ }
+ />
+
+ updateWorkspaceService(ws)
+ }
+ removeWorkspaceService={(ws: WorkspaceService) =>
+ removeWorkspaceService(ws)
+ }
+ />
+ }
+ />
-
- } />
-
- } />
-
- } />
-
- } />
+ }
+ />
+ }
+ />
+ } />
+ } />
>
)}
@@ -250,13 +343,16 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
);
case LoadingState.Error:
case LoadingState.AccessDenied:
- return (
-
- );
+ return ;
default:
return (
-
-
+
+
);
}
@@ -266,5 +362,5 @@ const { palette } = getTheme();
const warningIcon = mergeStyles({
color: palette.orangeLight,
fontSize: 18,
- marginRight: 8
+ marginRight: 8,
});
diff --git a/ui/app/src/components/workspaces/WorkspaceServiceItem.tsx b/ui/app/src/components/workspaces/WorkspaceServiceItem.tsx
index fb5237e086..8ffbd356c3 100644
--- a/ui/app/src/components/workspaces/WorkspaceServiceItem.tsx
+++ b/ui/app/src/components/workspaces/WorkspaceServiceItem.tsx
@@ -1,39 +1,46 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { useAuthApiCall, HttpMethod } from '../../hooks/useAuthApiCall';
-import { UserResource } from '../../models/userResource';
-import { WorkspaceService } from '../../models/workspaceService';
-import { PrimaryButton, Spinner, SpinnerSize, Stack } from '@fluentui/react';
-import { ComponentAction, Resource } from '../../models/resource';
-import { ResourceCardList } from '../shared/ResourceCardList';
-import { LoadingState } from '../../models/loadingState';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { ResourceType } from '../../models/resourceType';
-import { ResourceHeader } from '../shared/ResourceHeader';
-import { useComponentManager } from '../../hooks/useComponentManager';
-import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
-import { successStates } from '../../models/operation';
-import { UserResourceItem } from './UserResourceItem';
-import { ResourceBody } from '../shared/ResourceBody';
-import { SecuredByRole } from '../shared/SecuredByRole';
-import { WorkspaceRoleName } from '../../models/roleNames';
-import { APIError } from '../../models/exceptions';
-import { ExceptionLayout } from '../shared/ExceptionLayout';
+import React, { useContext, useEffect, useState } from "react";
+import { Route, Routes, useNavigate, useParams } from "react-router-dom";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { useAuthApiCall, HttpMethod } from "../../hooks/useAuthApiCall";
+import { UserResource } from "../../models/userResource";
+import { WorkspaceService } from "../../models/workspaceService";
+import { PrimaryButton, Spinner, SpinnerSize, Stack } from "@fluentui/react";
+import { ComponentAction, Resource } from "../../models/resource";
+import { ResourceCardList } from "../shared/ResourceCardList";
+import { LoadingState } from "../../models/loadingState";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { ResourceType } from "../../models/resourceType";
+import { ResourceHeader } from "../shared/ResourceHeader";
+import { useComponentManager } from "../../hooks/useComponentManager";
+import { CreateUpdateResourceContext } from "../../contexts/CreateUpdateResourceContext";
+import { successStates } from "../../models/operation";
+import { UserResourceItem } from "./UserResourceItem";
+import { ResourceBody } from "../shared/ResourceBody";
+import { SecuredByRole } from "../shared/SecuredByRole";
+import { WorkspaceRoleName } from "../../models/roleNames";
+import { APIError } from "../../models/exceptions";
+import { ExceptionLayout } from "../shared/ExceptionLayout";
interface WorkspaceServiceItemProps {
- workspaceService?: WorkspaceService,
- updateWorkspaceService: (ws: WorkspaceService) => void,
- removeWorkspaceService: (ws: WorkspaceService) => void
+ workspaceService?: WorkspaceService;
+ updateWorkspaceService: (ws: WorkspaceService) => void;
+ removeWorkspaceService: (ws: WorkspaceService) => void;
}
-export const WorkspaceServiceItem: React.FunctionComponent
= (props: WorkspaceServiceItemProps) => {
+export const WorkspaceServiceItem: React.FunctionComponent<
+ WorkspaceServiceItemProps
+> = (props: WorkspaceServiceItemProps) => {
const { workspaceServiceId } = useParams();
- const [userResources, setUserResources] = useState([] as Array)
- const [workspaceService, setWorkspaceService] = useState({} as WorkspaceService)
+ const [userResources, setUserResources] = useState([] as Array);
+ const [workspaceService, setWorkspaceService] = useState(
+ {} as WorkspaceService,
+ );
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
- const [selectedUserResource, setSelectedUserResource] = useState({} as UserResource);
- const [hasUserResourceTemplates, setHasUserResourceTemplates] = useState(false);
+ const [selectedUserResource, setSelectedUserResource] = useState(
+ {} as UserResource,
+ );
+ const [hasUserResourceTemplates, setHasUserResourceTemplates] =
+ useState(false);
const workspaceCtx = useContext(WorkspaceContext);
const createFormCtx = useContext(CreateUpdateResourceContext);
const navigate = useNavigate();
@@ -42,32 +49,58 @@ export const WorkspaceServiceItem: React.FunctionComponent { props.updateWorkspaceService(r as WorkspaceService); setWorkspaceService(r as WorkspaceService) },
- (r: Resource) => { props.removeWorkspaceService(r as WorkspaceService); navigate(`/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}`) }
+ (r: Resource) => {
+ props.updateWorkspaceService(r as WorkspaceService);
+ setWorkspaceService(r as WorkspaceService);
+ },
+ (r: Resource) => {
+ props.removeWorkspaceService(r as WorkspaceService);
+ navigate(
+ `/${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}`,
+ );
+ },
);
useEffect(() => {
const getData = async () => {
- if(!workspaceCtx.workspace.id) return;
+ if (!workspaceCtx.workspace.id) return;
setHasUserResourceTemplates(false);
try {
- let svc = props.workspaceService || {} as WorkspaceService;
+ let svc = props.workspaceService || ({} as WorkspaceService);
// did we get passed the workspace service, or shall we get it from the api?
- if (props.workspaceService && props.workspaceService.id && props.workspaceService.id === workspaceServiceId) {
+ if (
+ props.workspaceService &&
+ props.workspaceService.id &&
+ props.workspaceService.id === workspaceServiceId
+ ) {
setWorkspaceService(props.workspaceService);
} else {
- let ws = await apiCall(`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}`, HttpMethod.Get, workspaceCtx.workspaceApplicationIdURI);
+ let ws = await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}`,
+ HttpMethod.Get,
+ workspaceCtx.workspaceApplicationIdURI,
+ );
setWorkspaceService(ws.workspaceService);
svc = ws.workspaceService;
}
// get the user resources
- const u = await apiCall(`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}/${ApiEndpoint.UserResources}`, HttpMethod.Get, workspaceCtx.workspaceApplicationIdURI)
+ const u = await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${workspaceServiceId}/${ApiEndpoint.UserResources}`,
+ HttpMethod.Get,
+ workspaceCtx.workspaceApplicationIdURI,
+ );
// get user resource templates - to check
- const ut = await apiCall(`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServiceTemplates}/${svc.templateName}/${ApiEndpoint.UserResourceTemplates}`, HttpMethod.Get, workspaceCtx.workspaceApplicationIdURI);
- setHasUserResourceTemplates(ut && ut.templates && ut.templates.length > 0);
+ const ut = await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServiceTemplates}/${svc.templateName}/${ApiEndpoint.UserResourceTemplates}`,
+ HttpMethod.Get,
+ workspaceCtx.workspaceApplicationIdURI,
+ );
+ setHasUserResourceTemplates(
+ ut && ut.templates && ut.templates.length > 0,
+ );
setUserResources(u.userResources);
setLoadingState(LoadingState.Ok);
} catch (err: any) {
@@ -77,94 +110,150 @@ export const WorkspaceServiceItem: React.FunctionComponent {
let ur = [...userResources];
ur.push(u);
setUserResources(ur);
- }
+ };
const updateUserResource = (u: UserResource) => {
let ur = [...userResources];
let i = ur.findIndex((f: UserResource) => f.id === u.id);
ur.splice(i, 1, u);
setUserResources(ur);
- }
+ };
const removeUserResource = (u: UserResource) => {
let ur = [...userResources];
let i = ur.findIndex((f: UserResource) => f.id === u.id);
ur.splice(i, 1);
setUserResources(ur);
- }
+ };
switch (loadingState) {
case LoadingState.Ok:
return (
<>
-
-
-
- {
- hasUserResourceTemplates &&
-
-
-
- Resources
- {
- createFormCtx.openCreateForm({
- resourceType: ResourceType.UserResource,
- resourceParent: workspaceService,
- onAdd: (r: Resource) => addUserResource(r as UserResource),
- workspaceApplicationIdURI: workspaceCtx.workspaceApplicationIdURI
- })
- }} />
- } />
-
-
-
- {
- userResources &&
- setSelectedUserResource(r as UserResource)}
- updateResource={(r: Resource) => updateUserResource(r as UserResource)}
- removeResource={(r: Resource) => removeUserResource(r as UserResource)}
- emptyText="This workspace service contains no user resources."
- isExposedExternally={workspaceService.properties.is_exposed_externally} />
- }
-
-
- }
- >
- } />
- updateUserResource(u)}
- removeUserResource={(u: UserResource) => removeUserResource(u)}
- />
- } />
+
+
+
+ {hasUserResourceTemplates && (
+
+
+
+ Resources
+ {
+ createFormCtx.openCreateForm({
+ resourceType: ResourceType.UserResource,
+ resourceParent: workspaceService,
+ onAdd: (r: Resource) =>
+ addUserResource(r as UserResource),
+ workspaceApplicationIdURI:
+ workspaceCtx.workspaceApplicationIdURI,
+ });
+ }}
+ />
+ }
+ />
+
+
+
+ {userResources && (
+
+ setSelectedUserResource(r as UserResource)
+ }
+ updateResource={(r: Resource) =>
+ updateUserResource(r as UserResource)
+ }
+ removeResource={(r: Resource) =>
+ removeUserResource(r as UserResource)
+ }
+ emptyText="This workspace service contains no user resources."
+ isExposedExternally={
+ workspaceService.properties.is_exposed_externally
+ }
+ />
+ )}
+
+
+ )}
+ >
+ }
+ />
+
+ updateUserResource(u)
+ }
+ removeUserResource={(u: UserResource) =>
+ removeUserResource(u)
+ }
+ />
+ }
+ />
-
>
);
case LoadingState.Error:
- return (
-
- );
+ return ;
default:
return (
-
-
+
+
- )
+ );
}
};
diff --git a/ui/app/src/components/workspaces/WorkspaceServices.tsx b/ui/app/src/components/workspaces/WorkspaceServices.tsx
index ac8df6922c..0829dfeb75 100644
--- a/ui/app/src/components/workspaces/WorkspaceServices.tsx
+++ b/ui/app/src/components/workspaces/WorkspaceServices.tsx
@@ -1,24 +1,26 @@
-import React, { useContext } from 'react';
-import { Resource } from '../../models/resource';
-import { WorkspaceService } from '../../models/workspaceService';
-import { ResourceCardList } from '../shared/ResourceCardList';
-import { PrimaryButton, Stack } from '@fluentui/react';
-import { ResourceType } from '../../models/resourceType';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
-import { successStates } from '../../models/operation';
-import { WorkspaceRoleName } from '../../models/roleNames';
-import { SecuredByRole } from '../shared/SecuredByRole';
+import React, { useContext } from "react";
+import { Resource } from "../../models/resource";
+import { WorkspaceService } from "../../models/workspaceService";
+import { ResourceCardList } from "../shared/ResourceCardList";
+import { PrimaryButton, Stack } from "@fluentui/react";
+import { ResourceType } from "../../models/resourceType";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { CreateUpdateResourceContext } from "../../contexts/CreateUpdateResourceContext";
+import { successStates } from "../../models/operation";
+import { WorkspaceRoleName } from "../../models/roleNames";
+import { SecuredByRole } from "../shared/SecuredByRole";
interface WorkspaceServicesProps {
- workspaceServices: Array
,
- setWorkspaceService: (workspaceService: WorkspaceService) => void,
- addWorkspaceService: (workspaceService: WorkspaceService) => void,
- updateWorkspaceService: (workspaceService: WorkspaceService) => void,
- removeWorkspaceService: (workspaceService: WorkspaceService) => void
+ workspaceServices: Array;
+ setWorkspaceService: (workspaceService: WorkspaceService) => void;
+ addWorkspaceService: (workspaceService: WorkspaceService) => void;
+ updateWorkspaceService: (workspaceService: WorkspaceService) => void;
+ removeWorkspaceService: (workspaceService: WorkspaceService) => void;
}
-export const WorkspaceServices: React.FunctionComponent = (props: WorkspaceServicesProps) => {
+export const WorkspaceServices: React.FunctionComponent<
+ WorkspaceServicesProps
+> = (props: WorkspaceServicesProps) => {
const workspaceCtx = useContext(WorkspaceContext);
const createFormCtx = useContext(CreateUpdateResourceContext);
@@ -28,25 +30,46 @@ export const WorkspaceServices: React.FunctionComponent
Workspace Services
- {
- createFormCtx.openCreateForm({
- resourceType: ResourceType.WorkspaceService,
- resourceParent: workspaceCtx.workspace,
- onAdd: (r: Resource) => props.addWorkspaceService(r as WorkspaceService),
- workspaceApplicationIdURI: workspaceCtx.workspaceApplicationIdURI
- });
- }} />
- } />
+ {
+ createFormCtx.openCreateForm({
+ resourceType: ResourceType.WorkspaceService,
+ resourceParent: workspaceCtx.workspace,
+ onAdd: (r: Resource) =>
+ props.addWorkspaceService(r as WorkspaceService),
+ workspaceApplicationIdURI:
+ workspaceCtx.workspaceApplicationIdURI,
+ });
+ }}
+ />
+ }
+ />
props.setWorkspaceService(r as WorkspaceService)}
- updateResource={(r: Resource) => props.updateWorkspaceService(r as WorkspaceService)}
- removeResource={(r: Resource) => props.removeWorkspaceService(r as WorkspaceService)}
- emptyText="This workspace has no workspace services." />
+ selectResource={(r: Resource) =>
+ props.setWorkspaceService(r as WorkspaceService)
+ }
+ updateResource={(r: Resource) =>
+ props.updateWorkspaceService(r as WorkspaceService)
+ }
+ removeResource={(r: Resource) =>
+ props.removeWorkspaceService(r as WorkspaceService)
+ }
+ emptyText="This workspace has no workspace services."
+ />
>
diff --git a/ui/app/src/components/workspaces/WorkspaceUsers.tsx b/ui/app/src/components/workspaces/WorkspaceUsers.tsx
index 9bdde2f745..17fdfff86c 100644
--- a/ui/app/src/components/workspaces/WorkspaceUsers.tsx
+++ b/ui/app/src/components/workspaces/WorkspaceUsers.tsx
@@ -1,17 +1,17 @@
-import * as React from 'react';
-import { useState, useCallback, useEffect, useMemo, useContext } from 'react';
-import { GroupedList, IGroup } from '@fluentui/react/lib/GroupedList';
-import { IColumn, DetailsRow } from '@fluentui/react/lib/DetailsList';
-import { SelectionMode } from '@fluentui/react/lib/Selection';
-import { Persona, PersonaSize } from '@fluentui/react/lib/Persona';
-import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
-import { APIError } from '../../models/exceptions';
-import { WorkspaceContext } from '../../contexts/WorkspaceContext';
-import { ApiEndpoint } from '../../models/apiEndpoints';
-import { LoadingState } from '../../models/loadingState';
-import { ExceptionLayout } from '../shared/ExceptionLayout';
-import { User } from '../../models/user';
-import { Stack } from '@fluentui/react';
+import * as React from "react";
+import { useState, useCallback, useEffect, useMemo, useContext } from "react";
+import { GroupedList, IGroup } from "@fluentui/react/lib/GroupedList";
+import { IColumn, DetailsRow } from "@fluentui/react/lib/DetailsList";
+import { SelectionMode } from "@fluentui/react/lib/Selection";
+import { Persona, PersonaSize } from "@fluentui/react/lib/Persona";
+import { HttpMethod, useAuthApiCall } from "../../hooks/useAuthApiCall";
+import { APIError } from "../../models/exceptions";
+import { WorkspaceContext } from "../../contexts/WorkspaceContext";
+import { ApiEndpoint } from "../../models/apiEndpoints";
+import { LoadingState } from "../../models/loadingState";
+import { ExceptionLayout } from "../shared/ExceptionLayout";
+import { User } from "../../models/user";
+import { Stack } from "@fluentui/react";
interface IUser {
id: string;
@@ -29,24 +29,37 @@ export const WorkspaceUsers: React.FunctionComponent = () => {
});
const apiCall = useAuthApiCall();
- const { workspace, roles, workspaceApplicationIdURI } = useContext(WorkspaceContext);
+ const { workspace, roles, workspaceApplicationIdURI } =
+ useContext(WorkspaceContext);
const getUsers = useCallback(async () => {
- setState(prevState => ({ ...prevState, apiError: undefined, loadingState: LoadingState.Loading }));
+ setState((prevState) => ({
+ ...prevState,
+ apiError: undefined,
+ loadingState: LoadingState.Loading,
+ }));
try {
const scopeId = roles.length > 0 ? workspaceApplicationIdURI : "";
- const result = await apiCall(`${ApiEndpoint.Workspaces}/${workspace.id}/${ApiEndpoint.Users}`, HttpMethod.Get, scopeId);
+ const result = await apiCall(
+ `${ApiEndpoint.Workspaces}/${workspace.id}/${ApiEndpoint.Users}`,
+ HttpMethod.Get,
+ scopeId,
+ );
- const users = result.users.flatMap((user: any) =>
- user.roles.map((role: string) => ({
- id: user.id,
- name: user.name,
- email: user.email,
- role: role,
- roles: user.roles
- }))
- ).sort((a: { role: string; }, b: { role: string; }) => a.role.localeCompare(b.role));
+ const users = result.users
+ .flatMap((user: any) =>
+ user.roles.map((role: string) => ({
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ role: role,
+ roles: user.roles,
+ })),
+ )
+ .sort((a: { role: string }, b: { role: string }) =>
+ a.role.localeCompare(b.role),
+ );
setState({ users, apiError: undefined, loadingState: LoadingState.Ok });
} catch (err: any) {
@@ -61,10 +74,10 @@ export const WorkspaceUsers: React.FunctionComponent = () => {
const groupedUsers = useMemo(() => {
const groups: { [key: string]: IUser[] } = {};
- state.users.forEach(user => {
+ state.users.forEach((user) => {
if (!groups[user.role]) {
groups[user.role] = [];
- }
+ }
groups[user.role].push(user);
});
return groups;
@@ -79,12 +92,11 @@ export const WorkspaceUsers: React.FunctionComponent = () => {
}));
}, [groupedUsers]);
-
const columns: IColumn[] = [
{
- key: 'name',
- name: 'Name',
- fieldName: 'name',
+ key: "name",
+ name: "Name",
+ fieldName: "name",
minWidth: 150,
onRender: (item: User) => (
{
imageAlt={item.name}
/>
),
- }
+ },
];
const onRenderCell = (
@@ -103,7 +115,7 @@ export const WorkspaceUsers: React.FunctionComponent = () => {
itemIndex?: number,
group?: IGroup,
): React.ReactNode => {
- return item && typeof itemIndex === 'number' && itemIndex > -1 ? (
+ return item && typeof itemIndex === "number" && itemIndex > -1 ? (
{
{state.apiError && }
-
+