Skip to content

Commit

Permalink
Notifications Panel (#1851)
Browse files Browse the repository at this point in the history
* basic polling

* btn placement, panel

* fun with polling

* mostly implemented

* shimmer on loading

* polling from config

* pr feedback
  • Loading branch information
damoodamoo authored May 18, 2022
1 parent 2093efa commit 957b847
Show file tree
Hide file tree
Showing 19 changed files with 622 additions and 49 deletions.
1 change: 1 addition & 0 deletions ui/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@types/node": "^14.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"moment": "^2.29.3",
"node-sass": "^7.0.1",
"react": "^17.0.0",
"react-dom": "^17.0.0",
Expand Down
45 changes: 39 additions & 6 deletions ui/app/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.tre-logout-message{
margin:40px auto;
width:70%;
margin: 40px auto;
width: 70%;
}
#root{

Expand All @@ -28,20 +28,53 @@ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
box-shadow: 0 1px 2px 0px #033d68;
z-index: 100;
}

.tre-notifications-button{
position: relative;
top: 7px;
color: #fff;
}
.tre-notifications-button i{
font-size: 24px;
}
.tre-notifications-dismiss{
text-align: right;
}
ul.tre-notifications-list{
margin: 0;
padding: 0;
}
.tre-notifications-list li{
list-style: none;
margin-top: 20px;
padding-bottom: 10px;
border-bottom: 1px #ccc solid;
}
ul.tre-notifications-steps-list{
padding: 5px 15px 15px 15px;
background-color: #f9f9f9;
margin: 10px 0 0 0;
}
ul.tre-notifications-steps-list li{
border: none;
}
.tre-notification-time{
font-style: italic;
text-align: right;
font-size: 12px;
}
.tre-home-link{
color:#fff;
color: #fff;
text-decoration: none;
}
.tre-user-menu .ms-Persona-primaryText:hover{
color:#fff;
}
.ms-Persona-primaryText{
color:#fff;
color: #fff;
}

.tre-body{
height:100%;
height: 100%;
overflow: hidden;
}
.tre-body-inner{
Expand Down
49 changes: 27 additions & 22 deletions ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,42 @@ import { Workspace } from './models/workspace';
import { RootRolesContext } from './components/shared/RootRolesContext';
import { WorkspaceRolesContext } from './components/workspaces/WorkspaceRolesContext';
import { GenericErrorBoundary } from './components/shared/GenericErrorBoundary';
import { NotificationsContext } from './components/shared/notifications/NotificationsContext';
import { Operation } from './models/operation';

export const App: React.FunctionComponent = () => {
const [selectedWorkspace, setSelectedWorkspace] = useState({} as Workspace);
const [latestOperation, setLatestOperation] = useState({} as Operation);

return (
<>
<Routes>
<Route path="*" element={
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect}>
<RootRolesContext.Provider value={{ roles: [] as Array<string> }}>
<Stack styles={stackStyles} className='tre-root'>
<Stack.Item grow className='tre-top-nav'>
<TopNav />
</Stack.Item>
<Stack.Item grow={100} className='tre-body'>
<GenericErrorBoundary>
<Routes>
<Route path="*" element={<RootLayout selectWorkspace={(ws: Workspace) => setSelectedWorkspace(ws)} />} />
<Route path="/workspaces/:workspaceId//*" element={
<WorkspaceRolesContext.Provider value={{ roles: [] as Array<string> }}>
<WorkspaceProvider workspace={selectedWorkspace} />
</WorkspaceRolesContext.Provider>
} />
</Routes>
</GenericErrorBoundary>
</Stack.Item>
<Stack.Item grow>
<Footer />
</Stack.Item>
</Stack>
</RootRolesContext.Provider>
<NotificationsContext.Provider value={{ latestOperation: latestOperation, addOperation: (op: Operation) => {setLatestOperation(op);}}}>
<RootRolesContext.Provider value={{ roles: [] as Array<string> }}>
<Stack styles={stackStyles} className='tre-root'>
<Stack.Item grow className='tre-top-nav'>
<TopNav />
</Stack.Item>
<Stack.Item grow={100} className='tre-body'>
<GenericErrorBoundary>
<Routes>
<Route path="*" element={<RootLayout selectWorkspace={(ws: Workspace) => setSelectedWorkspace(ws)} />} />
<Route path="/workspaces/:workspaceId//*" element={
<WorkspaceRolesContext.Provider value={{ roles: [] as Array<string> }}>
<WorkspaceProvider workspace={selectedWorkspace} />
</WorkspaceRolesContext.Provider>
} />
</Routes>
</GenericErrorBoundary>
</Stack.Item>
<Stack.Item grow>
<Footer />
</Stack.Item>
</Stack>
</RootRolesContext.Provider>
</NotificationsContext.Provider>
</MsalAuthenticationTemplate>
} />
<Route path='/logout' element={
Expand Down
2 changes: 2 additions & 0 deletions ui/app/src/components/root/RootDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RootRolesContext } from '../shared/RootRolesContext';
import { PrimaryButton } from '@fluentui/react';
import { SecuredByRole } from '../shared/SecuredByRole';
import { RoleName } from '../../models/roleNames';
import { AddNotificationDemo } from '../shared/notifications/AddNotificationDemo';

// TODO:
// - Create WorkspaceCard component + use instead of <Link>
Expand All @@ -21,6 +22,7 @@ export const RootDashboard: React.FunctionComponent<RootDashboardProps> = (props

return (
<>
<AddNotificationDemo />
<h3>TRE Roles</h3>
<ul>
{
Expand Down
6 changes: 4 additions & 2 deletions ui/app/src/components/shared/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { getTheme, 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 = () => {
Expand All @@ -10,9 +11,10 @@ export const TopNav: React.FunctionComponent = () => {
<div className={contentClass}>
<Stack horizontal>
<Stack.Item grow={100}>

<Link to='/' className='tre-home-link'>Azure TRE</Link>

</Stack.Item>
<Stack.Item>
<NotificationPanel />
</Stack.Item>
<Stack.Item grow>
<UserMenu />
Expand Down
57 changes: 57 additions & 0 deletions ui/app/src/components/shared/notifications/AddNotificationDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useContext } from 'react';
import { Operation } from '../../../models/operation';
import { PrimaryButton } from '@fluentui/react';
import { NotificationsContext } from './NotificationsContext';
import dummyOp from './dummyOp.json';
import dummyOpSteps from './dummyOpSteps.json';
import { HttpMethod, ResultType, useAuthApiCall } from '../../../useAuthApiCall';

export const AddNotificationDemo: React.FunctionComponent = () => {
const opsContext = useContext(NotificationsContext);
const apiCall = useAuthApiCall();

const addSampleNotification = () => {
let d = JSON.parse(JSON.stringify(dummyOp)) as Operation; // avoid reusing the same object
console.log("adding test notification", d);
opsContext.addOperation(d);
}

const addSampleNotificationWithSteps = () => {
let d = JSON.parse(JSON.stringify(dummyOpSteps)) as Operation; // avoid reusing the same object
console.log("adding test notification with steps", d);
opsContext.addOperation(d);
}

const postDemoPatch = async () => {
let body = {
properties: {
description: `Updated ${new Date().getTime()}`
}
}
// patch "Jda VM"
let op = await apiCall("workspaces/1e800001-7385-46a1-9f6d-490a6201ea01/workspace-services/8c70974a-5f66-4ae9-9502-7a54e9e0bb86/user-resources/8b6e42a0-e236-46ae-9541-01b462e4b468", HttpMethod.Patch, "816634e3-141d-4183-87a1-aaf2b95b7e12", body, ResultType.JSON, undefined, undefined, "*");
opsContext.addOperation(op.operation);
}

const postMultiStageVm = async () => {
let body = {
templateName: "tre-service-dev-vm",
properties: {
display_name: "my user resource",
description: "some description"
}
}
let op = await apiCall("workspaces/30314a30-ddf5-4b66-9854-fa580c00b54e/workspace-services/6fba7e63-96bd-4253-aacb-76f62137fd63/user-resources", HttpMethod.Post, "653a0b60-b798-401d-a6cb-3e824b6f4308", body, ResultType.JSON, undefined, undefined, "*");
opsContext.addOperation(op.operation);
}

return (
<>
<h4>Notifications test harness</h4>
<PrimaryButton onClick={() => addSampleNotification()}>Add Test Notification</PrimaryButton>&nbsp;
<PrimaryButton onClick={() => addSampleNotificationWithSteps()}>Add Test Notification (with steps)</PrimaryButton>&nbsp;
<PrimaryButton onClick={() => postDemoPatch()}>Patch a real VM</PrimaryButton>&nbsp;
<PrimaryButton onClick={() => postMultiStageVm()}>Post a multi-step VM</PrimaryButton>
</>
);
};
92 changes: 92 additions & 0 deletions ui/app/src/components/shared/notifications/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useState } from 'react';
import { Icon, ProgressIndicator, Link as FluentLink, Stack } from '@fluentui/react';
import { TRENotification } from '../../../models/treNotification';
import { completedStates, failedStates, inProgressStates, OperationStep } from '../../../models/operation';
import { Link } from 'react-router-dom';
import moment from 'moment';
import { useInterval } from './useInterval';
import { isNoSubstitutionTemplateLiteral } from 'typescript';
import { relative } from 'path';

interface NotificationItemProps {
notification: TRENotification
}

export const NotificationItem: React.FunctionComponent<NotificationItemProps> = (props: NotificationItemProps) => {
const [now, setNow] = useState(moment.utc());
const [isExpanded, setIsExpanded] = useState(false);

const getRelativeTime = (createdWhen: number) => {
return (moment.utc(moment.unix(createdWhen))).from(now);
}

// update the 'now' time for comparison - only while the item is rendered (panel is open)
useInterval(() => {
setNow(moment.utc());
}, 10000)

const getIconAndColourForStatus = (status: string) => {
if (failedStates.includes(status)) return ['ErrorBadge', 'red'];
if (completedStates.includes(status)) return ['SkypeCheck', 'green'];
if (status === "not_deployed") return ['Clock', '#cccccc'];
return ['ProgressLoopInner', 'blue'];
}

return (
<li className="tre-notification-item">
{
inProgressStates.includes(props.notification.operation.status) ?
<>
<ProgressIndicator
barHeight={4}
label={<Link style={{ textDecoration: 'none', fontWeight: 'bold', color: 'blue' }} to={props.notification.operation.resourcePath}>
{props.notification.resource.properties.display_name}: {props.notification.operation.action}
</Link>}
description={`${props.notification.resource.resourceType} is ${props.notification.operation.status}`} />
</>
:
<ProgressIndicator
barHeight={4}
percentComplete={100}
label={
<Link style={{ textDecoration: 'none', fontWeight: 'bold', color: 'blue' }} to={props.notification.operation.resourcePath}>
<Icon iconName={getIconAndColourForStatus(props.notification.operation.status)[0]} style={{ color: getIconAndColourForStatus(props.notification.operation.status)[1], position: 'relative', top: '2px', marginRight: '10px' }} />
{props.notification.resource.properties.display_name}: {props.notification.operation.action}
</Link>
}
description={`${props.notification.resource.resourceType} is ${props.notification.operation.status}`} />
}
<Stack horizontal style={{ marginTop: '10px' }}>
<Stack.Item grow={5}>
{
props.notification.operation.steps && props.notification.operation.steps.length > 0 && !(props.notification.operation.steps.length === 1 && props.notification.operation.steps[0].stepId === 'main') ?
<FluentLink title={isExpanded ? 'Show less' : 'Show more'} href="#" onClick={() => { setIsExpanded(!isExpanded) }} style={{ position: 'relative', top: '2px' }}>{isExpanded ? <Icon iconName='ChevronUp' aria-label='Expand Steps' /> : <Icon iconName='ChevronDown' aria-label='Collapse Steps' />}</FluentLink>
:
' '
}
</Stack.Item>
<Stack.Item> <div className="tre-notification-time">{getRelativeTime(props.notification.operation.createdWhen)}</div></Stack.Item>
</Stack>

{
isExpanded &&
<>
<ul className="tre-notifications-steps-list">
{props.notification.operation.steps && props.notification.operation.steps.map((s: OperationStep, i: number) => {
return (
<li key={i}>
<Icon iconName={getIconAndColourForStatus(s.status)[0]} style={{ color: getIconAndColourForStatus(s.status)[1], position: 'relative', top: '2px', marginRight: '10px' }} />
{
s.stepId === "main" ?
<>{props.notification.resource.properties.display_name}: {props.notification.operation.action}</> :
s.stepTitle
}
</li>)
})
}
</ul>
</>
}
</li>
);
};
Loading

0 comments on commit 957b847

Please sign in to comment.