diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f8512d02..12f5d01bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/ui/app/package.json b/ui/app/package.json index 3b5bba0902..9f5ec83955 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -1,6 +1,6 @@ { "name": "tre-ui", - "version": "0.4.2", + "version": "0.5.0", "private": true, "dependencies": { "@azure/msal-browser": "^2.33.0", diff --git a/ui/app/src/components/shared/ConfirmUpgradeResource.tsx b/ui/app/src/components/shared/ConfirmUpgradeResource.tsx new file mode 100644 index 0000000000..cc84cfba4c --- /dev/null +++ b/ui/app/src/components/shared/ConfirmUpgradeResource.tsx @@ -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 = (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 ( +
+ {option.data && option.data.icon && ( +
+ ); + }; + + const convertToDropDownOptions = (upgrade: Array) => { + 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 (<> + + ); +}; diff --git a/ui/app/src/components/shared/ResourceContextMenu.tsx b/ui/app/src/components/shared/ResourceContextMenu.tsx index 9edb17b6a2..bf86ee4753 100644 --- a/ui/app/src/components/shared/ResourceContextMenu.tsx +++ b/ui/app/src/components/shared/ResourceContextMenu.tsx @@ -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 !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 setShowDelete(false)} resource={props.resource} /> } + { + showUpgrade && + setShowUpgrade(false)} resource={props.resource} /> + } ) }; diff --git a/ui/app/src/models/resource.ts b/ui/app/src/models/resource.ts index 73910be4f6..332d3cdfe1 100644 --- a/ui/app/src/models/resource.ts +++ b/ui/app/src/models/resource.ts @@ -10,6 +10,7 @@ export interface Resource { resourceType: ResourceType templateName: string, templateVersion: string, + availableUpgrades: Array, 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,