Skip to content

Commit

Permalink
Update UI to better handle different roles (#3733)
Browse files Browse the repository at this point in the history
  • Loading branch information
marrobi authored Oct 10, 2023
1 parent e396652 commit 13ad442
Show file tree
Hide file tree
Showing 23 changed files with 283 additions and 142 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ FEATURES:
ENHANCEMENTS:
* Reduce logging noise ([#2135](https://github.com/microsoft/AzureTRE/issues/2135))
* Update workspace template to use Terraform's AzureRM 3.73 ([#3715](https://github.com/microsoft/AzureTRE/pull/3715))
* Enable cost tags for workspace services and user resources ([#2932](https://github.com/microsoft/AzureTRE/issues/2932))

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

## 0.14.1 (September 1, 2023)

Expand Down
2 changes: 1 addition & 1 deletion api_app/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.15.16"
__version__ = "0.15.17"
2 changes: 0 additions & 2 deletions api_app/db/repositories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ async def _get_container(cls, container_name, partition_key_obj) -> ContainerPro
try:
database = cls._client.get_database_client(config.STATE_STORE_DATABASE)
container = await database.create_container_if_not_exists(id=container_name, partition_key=partition_key_obj)
properties = await container.read()
print(properties['partitionKey'])
return container
except Exception:
raise UnableToAccessDatabase
Expand Down
2 changes: 1 addition & 1 deletion ui/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tre-ui",
"version": "0.5.8",
"version": "0.5.9",
"private": true,
"dependencies": {
"@azure/msal-browser": "^2.35.0",
Expand Down
3 changes: 3 additions & 0 deletions ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const App: React.FunctionComponent = () => {
const [appRoles, setAppRoles] = useState([] as Array<string>);
const [selectedWorkspace, setSelectedWorkspace] = useState({} as Workspace);
const [workspaceRoles, setWorkspaceRoles] = useState([] as Array<string>);
const [workspaceCosts, setWorkspaceCosts] = useState([] as Array<CostResource>);
const [costs, setCosts] = useState([] as Array<CostResource>);
const [costsLoadingState, setCostsLoadingState] = useState(LoadingState.Loading);
const [createFormOpen, setCreateFormOpen] = useState(false);
Expand Down Expand Up @@ -88,6 +89,8 @@ export const App: React.FunctionComponent = () => {
<WorkspaceContext.Provider value={{
roles: workspaceRoles,
setRoles: (roles: Array<string>) => {setWorkspaceRoles(roles)},
costs: workspaceCosts,
setCosts: (costs: Array<CostResource>) => {setWorkspaceCosts(costs)},
workspace: selectedWorkspace,
setWorkspace: (w: Workspace) => {setSelectedWorkspace(w)},
workspaceApplicationIdURI: selectedWorkspace.properties?.scope_id
Expand Down
2 changes: 1 addition & 1 deletion ui/app/src/components/root/RootDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const RootDashboard: React.FunctionComponent<RootDashboardProps> = (props
<Stack horizontal horizontalAlign="space-between">
<Stack.Item><h1>Workspaces</h1></Stack.Item>
<Stack.Item style={{ width: 200, textAlign: 'right' }}>
<SecuredByRole allowedRoles={[RoleName.TREAdmin]} workspaceAuth={false} element={
<SecuredByRole allowedAppRoles={[RoleName.TREAdmin]} element={
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.Workspace,
Expand Down
33 changes: 18 additions & 15 deletions ui/app/src/components/root/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,21 @@ export const RootLayout: React.FunctionComponent = () => {
useEffect(() => {
const getCosts = async () => {
try {
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
]);

costsWriteCtx.current.setLoadingState(LoadingState.Ok)
setLoadingCostState(LoadingState.Ok);
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.setCosts([
...r.workspaces,
...r.shared_services
]);

costsWriteCtx.current.setLoadingState(LoadingState.Ok)
setLoadingCostState(LoadingState.Ok);
} else {
costsWriteCtx.current.setLoadingState(LoadingState.AccessDenied)
setLoadingCostState(LoadingState.AccessDenied);
}
}
catch (e: any) {
if (e instanceof APIError) {
Expand Down Expand Up @@ -86,9 +91,7 @@ export const RootLayout: React.FunctionComponent = () => {
}
};

if (appRolesCtx.roles && appRolesCtx.roles.includes(RoleName.TREAdmin)) {
getCosts();
}
getCosts();

const ctx = costsWriteCtx.current;

Expand Down Expand Up @@ -139,8 +142,8 @@ export const RootLayout: React.FunctionComponent = () => {
<Route path="/admin" element={<Admin />} />
<Route path="/shared-services/*" element={
<Routes>
<Route path="/" element={<SecuredByRole element={<SharedServices />} allowedRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
<Route path=":sharedServiceId" element={<SecuredByRole element={<SharedServiceItem />} allowedRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
<Route path="/" element={<SecuredByRole element={<SharedServices />} allowedAppRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
<Route path=":sharedServiceId" element={<SecuredByRole element={<SharedServiceItem />} allowedAppRoles={[RoleName.TREAdmin]} errorString={"You must be a TRE Admin to access this area"}/>} />
</Routes>
} />
</Routes>
Expand Down
73 changes: 51 additions & 22 deletions ui/app/src/components/shared/CostsTag.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,72 @@
import { Stack, Shimmer, TooltipHost, Icon } from "@fluentui/react";
import { useContext } from "react";
import { useContext, useEffect, useState } from "react";
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 { ApiEndpoint } from "../../models/apiEndpoints";

interface CostsTagProps {
resourceId: string
resourceId: string;
}

export const CostsTag: React.FunctionComponent<CostsTagProps> = (props: CostsTagProps) => {
const costsCtx = useContext(CostsContext);
const resourceCosts = costsCtx?.costs?.find((resourceCost) => {
return resourceCost.id === props.resourceId;
});
let costBadge = <></>;
switch(costsCtx.loadingState) {
case LoadingState.Loading:
costBadge = <Stack.Item style={{maxHeight: 18, width: 30}} className="tre-badge"><Shimmer/></Stack.Item>
break;
case LoadingState.Ok:
const workspaceCtx = useContext(WorkspaceContext);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const apiCall = useAuthApiCall();
const [formattedCost, setFormattedCost] = useState<string | undefined>(undefined);

useEffect(() => {
async function fetchCostData() {
let costs: CostResource[] = [];
if (workspaceCtx.costs.length > 0) {
costs = workspaceCtx.costs;
} else if (costsCtx.costs.length > 0) {
costs = costsCtx.costs;
} else {
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) => {
return cost.id === props.resourceId;
});

if (resourceCosts && resourceCosts.costs.length > 0) {
const formattedCost = new Intl.NumberFormat(undefined, {
style: 'currency',
currency: resourceCosts?.costs[0].currency,
currencyDisplay: 'narrowSymbol',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(resourceCosts?.costs[0].cost);
costBadge = <Stack.Item style={{maxHeight: 18}} className="tre-badge">{formattedCost}</Stack.Item>
} else {
costBadge = (
<Stack.Item style={{maxHeight: 18}} className="tre-badge">
}).format(resourceCosts.costs[0].cost);
setFormattedCost(formattedCost);
setLoadingState(LoadingState.Ok);
}
}
fetchCostData();
}, [apiCall, costsCtx.loadingState, props.resourceId, workspaceCtx.costs, costsCtx.costs]);

const costBadge = (
<Stack.Item style={{ maxHeight: 18 }} className="tre-badge">
{loadingState === LoadingState.Loading ? (
<Shimmer />
) : (
<>
{formattedCost ? (
formattedCost
) : (
<TooltipHost content="Cost data not yet available">
<Icon iconName="Clock" />
</TooltipHost>
</Stack.Item>
);
}
break;
}
)}
</>
)}
</Stack.Item>
);

return (costBadge);
}
};
46 changes: 37 additions & 9 deletions ui/app/src/components/shared/ResourceBody.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import React, { } from 'react';
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 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,
readonly?: boolean
readonly?: boolean;
}

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

const workspaceCtx = useContext(WorkspaceContext);

const operationsRolesByResourceType = {
[ResourceType.Workspace]: [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner],
[ResourceType.SharedService]: [RoleName.TREAdmin],
[ResourceType.WorkspaceService]: [WorkspaceRoleName.WorkspaceOwner],
[ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]
};

const historyRolesByResourceType = {
[ResourceType.Workspace]: [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner],
[ResourceType.SharedService]: [RoleName.TREAdmin],
[ResourceType.WorkspaceService]: [WorkspaceRoleName.WorkspaceOwner],
[ResourceType.UserResource]: [WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]
};

const operationsRoles = operationsRolesByResourceType[props.resource.resourceType];
const historyRoles = historyRolesByResourceType[props.resource.resourceType];
const workspaceId = workspaceCtx.workspace?.id || "";

return (
<Pivot aria-label="Resource Menu" className='tre-resource-panel'>
<PivotItem
Expand All @@ -38,15 +62,19 @@ export const ResourceBody: React.FunctionComponent<ResourceBodyProps> = (props:
}
{
!props.readonly &&
<PivotItem headerText="History">
<ResourceHistoryList resource={props.resource} />
</PivotItem>
<SecuredByRole allowedAppRoles={historyRoles} allowedWorkspaceRoles={historyRoles} workspaceId={workspaceId} element={
<PivotItem headerText="History">
<ResourceHistoryList resource={props.resource} />
</PivotItem>
} />
}
{
!props.readonly &&
<PivotItem headerText="Operations">
<ResourceOperationsList resource={props.resource} />
</PivotItem>
<SecuredByRole allowedAppRoles={operationsRoles} allowedWorkspaceRoles={operationsRoles} workspaceId={workspaceId} element={
<PivotItem headerText="Operations">
<ResourceOperationsList resource={props.resource} />
</PivotItem>
} />
}
</Pivot>
);
Expand Down
Loading

0 comments on commit 13ad442

Please sign in to comment.