Skip to content

Commit

Permalink
Airlock UI: view requests (workspace-level) (#2512)
Browse files Browse the repository at this point in the history
* Basic layout

* Added sorting

* Added pre-sorting

* Added routing and dialogs

* Visual improvements

* Added display for no requests

* Fix for new api return type

* Added create time

* Added creation time to view request pane

* removed unused import

* PR snags

* Changelog

Co-authored-by: jjgriff93 <jamesgr@microsoft.com>
Co-authored-by: David Moore <35696285+damoodamoo@users.noreply.github.com>
Co-authored-by: David Moore <damoo@microsoft.com>
  • Loading branch information
4 people authored Sep 1, 2022
1 parent 60bc98c commit 1473d8d
Show file tree
Hide file tree
Showing 13 changed files with 622 additions and 26 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ ENHANCEMENTS:
* Gitea shared service support app-service standard SKUs ([#2523](https://github.com/microsoft/AzureTRE/pull/2523))
* Keyvault diagnostic settings in base workspace ([#2521](https://github.com/microsoft/AzureTRE/pull/2521))
* Airlock requests contain a field with information about the files that were submitted ([#2504](https://github.com/microsoft/AzureTRE/pull/2504))
* UI - Operations and notifications stability improvements ([[#2530](https://github.com/microsoft/AzureTRE/pull/2530)])
* UI - Operations and notifications stability improvements ([[#2530](https://github.com/microsoft/AzureTRE/pull/2530))
* UI - Initial implemetation of Workspace Airlock Request View ([#2512](https://github.com/microsoft/AzureTRE/pull/2512))

BUG FIXES:

Expand Down
46 changes: 35 additions & 11 deletions ui/app/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,28 @@ code {
width: 70%;
}

#root {}

.tre-root {}

.tre-top-nav {
box-shadow: 0 1px 2px 0px #033d68;
z-index: 100;
}

.ms-CommandBar {
background-color: transparent;
padding-left: 0px;

.ms-Button {
background-color: transparent;
}
}

.tre-notifications-button {
position: relative;
top: 7px;
color: #fff;

i {
font-size: 20px !important;
}
}

.tre-notifications-button i {
Expand Down Expand Up @@ -81,12 +90,20 @@ ul.tre-notifications-steps-list li {
font-size:1.2rem;
}

.tre-user-menu .ms-Persona-primaryText:hover {
color: #fff;
}
.tre-user-menu {
margin-top: 2px;

.ms-Persona-primaryText {
color: #fff;
.ms-Persona-primaryText:hover {
color: #fff;
}

.ms-Persona-primaryText {
color: #fff;
}

.ms-Icon {
margin-top: 3px;
}
}

.tre-hide-chevron i[data-icon-name=ChevronDown] {
Expand Down Expand Up @@ -130,14 +147,21 @@ ul.tre-notifications-steps-list li {
}

.tre-panel {
margin: 10px 15px 10px 10px;
padding: 10px;
}

.tre-resource-panel {
box-shadow: 1px 0px 5px 0px #ccc;
margin: 10px 15px 10px 10px;
padding: 10px;
background-color: #fff;
}

.ms-CommandBar {
padding-left: 0;
.tre-table-rows-align-centre {
.ms-DetailsRow-cell {
align-self: baseline;
}
}

.ms-Pivot {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ 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 { Footer } from './components/shared/Footer';
import { Routes, Route } from 'react-router-dom';
import { RootLayout } from './components/root/RootLayout';
import { WorkspaceProvider } from './components/workspaces/WorkspaceProvider';
Expand All @@ -19,6 +18,7 @@ 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';

export const App: React.FunctionComponent = () => {
const [appRoles, setAppRoles] = useState([] as Array<string>);
Expand Down
4 changes: 2 additions & 2 deletions ui/app/src/components/shared/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AnimationClassNames, getTheme, mergeStyles } from '@fluentui/react';
export const Footer: React.FunctionComponent = () => {
return (
<div className={contentClass}>
Azure TRE
Azure Trusted Research Environment
</div>
);
};
Expand All @@ -22,4 +22,4 @@ const contentClass = mergeStyles([
padding: '0 20px',
},
AnimationClassNames.scaleUpIn100,
]);
]);
2 changes: 1 addition & 1 deletion ui/app/src/components/shared/ResourceBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface ResourceBodyProps {
export const ResourceBody: React.FunctionComponent<ResourceBodyProps> = (props: ResourceBodyProps) => {

return (
<Pivot aria-label="Resource Menu" className='tre-panel'>
<Pivot aria-label="Resource Menu" className='tre-resource-panel'>
<PivotItem
headerText="Overview"
headerButtonProps={{
Expand Down
7 changes: 5 additions & 2 deletions ui/app/src/components/shared/TopNav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { getTheme, mergeStyles, Stack } from '@fluentui/react';
import { getTheme, Icon, mergeStyles, Stack } from '@fluentui/react';
import { Link } from 'react-router-dom';
import { UserMenu } from './UserMenu';
import { NotificationPanel } from './notifications/NotificationPanel';
Expand All @@ -10,7 +10,10 @@ export const TopNav: React.FunctionComponent = () => {
<div className={contentClass}>
<Stack horizontal>
<Stack.Item grow={100}>
<Link to='/' className='tre-home-link'>Azure Trusted Research Environment</Link>
<Link to='/' className='tre-home-link'>
<Icon iconName="TestBeakerSolid" style={{ marginLeft: '10px', marginRight: '10px', verticalAlign: 'middle' }} />
<h5 style={{display: 'inline'}}>Azure Trusted Research Environment</h5>
</Link>
</Stack.Item>
<Stack.Item>
<NotificationPanel />
Expand Down
229 changes: 229 additions & 0 deletions ui/app/src/components/shared/airlock/Airlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import React, { useContext, useEffect, useState } from 'react';
import { CommandBarButton, DetailsList, getTheme, IColumn, MessageBar, MessageBarType, Persona, PersonaSize, SelectionMode, Spinner, SpinnerSize, Stack } from '@fluentui/react';
import { HttpMethod, useAuthApiCall } from '../../../hooks/useAuthApiCall';
import { ApiEndpoint } from '../../../models/apiEndpoints';
import { WorkspaceContext } from '../../../contexts/WorkspaceContext';
import { AirlockRequest } from '../../../models/airlock';
import moment from 'moment';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { AirlockViewRequest } from './AirlockViewRequest';
import { LoadingState } from '../../../models/loadingState';

interface AirlockProps {
}

export const Airlock: React.FunctionComponent<AirlockProps> = (props: AirlockProps) => {
const [airlockRequests, setAirlockRequests] = useState([] as AirlockRequest[]);
const [requestColumns, setRequestColumns] = useState([] as IColumn[]);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const workspaceCtx = useContext(WorkspaceContext);
const apiCall = useAuthApiCall();
const theme = getTheme();
const navigate = useNavigate();

useEffect(() => {
const getAirlockRequests = async () => {
let requests: AirlockRequest[];

try {
if (workspaceCtx.workspace) {
const result = await apiCall(
`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.AirlockRequests}`,
HttpMethod.Get,
workspaceCtx.workspaceApplicationIdURI
);
requests = result.airlockRequests.map((r: { airlockRequest: AirlockRequest }) => r.airlockRequest);
} else {
// TODO: Get all requests across workspaces
requests = [];
}
// Order by updatedWhen for initial view
requests.sort((a, b) => a.updatedWhen < b.updatedWhen ? 1 : -1);
setAirlockRequests(requests);
setLoadingState(LoadingState.Ok);
} catch (error) {
setLoadingState(LoadingState.Error);
}
}
getAirlockRequests();
}, [apiCall, workspaceCtx.workspace, workspaceCtx.workspaceApplicationIdURI]);

useEffect(() => {
const reorderColumn = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
// Reset sorting on other columns and invert selected column if already sorted asc/desc
setRequestColumns(columns => {
const orderedColumns: IColumn[] = columns.slice();
const selectedColumn: IColumn = orderedColumns.filter(selCol => column.key === selCol.key)[0];
orderedColumns.forEach((newCol: IColumn) => {
if (newCol === selectedColumn) {
selectedColumn.isSortedDescending = !selectedColumn.isSortedDescending;
selectedColumn.isSorted = true;
} else {
newCol.isSorted = false;
newCol.isSortedDescending = true;
}
});
return orderedColumns;
});

// Re-order airlock requests
setAirlockRequests(requests => {
const key = column.fieldName! as keyof AirlockRequest;
return requests
.slice(0)
.sort((a: AirlockRequest, b: AirlockRequest) => (
(column.isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1)
);
})
};

const columns: IColumn[] = [
{
key: 'avatar',
name: '',
minWidth: 16,
maxWidth: 16,
isIconOnly: true,
onRender: (request: AirlockRequest) => {
return <Persona size={ PersonaSize.size24 } text={ request.user?.name } />
}
},
{
key: 'initiator',
name: 'Initiator',
ariaLabel: 'Creator of the airlock request',
minWidth: 150,
maxWidth: 200,
isResizable: true,
onRender: (request: AirlockRequest) => request.user?.name,
onColumnClick: reorderColumn
},
{
key: 'type',
name: 'Type',
ariaLabel: 'Whether the request is import or export',
minWidth: 70,
maxWidth: 100,
isResizable: true,
fieldName: 'requestType',
onColumnClick: reorderColumn
},
{
key: 'status',
name: 'Status',
ariaLabel: 'Status of the request',
minWidth: 70,
isResizable: true,
fieldName: 'status',
onColumnClick: reorderColumn
},
{
key: 'created',
name: 'Created',
ariaLabel: 'When the request was created',
minWidth: 120,
data: 'number',
isResizable: true,
fieldName: 'createdTime',
onRender: (request: AirlockRequest) => {
return <span>{ moment.unix(request.creationTime).format('DD/MM/YYYY') }</span>;
},
onColumnClick: reorderColumn
},
{
key: 'updated',
name: 'Updated',
ariaLabel: 'When the request was last updated',
minWidth: 120,
data: 'number',
isResizable: true,
isSorted: true,
fieldName: 'updatedWhen',
onRender: (request: AirlockRequest) => {
return <span>{ moment.unix(request.updatedWhen).fromNow() }</span>;
},
onColumnClick: reorderColumn
}
];
setRequestColumns(columns);
}, []);

let requestsList;
switch (loadingState) {
case LoadingState.Ok:
if (airlockRequests.length > 0) {
requestsList = (
<DetailsList
items={airlockRequests}
columns={requestColumns}
selectionMode={SelectionMode.none}
getKey={(item) => item.id}
onItemInvoked={(item) => navigate(item.id)}
className="tre-table-rows-align-centre"
/>
);
} else {
requestsList = (
<div style={{textAlign: 'center', padding: '50px'}}>
<h4>No requests found</h4>
<small>Looks like there are no airlock requests yet. Create a new request to get started.</small>
</div>
)
}
break;
case LoadingState.Error:
requestsList = (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error fetching airlock requests</h3>
<p>There was an error fetching the airlock requests. Please see the browser console for details.</p>
</MessageBar>
); break;
default:
requestsList = (
<div style={{ padding: '50px' }}>
<Spinner label="Loading airlock requests" ariaLive="assertive" labelPosition="top" size={SpinnerSize.large} />
</div>
); break;
}

const updateRequest = (updatedRequest: AirlockRequest) => {
setAirlockRequests(requests => {
const i = requests.findIndex(r => r.id === updatedRequest.id);
const updatedRequests = [...requests];
updatedRequests[i] = updatedRequest;
return updatedRequests;
});
};

return (
<>
<Stack className="tre-panel">
<Stack.Item>
<Stack horizontal horizontalAlign="space-between">
<h1 style={{marginBottom: '0px'}}>Airlock</h1>
<CommandBarButton
iconProps={{ iconName: 'add' }}
text="New request"
style={{ background: 'none', color: theme.palette.themePrimary }}
/>
</Stack>
</Stack.Item>
</Stack>

<div className="tre-resource-panel" style={{padding: '0px'}}>
{ requestsList }
</div>

<Routes>
<Route path=":requestId" element={
<AirlockViewRequest requests={airlockRequests} updateRequest={updateRequest}/>
} />
</Routes>
</>
);

};

Loading

0 comments on commit 1473d8d

Please sign in to comment.