Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Comment thread
damoodamoo marked this conversation as resolved.
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} />
Comment thread
damoodamoo marked this conversation as resolved.
</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