UI - Add Upgrade Button to Resources (#3387)

* 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>
This commit is contained in:
Yuval Yaron 2023-03-30 17:49:22 +03:00 коммит произвёл GitHub
Родитель b1544e805a
Коммит 9167c956be
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 147 добавлений и 1 удалений

Просмотреть файл

@ -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)

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "tre-ui",
"version": "0.4.2",
"version": "0.5.0",
"private": true,
"dependencies": {
"@azure/msal-browser": "^2.33.0",

Просмотреть файл

@ -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>
</>);
};

Просмотреть файл

@ -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,
@ -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);
@ -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
@ -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} />
}
</>
)
};

Просмотреть файл

@ -10,6 +10,7 @@ export interface Resource {
resourceType: ResourceType
templateName: string,
templateVersion: string,
availableUpgrades: Array<AvailableUpgrade>,
deploymentStatus: string,
updatedWhen: number,
user: User,
@ -30,6 +31,11 @@ export interface HistoryItem {
templateVersion: string
}
export interface AvailableUpgrade {
version: string,
forceUpdateRequired : boolean
}
export enum ComponentAction {
None,
Reload,