Skip to content

Commit

Permalink
My Operations + UI Stability improvements (#2530)
Browse files Browse the repository at this point in the history
* try except around getting azure status

* catch validation errors and prevent pipeline from hanging forever

* update primary resource on failure of pipeline

* strip string escape chars from outputs as they're passed back to cosmos

* versions

* my ops endpoint

* ops context stability

* moved from resource updates to checking ops as a readonly collection

* ops progress bar on inprogress ops, deploymentStatus sent to badge correctly

* show success/fail indication on ops callout

* fixed double reload on operation completion

* unwound change made to python file for no apparent reason

* PR feedback

* changelog

* api v bump
  • Loading branch information
damoodamoo committed Aug 31, 2022
1 parent d9cd10b commit 5083fbd
Show file tree
Hide file tree
Showing 24 changed files with 284 additions and 282 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ ENHANCEMENTS:
* Gitea shared service support app-service standard SKUs ([#2523](https://github.com/microsoft/AzureTRE/pull/2523))
* Keyvault diagnostic settings in base workspace ([#2521](https://github.com/microsoft/AzureTRE/pull/2521))
* Airlock requests contain a field with information about the files that were submitted ([#2504](https://github.com/microsoft/AzureTRE/pull/2504))
* UI - Operations and notifications stability improvements ([[#2530](https://github.com/microsoft/AzureTRE/pull/2530)])

BUG FIXES:

Expand Down
2 changes: 1 addition & 1 deletion api_app/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.24"
__version__ = "0.4.25"
3 changes: 2 additions & 1 deletion api_app/api/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from api.dependencies.database import get_repository
from db.repositories.workspaces import WorkspaceRepository
from api.routes import health, workspaces, workspace_templates, workspace_service_templates, user_resource_templates, \
shared_services, shared_service_templates, migrations, costs, airlock
shared_services, shared_service_templates, migrations, costs, airlock, operations
from core import config

core_tags_metadata = [
Expand Down Expand Up @@ -38,6 +38,7 @@
core_router.include_router(user_resource_templates.user_resource_templates_core_router, tags=["user resource templates"])
core_router.include_router(shared_service_templates.shared_service_templates_core_router, tags=["shared service templates"])
core_router.include_router(shared_services.shared_services_router, tags=["shared services"])
core_router.include_router(operations.operations_router, tags=["operations"])
core_router.include_router(workspaces.workspaces_core_router, tags=["workspaces"])
core_router.include_router(workspaces.workspaces_shared_router, tags=["workspaces"])
core_router.include_router(migrations.migrations_core_router, tags=["migrations"])
Expand Down
16 changes: 16 additions & 0 deletions api_app/api/routes/operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import APIRouter, Depends

from db.repositories.operations import OperationRepository
from api.dependencies.database import get_repository
from models.schemas.operation import OperationInList
from resources import strings
from services.authentication import get_current_tre_user_or_tre_admin


operations_router = APIRouter(dependencies=[Depends(get_current_tre_user_or_tre_admin)])


@operations_router.get("/operations", response_model=OperationInList, name=strings.API_GET_MY_OPERATIONS)
async def get_my_operations(user=Depends(get_current_tre_user_or_tre_admin), operations_repo=Depends(get_repository(OperationRepository))) -> OperationInList:
operations = operations_repo.get_my_operations(user_id=user.id)
return OperationInList(operations=operations)
5 changes: 5 additions & 0 deletions api_app/db/repositories/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ def get_operation_by_id(self, operation_id: str) -> Operation:
raise EntityDoesNotExist
return parse_obj_as(Operation, operation[0])

def get_my_operations(self, user_id: str) -> List[Operation]:
query = self.operations_query() + f' c.user.id = "{user_id}" AND c.status IN ("{Status.AwaitingAction}", "{Status.InvokingAction}", "{Status.AwaitingDeployment}", "{Status.Deploying}", "{Status.AwaitingDeletion}", "{Status.Deleting}", "{Status.AwaitingUpdate}", "{Status.Updating}", "{Status.PipelineRunning}") ORDER BY c.createdWhen ASC'
operations = self.query(query=query)
return parse_obj_as(List[Operation], operations)

def get_operations_by_resource_id(self, resource_id: str) -> List[Operation]:
query = self.operations_query() + f' c.resourceId = "{resource_id}"'
operations = self.query(query=query)
Expand Down
1 change: 1 addition & 0 deletions api_app/resources/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
API_GET_HEALTH_STATUS = "Get health status"
API_MIGRATE_DATABASE = "Migrate documents in the database"

API_GET_MY_OPERATIONS = "Get Operations that the current user has initiated"
API_GET_ALL_WORKSPACES = "Get all workspaces"
API_GET_WORKSPACE_BY_ID = "Get workspace by Id"
API_CREATE_WORKSPACE = "Create a workspace"
Expand Down
37 changes: 16 additions & 21 deletions ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import { Workspace } from './models/workspace';
import { AppRolesContext } from './contexts/AppRolesContext';
import { WorkspaceContext } from './contexts/WorkspaceContext';
import { GenericErrorBoundary } from './components/shared/GenericErrorBoundary';
import { NotificationsContext } from './contexts/NotificationsContext';
import { Operation } from './models/operation';
import { ResourceUpdate } from './models/resource';
import { OperationsContext } from './contexts/OperationsContext';
import { completedStates, Operation } from './models/operation';
import { HttpMethod, ResultType, useAuthApiCall } from './hooks/useAuthApiCall';
import { ApiEndpoint } from './models/apiEndpoints';
import { CreateUpdateResource } from './components/shared/create-update-resource/CreateUpdateResource';
Expand All @@ -26,7 +25,6 @@ export const App: React.FunctionComponent = () => {
const [selectedWorkspace, setSelectedWorkspace] = useState({} as Workspace);
const [workspaceRoles, setWorkspaceRoles] = useState([] as Array<string>);
const [operations, setOperations] = useState([] as Array<Operation>);
const [resourceUpdates, setResourceUpdates] = useState([] as Array<ResourceUpdate>);

const [createFormOpen, setCreateFormOpen] = useState(false);
const [createFormResource, setCreateFormResource] = useState({ resourceType: ResourceType.Workspace } as CreateFormResource);
Expand Down Expand Up @@ -54,32 +52,29 @@ export const App: React.FunctionComponent = () => {
setCreateFormOpen(true);
}
}} >
<NotificationsContext.Provider value={{
<OperationsContext.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) {
if (i !== -1) {
stateOps.splice(i, 1, op);
} else {
stateOps.push(op);
}
});
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); }
dismissCompleted: () => {
let stateOps = [...operations];
stateOps.forEach((o:Operation) => {
if(completedStates.includes(o.status)) {
o.dismiss = true;
}
})
setOperations(stateOps);
}
}}>
<AppRolesContext.Provider value={{
roles: appRoles,
Expand All @@ -105,9 +100,9 @@ export const App: React.FunctionComponent = () => {
<Route path="/workspaces/:workspaceId//*" element={
<WorkspaceContext.Provider value={{
roles: workspaceRoles,
setRoles: (roles: Array<string>) => { console.warn("Workspace roles", roles); setWorkspaceRoles(roles) },
setRoles: (roles: Array<string>) => { console.info("Workspace roles", roles); setWorkspaceRoles(roles) },
workspace: selectedWorkspace,
setWorkspace: (w: Workspace) => { console.warn("Workspace set", w); setSelectedWorkspace(w) },
setWorkspace: (w: Workspace) => { console.info("Workspace set", w); setSelectedWorkspace(w) },
workspaceApplicationIdURI: selectedWorkspace.properties?.scope_id
}}>
<WorkspaceProvider />
Expand All @@ -121,7 +116,7 @@ export const App: React.FunctionComponent = () => {
</Stack.Item>
</Stack>
</AppRolesContext.Provider>
</NotificationsContext.Provider>
</OperationsContext.Provider>
</CreateUpdateResourceContext.Provider>
</MsalAuthenticationTemplate>
} />
Expand Down
4 changes: 2 additions & 2 deletions ui/app/src/components/shared/ConfirmDeleteResource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useContext, useState } from 'react';
import { Resource } from '../../models/resource';
import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { NotificationsContext } from '../../contexts/NotificationsContext';
import { OperationsContext } from '../../contexts/OperationsContext';
import { ResourceType } from '../../models/resourceType';

interface ConfirmDeleteProps {
Expand All @@ -16,7 +16,7 @@ export const ConfirmDeleteResource: React.FunctionComponent<ConfirmDeleteProps>
const apiCall = useAuthApiCall();
const [isSending, setIsSending] = useState(false);
const workspaceCtx = useContext(WorkspaceContext);
const opsCtx = useContext(NotificationsContext);
const opsCtx = useContext(OperationsContext);

const deleteProps = {
type: DialogType.normal,
Expand Down
4 changes: 2 additions & 2 deletions ui/app/src/components/shared/ConfirmDisableEnableResource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useContext, useState } from 'react';
import { Resource } from '../../models/resource';
import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCall';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { NotificationsContext } from '../../contexts/NotificationsContext';
import { OperationsContext } from '../../contexts/OperationsContext';
import { ResourceType } from '../../models/resourceType';

interface ConfirmDisableEnableResourceProps {
Expand All @@ -17,7 +17,7 @@ export const ConfirmDisableEnableResource: React.FunctionComponent<ConfirmDisabl
const apiCall = useAuthApiCall();
const [isSending, setIsSending] = useState(false);
const workspaceCtx = useContext(WorkspaceContext);
const opsCtx = useContext(NotificationsContext);
const opsCtx = useContext(OperationsContext);

const disableProps = {
type: DialogType.normal,
Expand Down
3 changes: 1 addition & 2 deletions ui/app/src/components/shared/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
const [loading] = useState(false);
const [showInfo, setShowInfo] = useState(false);
const workspaceCtx = useContext(WorkspaceContext);

const latestUpdate = useComponentManager(
props.resource,
(r: Resource) => { props.onUpdate(r) },
Expand Down Expand Up @@ -127,7 +126,7 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
}
</Stack.Item>
<Stack.Item style={{ paddingTop: 2, paddingLeft: 10 }}>
<StatusBadge resourceId={props.resource.id} status={latestUpdate.operation ? latestUpdate.operation?.status : props.resource.deploymentStatus} />
<StatusBadge resourceId={props.resource.id} status={latestUpdate.operation?.status ? latestUpdate.operation.status : props.resource.deploymentStatus} />
</Stack.Item>
</Stack>
</Stack.Item>
Expand Down
4 changes: 2 additions & 2 deletions ui/app/src/components/shared/ResourceContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CommandBar, IconButton, IContextualMenuItem, IContextualMenuProps } fro
import { RoleName, WorkspaceRoleName } from '../../models/roleNames';
import { SecuredByRole } from './SecuredByRole';
import { ResourceType } from '../../models/resourceType';
import { NotificationsContext } from '../../contexts/NotificationsContext';
import { OperationsContext } from '../../contexts/OperationsContext';
import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { ApiEndpoint } from '../../models/apiEndpoints';
Expand All @@ -30,7 +30,7 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
const [showDelete, setShowDelete] = useState(false);
const [resourceTemplate, setResourceTemplate] = useState({} as ResourceTemplate);
const createFormCtx = useContext(CreateUpdateResourceContext);
const opsWriteContext = useRef(useContext(NotificationsContext)); // useRef to avoid re-running a hook on context write
const opsWriteContext = useRef(useContext(OperationsContext)); // useRef to avoid re-running a hook on context write
const [parentResource, setParentResource] = useState({} as WorkspaceService | Workspace);

// get the resource template
Expand Down
2 changes: 1 addition & 1 deletion ui/app/src/components/shared/ResourceHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const ResourceHeader: React.FunctionComponent<ResourceHeaderProps> = (pro
{
(props.latestUpdate.operation || props.resource.deploymentStatus) &&
<Stack.Item align="center">
<StatusBadge resourceId={props.resource.id} status={props.latestUpdate.operation ? props.latestUpdate.operation?.status : props.resource.deploymentStatus} />
<StatusBadge resourceId={props.resource.id} status={props.latestUpdate.operation?.status ? props.latestUpdate.operation.status : props.resource.deploymentStatus} />
</Stack.Item>
}
</Stack>
Expand Down
4 changes: 1 addition & 3 deletions ui/app/src/components/shared/ResourcePropertyPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DefaultPalette, getTheme, IStackItemStyles, IStackStyles, Stack } from "@fluentui/react";
import { DefaultPalette, IStackItemStyles, IStackStyles, Stack } from "@fluentui/react";
import moment from "moment";
import React from "react";
import { Resource } from "../../models/resource";
Expand Down Expand Up @@ -94,5 +94,3 @@ export const ResourcePropertyPanel: React.FunctionComponent<ResourcePropertyPane
</> : <></>
);
};

const theme = getTheme();
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Operation } from '../../../models/operation';
import { ResourceType } from '../../../models/resourceType';
import { Workspace } from '../../../models/workspace';
import { WorkspaceService } from '../../../models/workspaceService';
import { NotificationsContext } from '../../../contexts/NotificationsContext';
import { OperationsContext } from '../../../contexts/OperationsContext';
import { ResourceForm } from './ResourceForm';
import { SelectTemplate } from './SelectTemplate';
import { getResourceFromResult, Resource } from '../../../models/resource';
Expand Down Expand Up @@ -41,7 +41,7 @@ export const CreateUpdateResource: React.FunctionComponent<CreateUpdateResourceP
const [page, setPage] = useState('selectTemplate' as keyof PageTitle);
const [selectedTemplate, setTemplate] = useState(props.updateResource?.templateName || '');
const [deployOperation, setDeployOperation] = useState({} as Operation);
const opsContext = useContext(NotificationsContext);
const opsContext = useContext(OperationsContext);
const navigate = useNavigate();
const apiCall = useAuthApiCall();

Expand Down
Loading

0 comments on commit 5083fbd

Please sign in to comment.