Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource Actions #1907

Merged
merged 6 commits into from
May 24, 2022
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
7 changes: 0 additions & 7 deletions ui/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
# NOTE FOR OCW:
All the config has been setup (so no manual steps are needed). Just do:
- Open this folder (`ui`) in the devcontainer
- `cd ./app`
- `yarn start`


# TRE UI

This is very much a work in progress.
Expand Down
3 changes: 3 additions & 0 deletions ui/app/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,7 @@ ul.tre-notifications-steps-list li{
}
.tre-workspace-header h1{
margin:10px 0 10px 0;
}
.tre-context-menu button{
text-transform: capitalize;
}
65 changes: 34 additions & 31 deletions ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,36 @@ export const App: React.FunctionComponent = () => {
<Routes>
<Route path="*" element={
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect}>
<NotificationsContext.Provider value={{
operations: operations,
addOperation: (op: Operation) => {
let ops = [...operations];
let i = ops.findIndex((f: Operation) => f.id === op.id);
if (i > 0) {
ops.splice(i, 1, op);
<NotificationsContext.Provider value={{
operations: operations,
addOperations: (ops: Array<Operation>) => {
let stateOps = [...operations];
ops.forEach((op: Operation) => {
let i = stateOps.findIndex((f: Operation) => f.id === op.id);
if (i > 0) {
stateOps.splice(i, 1, op);
} else {
ops.push(op);
stateOps.push(op);
}
setOperations(ops)
},
resourceUpdates: resourceUpdates,
addResourceUpdate: (r: ResourceUpdate) => {
let updates = [...resourceUpdates];
let i = updates.findIndex((f: ResourceUpdate) => f.resourceId === r.resourceId);
if (i > 0) {
updates.splice(i, 1, r);
} else {
updates.push(r);
}
setResourceUpdates(updates);
},
clearUpdatesForResource: (resourceId: string) => {let updates = [...resourceUpdates].filter((r: ResourceUpdate) => r.resourceId !== resourceId); setResourceUpdates(updates);}
}}>
<AppRolesContext.Provider value={{
});
setOperations(stateOps);
},
resourceUpdates: resourceUpdates,
addResourceUpdate: (r: ResourceUpdate) => {
let updates = [...resourceUpdates];
let i = updates.findIndex((f: ResourceUpdate) => f.resourceId === r.resourceId);
if (i > 0) {
updates.splice(i, 1, r);
} else {
updates.push(r);
}
setResourceUpdates(updates);
},
clearUpdatesForResource: (resourceId: string) => { let updates = [...resourceUpdates].filter((r: ResourceUpdate) => r.resourceId !== resourceId); setResourceUpdates(updates); }
}}>
<AppRolesContext.Provider value={{
roles: appRoles,
setAppRoles: (roles: Array<string>) => {setAppRoles(roles)}
setAppRoles: (roles: Array<string>) => { setAppRoles(roles) }
}}>
<Stack styles={stackStyles} className='tre-root'>
<Stack.Item grow className='tre-top-nav'>
Expand All @@ -64,15 +66,16 @@ export const App: React.FunctionComponent = () => {
<Stack.Item grow={100} className='tre-body'>
<GenericErrorBoundary>
<Routes>
<Route path="*" element={<RootLayout selectWorkspace={(ws: Workspace) => setSelectedWorkspace(ws)} />} />
<Route path="*" element={<RootLayout />} />
<Route path="/workspaces/:workspaceId//*" element={
<WorkspaceContext.Provider value={{
roles: workspaceRoles,
<WorkspaceContext.Provider value={{
roles: workspaceRoles,
setRoles: (roles: Array<string>) => setWorkspaceRoles(roles),
workspace: selectedWorkspace,
setWorkspace: (w: Workspace) => setSelectedWorkspace(w),
workspaceClientId: selectedWorkspace.properties?.app_id}}>
<WorkspaceProvider workspace={selectedWorkspace} />
setWorkspace: (w: Workspace) => { console.warn("Workspace set", w); setSelectedWorkspace(w) },
workspaceClientId: selectedWorkspace.properties?.scope_id.replace("api://", "")
}}>
<WorkspaceProvider />
</WorkspaceContext.Provider>
} />
</Routes>
Expand Down
3 changes: 1 addition & 2 deletions ui/app/src/components/root/RootDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ResourceType } from '../../models/resourceType';
import { useBoolean } from '@fluentui/react-hooks';

interface RootDashboardProps {
selectWorkspace: (workspace: Workspace) => void,
selectWorkspace?: (workspace: Workspace) => void,
workspaces: Array<Workspace>,
updateWorkspace: (w: Workspace) => void,
removeWorkspace: (w: Workspace) => void
Expand All @@ -28,7 +28,6 @@ export const RootDashboard: React.FunctionComponent<RootDashboardProps> = (props
</Stack>
<ResourceCardList
resources={props.workspaces}
selectResource={(r: Resource) => props.selectWorkspace(r as Workspace)}
updateResource={(r: Resource) => props.updateWorkspace(r as Workspace)}
removeResource={(r: Resource) => props.removeWorkspace(r as Workspace)}
emptyText="No workspaces to display. Create one to get started." />
Expand Down
7 changes: 1 addition & 6 deletions ui/app/src/components/root/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import { LeftNav } from './LeftNav';
import config from '../../config.json';
import { LoadingState } from '../../models/loadingState';

interface RootLayoutProps {
selectWorkspace: (workspace: Workspace) => void
}

export const RootLayout: React.FunctionComponent<RootLayoutProps> = (props: RootLayoutProps) => {
export const RootLayout: React.FunctionComponent = () => {
const [workspaces, setWorkspaces] = useState([] as Array<Workspace>);
const appRolesContext = useRef(useContext(AppRolesContext));
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
Expand Down Expand Up @@ -64,7 +60,6 @@ export const RootLayout: React.FunctionComponent<RootLayoutProps> = (props: Root
<Routes>
<Route path="/" element={
<RootDashboard
selectWorkspace={props.selectWorkspace}
workspaces={workspaces}
updateWorkspace={(w: Workspace) => updateWorkspace(w)}
removeWorkspace={(w: Workspace) => removeWorkspace(w)} />
Expand Down
2 changes: 1 addition & 1 deletion ui/app/src/components/shared/ConfirmDeleteResource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const ConfirmDeleteResource: React.FunctionComponent<ConfirmDeleteProps>
const deleteCall = async () => {
setIsSending(true);
let op = await apiCall(props.resource.resourcePath, HttpMethod.Delete, workspaceCtx.workspaceClientId, undefined, ResultType.JSON);
opsCtx.addOperation(op.operation);
opsCtx.addOperations([op.operation]);
setIsSending(false);
props.onDismiss();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ConfirmDisableEnableResource: React.FunctionComponent<ConfirmDisabl
setIsSending(true);
let body = { isEnabled: props.isEnabled }
let op = await apiCall(props.resource.resourcePath, HttpMethod.Patch, workspaceCtx.workspaceClientId, body, ResultType.JSON, undefined, undefined, props.resource._etag);
opsCtx.addOperation(op.operation);
opsCtx.addOperations([op.operation]);
setIsSending(false);
props.onDismiss();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const CreateUpdateResource: React.FunctionComponent<CreateUpdateResourceP
setDeployOperation(operation);
setPage('creating');
// Add deployment operation to notifications operation poller
opsContext.addOperation(operation);
opsContext.addOperations([operation]);
}

// Render the current panel sub-page
Expand Down
62 changes: 55 additions & 7 deletions ui/app/src/components/shared/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import { NotificationsContext } from '../../contexts/NotificationsContext';
import { HttpMethod, useAuthApiCall } from '../../useAuthApiCall';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { ConfirmDeleteResource } from './ConfirmDeleteResource';
import { ApiEndpoint } from '../../models/apiEndpoints';
import { UserResource } from '../../models/userResource';
import { getActionIcon, ResourceTemplate, TemplateAction } from '../../models/resourceTemplate';

interface ResourceCardProps {
resource: Resource,
itemId: number,
selectResource: (resource: Resource) => void,
selectResource?: (resource: Resource) => void,
onUpdate: (resource: Resource) => void,
onDelete: (resource: Resource) => void
}
Expand All @@ -28,10 +31,39 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
const [showDisable, setShowDisable] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [componentAction, setComponentAction] = useState(ComponentAction.None);
const [resourceTemplate, setResourceTemplate] = useState({} as ResourceTemplate);

const opsReadContext = useContext(NotificationsContext);
const opsWriteContext = useRef(useContext(NotificationsContext)); // useRef to avoid re-running a hook on context write

// get the resource template
useEffect(() => {
const getTemplate = async () => {
let templatesPath;
switch (props.resource.resourceType) {
case ResourceType.Workspace:
templatesPath = ApiEndpoint.WorkspaceTemplates; break;
case ResourceType.WorkspaceService:
templatesPath = ApiEndpoint.WorkspaceServiceTemplates; break;
case ResourceType.SharedService:
templatesPath = ApiEndpoint.SharedServiceTemplates; break;
case ResourceType.UserResource:
const ur = props.resource as UserResource;
const parentService = (await apiCall(
`${ApiEndpoint.Workspaces}/${workspaceCtx.workspace.id}/${ApiEndpoint.WorkspaceServices}/${ur.parentWorkspaceServiceId}`,
HttpMethod.Get,
workspaceCtx.workspaceClientId))
.workspaceService;
templatesPath = `${ApiEndpoint.WorkspaceServiceTemplates}/${parentService.templateName}/${ApiEndpoint.UserResourceTemplates}`; break;
default:
throw Error('Unsupported resource type.');
}
const template = await apiCall(`${templatesPath}/${props.resource.templateName}`, HttpMethod.Get);
setResourceTemplate(template);
};
getTemplate();
}, [apiCall, props.resource, workspaceCtx.workspace.id, workspaceCtx.workspaceClientId]);

// set the latest component action
useEffect(() => {
let updates = opsReadContext.resourceUpdates.filter((r: ResourceUpdate) => { return r.resourceId === props.resource.id });
Expand All @@ -58,6 +90,11 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
checkForReload();
}, [apiCall, componentAction, props, workspaceCtx.workspaceClientId]);

const doAction = async(actionName: string) => {
const action = await apiCall(`${props.resource.resourcePath}/${ApiEndpoint.InvokeAction}?action=${actionName}`, HttpMethod.Post, workspaceCtx.workspaceClientId);
action && action.operation && opsWriteContext.current.addOperations([action.operation]);
}

// context menu
let menuItems: Array<IContextualMenuItem> = [];
let roles: Array<string> = [];
Expand All @@ -66,9 +103,20 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
menuItems = [
{ key: 'update', text: 'Update', iconProps: { iconName: 'WindowEdit' }, onClick: () => console.log('update') },
{ key: 'disable', text: props.resource.isEnabled ? 'Disable' : 'Enable', iconProps: { iconName: props.resource.isEnabled ? 'CirclePause' : 'PlayResume' }, onClick: () => setShowDisable(true) },
{ key: 'delete', text: 'Delete', title: props.resource.isEnabled ? 'Must be disabled to delete' : 'Delete this resource', iconProps: { iconName: 'Delete' }, onClick: () => setShowDelete(true), disabled: props.resource.isEnabled }
{ key: 'delete', text: 'Delete', title: props.resource.isEnabled ? 'Must be disabled to delete' : 'Delete this resource', iconProps: { iconName: 'Delete' }, onClick: () => setShowDelete(true), disabled: props.resource.isEnabled },
];

// add custom actions if we have any
if (resourceTemplate && resourceTemplate.customActions && resourceTemplate.customActions.length > 0) {
let customActions: Array<IContextualMenuItem> = [];
resourceTemplate.customActions.forEach((a: TemplateAction) => {
customActions.push(
{ key: a.name, text: a.name, title: a.description, iconProps: { iconName: getActionIcon(a.name) }, className: 'tre-context-menu', onClick: () => { doAction(a.name) }}
);
});
menuItems.push({ key: 'custom-actions', text: 'Actions', iconProps: { iconName: 'Asterisk' }, subMenuProps: { items: customActions }} );
}

switch (props.resource.resourceType) {
case ResourceType.Workspace:
case ResourceType.SharedService:
Expand All @@ -83,7 +131,7 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:

const menuProps: IContextualMenuProps = {
shouldFocusOnMount: true,
items: menuItems,
items: menuItems
};

let connectUri = props.resource.properties && props.resource.properties.connection_uri;
Expand Down Expand Up @@ -111,7 +159,7 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
<Stack style={cardStyles}>
<Stack horizontal>
<Stack.Item grow={5} style={headerStyles}>
<Link to={props.resource.resourcePath} onClick={() => { props.selectResource(props.resource); return false }} style={headerLinkStyles}>{props.resource.properties.display_name}</Link>
<Link to={props.resource.resourcePath} onClick={() => { props.selectResource && props.selectResource(props.resource); return false }} style={headerLinkStyles}>{props.resource.properties.display_name}</Link>
</Stack.Item>
<Stack.Item style={headerIconStyles}>
<Stack horizontal>
Expand All @@ -132,7 +180,7 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
{
connectUri &&
<Stack.Item style={connectStyles}>
<PrimaryButton onClick={() => window.open(connectUri)}>Connect</PrimaryButton>
<PrimaryButton onClick={() => window.open(connectUri)} disabled={!props.resource.isEnabled} title={!props.resource.isEnabled ? 'Enable resource to connect' : 'Connect to resource'}>Connect</PrimaryButton>
</Stack.Item>
}
<Stack.Item style={footerStyles}>
Expand Down Expand Up @@ -242,10 +290,10 @@ const styles = mergeStyleSets({
},
title: {
marginBottom: 12,
fontWeight: FontWeights.semilight,
fontWeight: FontWeights.semilight
},
link: {
display: 'block',
marginTop: 20,
},
}
});
4 changes: 2 additions & 2 deletions ui/app/src/components/shared/ResourceCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Resource } from '../../models/resource';

interface ResourceCardListProps {
resources: Array<Resource>,
selectResource: (resource: Resource) => void,
selectResource?: (resource: Resource) => void,
updateResource: (resource: Resource) => void,
removeResource: (resource: Resource) => void
emptyText: string,
Expand All @@ -25,7 +25,7 @@ export const ResourceCardList: React.FunctionComponent<ResourceCardListProps> =
<Stack.Item key={i} style={gridItemStyles} >
<ResourceCard
resource={r}
selectResource={(resource: Resource) => props.selectResource(resource)}
selectResource={(resource: Resource) => props.selectResource && props.selectResource(resource)}
onUpdate={(resource: Resource) => props.updateResource(resource)}
onDelete={(resource: Resource) => props.removeResource(resource)}
itemId={i} />
Expand Down
5 changes: 3 additions & 2 deletions ui/app/src/components/shared/ResourcePropertyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const ResourcePropertyPanelItem: React.FunctionComponent<ResourcePropertyPanelIt
}

function renderValue(val: String) {
if (val.startsWith('https://')) {
if (val && val.startsWith('https://')) {
return (<a href={val.toString()} target='_blank' rel="noreferrer">{val}</a>)
}
return val;
Expand Down Expand Up @@ -58,6 +58,7 @@ export const ResourcePropertyPanel: React.FunctionComponent<ResourcePropertyPane
}

return (
props.resource && props.resource.id ?
<>
<Stack wrap horizontal>
<Stack grow styles={stackStyles}>
Expand All @@ -82,6 +83,6 @@ export const ResourcePropertyPanel: React.FunctionComponent<ResourcePropertyPane
}
</Stack>
</Stack>
</>
</> : <></>
);
};
Loading