Skip to content

Commit 13ad442

Browse files
authored
Update UI to better handle different roles (#3733)
1 parent e396652 commit 13ad442

23 files changed

+283
-142
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ FEATURES:
66
ENHANCEMENTS:
77
* Reduce logging noise ([#2135](https://github.com/microsoft/AzureTRE/issues/2135))
88
* Update workspace template to use Terraform's AzureRM 3.73 ([#3715](https://github.com/microsoft/AzureTRE/pull/3715))
9+
* Enable cost tags for workspace services and user resources ([#2932](https://github.com/microsoft/AzureTRE/issues/2932))
910

1011
BUG FIXES:
1112
* Upgrade unresticted and airlock base template versions due to diagnostic settings retention period being depreciated ([#3704](https://github.com/microsoft/AzureTRE/pull/3704))
1213
* Enable TRE Admins to view workspace details when don't have a workspace role ([#2363](https://github.com/microsoft/AzureTRE/issues/2363))
1314
* Fix shared services list return restricted resource for admins causing issues with updates ([#3716](https://github.com/microsoft/AzureTRE/issues/3716))
1415
* Fix grey box appearing on resource card when costs are not available. ([#3254](https://github.com/microsoft/AzureTRE/issues/3254))
1516
* Fix notification panel not passing the workspace scope id to the API hence UI not updating ([#3353](https://github.com/microsoft/AzureTRE/issues/3353))
17+
* Fix issue with cost tags not displaying correctly for some user roles ([#3721](https://github.com/microsoft/AzureTRE/issues/3721))
1618

1719
## 0.14.1 (September 1, 2023)
1820

api_app/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.15.16"
1+
__version__ = "0.15.17"

api_app/db/repositories/base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ async def _get_container(cls, container_name, partition_key_obj) -> ContainerPro
2525
try:
2626
database = cls._client.get_database_client(config.STATE_STORE_DATABASE)
2727
container = await database.create_container_if_not_exists(id=container_name, partition_key=partition_key_obj)
28-
properties = await container.read()
29-
print(properties['partitionKey'])
3028
return container
3129
except Exception:
3230
raise UnableToAccessDatabase

ui/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tre-ui",
3-
"version": "0.5.8",
3+
"version": "0.5.9",
44
"private": true,
55
"dependencies": {
66
"@azure/msal-browser": "^2.35.0",

ui/app/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const App: React.FunctionComponent = () => {
2626
const [appRoles, setAppRoles] = useState([] as Array<string>);
2727
const [selectedWorkspace, setSelectedWorkspace] = useState({} as Workspace);
2828
const [workspaceRoles, setWorkspaceRoles] = useState([] as Array<string>);
29+
const [workspaceCosts, setWorkspaceCosts] = useState([] as Array<CostResource>);
2930
const [costs, setCosts] = useState([] as Array<CostResource>);
3031
const [costsLoadingState, setCostsLoadingState] = useState(LoadingState.Loading);
3132
const [createFormOpen, setCreateFormOpen] = useState(false);
@@ -88,6 +89,8 @@ export const App: React.FunctionComponent = () => {
8889
<WorkspaceContext.Provider value={{
8990
roles: workspaceRoles,
9091
setRoles: (roles: Array<string>) => {setWorkspaceRoles(roles)},
92+
costs: workspaceCosts,
93+
setCosts: (costs: Array<CostResource>) => {setWorkspaceCosts(costs)},
9194
workspace: selectedWorkspace,
9295
setWorkspace: (w: Workspace) => {setSelectedWorkspace(w)},
9396
workspaceApplicationIdURI: selectedWorkspace.properties?.scope_id

ui/app/src/components/root/RootDashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const RootDashboard: React.FunctionComponent<RootDashboardProps> = (props
2727
<Stack horizontal horizontalAlign="space-between">
2828
<Stack.Item><h1>Workspaces</h1></Stack.Item>
2929
<Stack.Item style={{ width: 200, textAlign: 'right' }}>
30-
<SecuredByRole allowedRoles={[RoleName.TREAdmin]} workspaceAuth={false} element={
30+
<SecuredByRole allowedAppRoles={[RoleName.TREAdmin]} element={
3131
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" onClick={() => {
3232
createFormCtx.openCreateForm({
3333
resourceType: ResourceType.Workspace,

ui/app/src/components/root/RootLayout.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,21 @@ export const RootLayout: React.FunctionComponent = () => {
4848
useEffect(() => {
4949
const getCosts = async () => {
5050
try {
51-
costsWriteCtx.current.setLoadingState(LoadingState.Loading)
52-
const r = await apiCall(ApiEndpoint.Costs, HttpMethod.Get, undefined, undefined, ResultType.JSON);
53-
54-
costsWriteCtx.current.setCosts([
55-
...r.workspaces,
56-
...r.shared_services
57-
]);
58-
59-
costsWriteCtx.current.setLoadingState(LoadingState.Ok)
60-
setLoadingCostState(LoadingState.Ok);
51+
if (appRolesCtx.roles.includes(RoleName.TREAdmin)) {
52+
costsWriteCtx.current.setLoadingState(LoadingState.Loading)
53+
const r = await apiCall(ApiEndpoint.Costs, HttpMethod.Get, undefined, undefined, ResultType.JSON);
54+
55+
costsWriteCtx.current.setCosts([
56+
...r.workspaces,
57+
...r.shared_services
58+
]);
59+
60+
costsWriteCtx.current.setLoadingState(LoadingState.Ok)
61+
setLoadingCostState(LoadingState.Ok);
62+
} else {
63+
costsWriteCtx.current.setLoadingState(LoadingState.AccessDenied)
64+
setLoadingCostState(LoadingState.AccessDenied);
65+
}
6166
}
6267
catch (e: any) {
6368
if (e instanceof APIError) {
@@ -86,9 +91,7 @@ export const RootLayout: React.FunctionComponent = () => {
8691
}
8792
};
8893

89-
if (appRolesCtx.roles && appRolesCtx.roles.includes(RoleName.TREAdmin)) {
90-
getCosts();
91-
}
94+
getCosts();
9295

9396
const ctx = costsWriteCtx.current;
9497

@@ -139,8 +142,8 @@ export const RootLayout: React.FunctionComponent = () => {
139142
<Route path="/admin" element={<Admin />} />
140143
<Route path="/shared-services/*" element={
141144
<Routes>
142-
<Route path="/" element={<SecuredByRole element={<SharedServices />} allowedRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
143-
<Route path=":sharedServiceId" element={<SecuredByRole element={<SharedServiceItem />} allowedRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
145+
<Route path="/" element={<SecuredByRole element={<SharedServices />} allowedAppRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
146+
<Route path=":sharedServiceId" element={<SecuredByRole element={<SharedServiceItem />} allowedAppRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
144147
</Routes>
145148
} />
146149
</Routes>
Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,72 @@
11
import { Stack, Shimmer, TooltipHost, Icon } from "@fluentui/react";
2-
import { useContext } from "react";
2+
import { useContext, useEffect, useState } from "react";
33
import { CostsContext } from "../../contexts/CostsContext";
44
import { LoadingState } from "../../models/loadingState";
5+
import { WorkspaceContext } from "../../contexts/WorkspaceContext";
6+
import { CostResource } from "../../models/costs";
7+
import { useAuthApiCall, HttpMethod, ResultType } from '../../hooks/useAuthApiCall';
8+
import { ApiEndpoint } from "../../models/apiEndpoints";
59

610
interface CostsTagProps {
7-
resourceId: string
11+
resourceId: string;
812
}
913

1014
export const CostsTag: React.FunctionComponent<CostsTagProps> = (props: CostsTagProps) => {
1115
const costsCtx = useContext(CostsContext);
12-
const resourceCosts = costsCtx?.costs?.find((resourceCost) => {
13-
return resourceCost.id === props.resourceId;
14-
});
15-
let costBadge = <></>;
16-
switch(costsCtx.loadingState) {
17-
case LoadingState.Loading:
18-
costBadge = <Stack.Item style={{maxHeight: 18, width: 30}} className="tre-badge"><Shimmer/></Stack.Item>
19-
break;
20-
case LoadingState.Ok:
16+
const workspaceCtx = useContext(WorkspaceContext);
17+
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
18+
const apiCall = useAuthApiCall();
19+
const [formattedCost, setFormattedCost] = useState<string | undefined>(undefined);
20+
21+
useEffect(() => {
22+
async function fetchCostData() {
23+
let costs: CostResource[] = [];
24+
if (workspaceCtx.costs.length > 0) {
25+
costs = workspaceCtx.costs;
26+
} else if (costsCtx.costs.length > 0) {
27+
costs = costsCtx.costs;
28+
} else {
29+
let scopeId = (await apiCall(`${ApiEndpoint.Workspaces}/${props.resourceId}/scopeid`, HttpMethod.Get)).workspaceAuth.scopeId;
30+
const r = await apiCall(`${ApiEndpoint.Workspaces}/${props.resourceId}/${ApiEndpoint.Costs}`, HttpMethod.Get, scopeId, undefined, ResultType.JSON);
31+
costs = [{costs: r.costs, id: r.id, name: r.name }];
32+
}
33+
34+
const resourceCosts = costs.find((cost) => {
35+
return cost.id === props.resourceId;
36+
});
37+
2138
if (resourceCosts && resourceCosts.costs.length > 0) {
2239
const formattedCost = new Intl.NumberFormat(undefined, {
2340
style: 'currency',
2441
currency: resourceCosts?.costs[0].currency,
2542
currencyDisplay: 'narrowSymbol',
2643
minimumFractionDigits: 2,
2744
maximumFractionDigits: 2
28-
}).format(resourceCosts?.costs[0].cost);
29-
costBadge = <Stack.Item style={{maxHeight: 18}} className="tre-badge">{formattedCost}</Stack.Item>
30-
} else {
31-
costBadge = (
32-
<Stack.Item style={{maxHeight: 18}} className="tre-badge">
45+
}).format(resourceCosts.costs[0].cost);
46+
setFormattedCost(formattedCost);
47+
setLoadingState(LoadingState.Ok);
48+
}
49+
}
50+
fetchCostData();
51+
}, [apiCall, costsCtx.loadingState, props.resourceId, workspaceCtx.costs, costsCtx.costs]);
52+
53+
const costBadge = (
54+
<Stack.Item style={{ maxHeight: 18 }} className="tre-badge">
55+
{loadingState === LoadingState.Loading ? (
56+
<Shimmer />
57+
) : (
58+
<>
59+
{formattedCost ? (
60+
formattedCost
61+
) : (
3362
<TooltipHost content="Cost data not yet available">
3463
<Icon iconName="Clock" />
3564
</TooltipHost>
36-
</Stack.Item>
37-
);
38-
}
39-
break;
40-
}
65+
)}
66+
</>
67+
)}
68+
</Stack.Item>
69+
);
4170

4271
return (costBadge);
43-
}
72+
};

ui/app/src/components/shared/ResourceBody.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
1-
import React, { } from 'react';
1+
import React, { useContext } from 'react';
22
import { ResourceDebug } from '../shared/ResourceDebug';
33
import { Pivot, PivotItem } from '@fluentui/react';
44
import { ResourcePropertyPanel } from '../shared/ResourcePropertyPanel';
55
import { Resource } from '../../models/resource';
66
import { ResourceHistoryList } from '../shared/ResourceHistoryList';
77
import { ResourceOperationsList } from '../shared/ResourceOperationsList';
88
import ReactMarkdown from 'react-markdown';
9-
import remarkGfm from 'remark-gfm'
9+
import remarkGfm from 'remark-gfm';
10+
import { RoleName, WorkspaceRoleName } from '../../models/roleNames';
11+
import { ResourceType } from '../../models/resourceType';
12+
import { SecuredByRole } from './SecuredByRole';
13+
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
1014

1115
interface ResourceBodyProps {
1216
resource: Resource,
13-
readonly?: boolean
17+
readonly?: boolean;
1418
}
1519

1620
export const ResourceBody: React.FunctionComponent<ResourceBodyProps> = (props: ResourceBodyProps) => {
1721

22+
const workspaceCtx = useContext(WorkspaceContext);
23+
24+
const operationsRolesByResourceType = {
25+
[ResourceType.Workspace]: [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner],
26+
[ResourceType.SharedService]: [RoleName.TREAdmin],
27+
[ResourceType.WorkspaceService]: [WorkspaceRoleName.WorkspaceOwner],
28+
[ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]
29+
};
30+
31+
const historyRolesByResourceType = {
32+
[ResourceType.Workspace]: [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner],
33+
[ResourceType.SharedService]: [RoleName.TREAdmin],
34+
[ResourceType.WorkspaceService]: [WorkspaceRoleName.WorkspaceOwner],
35+
[ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]
36+
};
37+
38+
const operationsRoles = operationsRolesByResourceType[props.resource.resourceType];
39+
const historyRoles = historyRolesByResourceType[props.resource.resourceType];
40+
const workspaceId = workspaceCtx.workspace?.id || "";
41+
1842
return (
1943
<Pivot aria-label="Resource Menu" className='tre-resource-panel'>
2044
<PivotItem
@@ -38,15 +62,19 @@ export const ResourceBody: React.FunctionComponent<ResourceBodyProps> = (props:
3862
}
3963
{
4064
!props.readonly &&
41-
<PivotItem headerText="History">
42-
<ResourceHistoryList resource={props.resource} />
43-
</PivotItem>
65+
<SecuredByRole allowedAppRoles={historyRoles} allowedWorkspaceRoles={historyRoles} workspaceId={workspaceId} element={
66+
<PivotItem headerText="History">
67+
<ResourceHistoryList resource={props.resource} />
68+
</PivotItem>
69+
} />
4470
}
4571
{
4672
!props.readonly &&
47-
<PivotItem headerText="Operations">
48-
<ResourceOperationsList resource={props.resource} />
49-
</PivotItem>
73+
<SecuredByRole allowedAppRoles={operationsRoles} allowedWorkspaceRoles={operationsRoles} workspaceId={workspaceId} element={
74+
<PivotItem headerText="Operations">
75+
<ResourceOperationsList resource={props.resource} />
76+
</PivotItem>
77+
} />
5078
}
5179
</Pivot>
5280
);

0 commit comments

Comments
 (0)