Skip to content

Commit

Permalink
UI - Add Upgrade Button to Resources (#3387)
Browse files Browse the repository at this point in the history
* add upgrade button to resources that can be upgraded (non major upgrades only)

* update changelog

* update ui version

* fix cr review comments

* Update ui/app/src/components/shared/ConfirmUpgradeResource.tsx

Co-authored-by: James Griffin <me@JamesGriff.in>

---------

Co-authored-by: James Griffin <me@JamesGriff.in>
  • Loading branch information
yuvalyaron and jjgriff93 authored Mar 30, 2023
1 parent b1544e8 commit 9167c95
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
A migration for OperationSteps in Operation objects was added ([#3358](https://github.com/microsoft/AzureTRE/pull/3358)).

FEATURES:
* (UI) Added upgrade button to resources that have pending template upgrades ([#3387](https://github.com/microsoft/AzureTRE/pull/3387))

ENHANCEMENTS:
* Added 'availableUpgrades' field to Resources in GET/GET all Resources endpoints. The field indicates whether there are template versions that a resource can be upgraded to [#3234](https://github.com/microsoft/AzureTRE/pull/3234)
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.4.2",
"version": "0.5.0",
"private": true,
"dependencies": {
"@azure/msal-browser": "^2.33.0",
Expand Down
120 changes: 120 additions & 0 deletions ui/app/src/components/shared/ConfirmUpgradeResource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Dialog, DialogFooter, PrimaryButton, DialogType, Spinner, Dropdown, MessageBar, MessageBarType, DropdownMenuItemType, IDropdownOption, Icon, Stack, Label, IconButton, IDropdownProps } from '@fluentui/react';
import React, { useContext, useState } from 'react';
import { AvailableUpgrade, Resource } from '../../models/resource';
import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { ResourceType } from '../../models/resourceType';
import { APIError } from '../../models/exceptions';
import { LoadingState } from '../../models/loadingState';
import { ExceptionLayout } from './ExceptionLayout';
import { useAppDispatch } from '../../hooks/customReduxHooks';
import { addUpdateOperation } from '../shared/notifications/operationsSlice';

interface ConfirmUpgradeProps {
resource: Resource,
onDismiss: () => void
}

export const ConfirmUpgradeResource: React.FunctionComponent<ConfirmUpgradeProps> = (props: ConfirmUpgradeProps) => {
const apiCall = useAuthApiCall();
const [selectedVersion, setSelectedVersion] = useState("")
const [apiError, setApiError] = useState({} as APIError);
const [requestLoadingState, setRequestLoadingState] = useState(LoadingState.Ok);
const workspaceCtx = useContext(WorkspaceContext);
const dispatch = useAppDispatch();

const upgradeProps = {
type: DialogType.normal,
title: `Upgrade Template Version?`,
closeButtonAriaLabel: 'Close',
subText: `Are you sure you want upgrade the template version of ${props.resource.properties.display_name} from version ${props.resource.templateVersion}?`,
};

const dialogStyles = { main: { maxWidth: 450 } };
const modalProps = {
titleAriaId: 'labelId',
subtitleAriaId: 'subTextId',
isBlocking: true,
styles: dialogStyles
};

const wsAuth = (props.resource.resourceType === ResourceType.WorkspaceService || props.resource.resourceType === ResourceType.UserResource);

const upgradeCall = async () => {
setRequestLoadingState(LoadingState.Loading);
try {
let body = { templateVersion: selectedVersion }
let op = await apiCall(props.resource.resourcePath,
HttpMethod.Patch,
wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined,
body,
ResultType.JSON,
undefined,
undefined,
props.resource._etag);
dispatch(addUpdateOperation(op.operation));
props.onDismiss();
} catch (err: any) {
err.userMessage = 'Failed to upgrade resource';
setApiError(err);
setRequestLoadingState(LoadingState.Error);
}
}

const onRenderOption = (option: any): JSX.Element => {
return (
<div>
{option.data && option.data.icon && (
<Icon style={{ marginRight: '8px' }} iconName={option.data.icon} aria-hidden="true" title={option.data.icon} />
)}
<span>{option.text}</span>
</div>
);
};

const convertToDropDownOptions = (upgrade: Array<AvailableUpgrade>) => {
return upgrade.map(upgrade => ({ "key": upgrade.version, "text": upgrade.version, data: { icon: upgrade.forceUpdateRequired ? 'Warning' : '' } }))
}

const getDropdownOptions = () => {
const options = []
const nonMajorUpgrades = props.resource.availableUpgrades.filter(upgrade => !upgrade.forceUpdateRequired)
options.push(...convertToDropDownOptions(nonMajorUpgrades))
return options;
}

return (<>
<Dialog
hidden={false}
onDismiss={() => props.onDismiss()}
dialogContentProps={upgradeProps}
modalProps={modalProps}
>
{
requestLoadingState === LoadingState.Ok &&
<>
<MessageBar messageBarType={MessageBarType.warning} >Upgrading the template version is irreversible.</MessageBar>
<DialogFooter>
<Dropdown
placeholder='Select Version'
options={getDropdownOptions()}
onRenderOption={onRenderOption}
styles={{ dropdown: { width: 125 } }}
onChange={(event, option) => { option && setSelectedVersion(option.text); }}
selectedKey={selectedVersion}
/>
<PrimaryButton primaryDisabled={!selectedVersion} text="Upgrade" onClick={() => upgradeCall()} />
</DialogFooter>
</>
}
{
requestLoadingState === LoadingState.Loading &&
<Spinner label="Sending request..." ariaLive="assertive" labelPosition="right" />
}
{
requestLoadingState === LoadingState.Error &&
<ExceptionLayout e={apiError} />
}
</Dialog>
</>);
};
19 changes: 19 additions & 0 deletions ui/app/src/components/shared/ResourceContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { actionsDisabledStates } from '../../models/operation';
import { AppRolesContext } from '../../contexts/AppRolesContext';
import { useAppDispatch } from '../../hooks/customReduxHooks';
import { addUpdateOperation } from '../shared/notifications/operationsSlice';
import { ConfirmUpgradeResource } from './ConfirmUpgradeResource';

interface ResourceContextMenuProps {
resource: Resource,
Expand All @@ -30,6 +31,7 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
const workspaceCtx = useContext(WorkspaceContext);
const [showDisable, setShowDisable] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [showUpgrade, setShowUpgrade] = useState(false);
const [resourceTemplate, setResourceTemplate] = useState({} as ResourceTemplate);
const createFormCtx = useContext(CreateUpdateResourceContext);
const [parentResource, setParentResource] = useState({} as WorkspaceService | Workspace);
Expand Down Expand Up @@ -182,6 +184,19 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
});
}

// add 'upgrade' button if we have available template upgrades
const nonMajorUpgrades = props.resource.availableUpgrades?.filter(upgrade => !upgrade.forceUpdateRequired)
if (nonMajorUpgrades.length > 0) {
menuItems.push({
key: 'upgrade',
text: 'Upgrade',
title: 'Upgrade this resource template version',
iconProps: { iconName: 'Refresh' },
onClick: () => setShowUpgrade(true),
disabled: (props.componentAction === ComponentAction.Lock)
})
}

const menuProps: IContextualMenuProps = {
shouldFocusOnMount: true,
items: menuItems
Expand All @@ -206,6 +221,10 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
showDelete &&
<ConfirmDeleteResource onDismiss={() => setShowDelete(false)} resource={props.resource} />
}
{
showUpgrade &&
<ConfirmUpgradeResource onDismiss={() => setShowUpgrade(false)} resource={props.resource} />
}
</>
)
};
6 changes: 6 additions & 0 deletions ui/app/src/models/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface Resource {
resourceType: ResourceType
templateName: string,
templateVersion: string,
availableUpgrades: Array<AvailableUpgrade>,
deploymentStatus: string,
updatedWhen: number,
user: User,
Expand All @@ -30,6 +31,11 @@ export interface HistoryItem {
templateVersion: string
}

export interface AvailableUpgrade {
version: string,
forceUpdateRequired : boolean
}

export enum ComponentAction {
None,
Reload,
Expand Down

0 comments on commit 9167c95

Please sign in to comment.