Skip to content

Commit

Permalink
UI Snagging + Migration (#1996)
Browse files Browse the repository at this point in the history
* initial swa deploy

* fixed create form state reset (again)

* added required roles to create buttons

* API migration for deploymentStatus field

* include num rows in migration
  • Loading branch information
damoodamoo authored Jun 8, 2022
1 parent 8a6172d commit 5523113
Show file tree
Hide file tree
Showing 14 changed files with 116 additions and 71 deletions.
10 changes: 9 additions & 1 deletion api_app/api/routes/migrations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging

from fastapi import APIRouter, Depends, HTTPException, status
from db.migrations.resources import ResourceMigration
from db.repositories.operations import OperationRepository
from services.authentication import get_current_admin_user
from resources import strings
from api.dependencies.database import get_repository
Expand All @@ -19,8 +21,10 @@
response_model=MigrationOutList,
dependencies=[Depends(get_current_admin_user)])
async def migrate_database(resources_repo=Depends(get_repository(ResourceRepository)),
operations_repo=Depends(get_repository(OperationRepository)),
shared_services_migration=Depends(get_repository(SharedServiceMigration)),
workspace_migration=Depends(get_repository(WorkspaceMigration))):
workspace_migration=Depends(get_repository(WorkspaceMigration)),
resource_migration=Depends(get_repository(ResourceMigration))):
try:
migrations = list()
logging.info("PR 1030.")
Expand All @@ -43,6 +47,10 @@ async def migrate_database(resources_repo=Depends(get_repository(ResourceReposit
migration_status = "Executed" if workspace_migration.moveAuthInformationToProperties() else "Skipped"
migrations.append(Migration(issueNumber="PR 1726", status=migration_status))

logging.info("#1406 - Extra field to support UI")
num_rows = resource_migration.add_deployment_status_field(operations_repo)
migrations.append(Migration(issueNumber="1406", status=f'Updated {num_rows} resource objects'))

return MigrationOutList(migrations=migrations)
except Exception as e:
logging.error(f"Failed to migrate database: {e}")
Expand Down
4 changes: 2 additions & 2 deletions api_app/api/routes/shared_service_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ async def get_shared_service_templates(template_repo=Depends(get_repository(Reso


@shared_service_templates_core_router.get("/shared-service-templates/{shared_service_template_name}", response_model=SharedServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME, dependencies=[Depends(get_current_tre_user_or_tre_admin)])
async def get_current_shared_service_template_by_name(shared_service_template_name: str, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> SharedServiceTemplateInResponse:
async def get_current_shared_service_template_by_name(shared_service_template_name: str, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> SharedServiceTemplateInResponse:
try:
template = get_current_template_by_name(shared_service_template_name, template_repo, ResourceType.SharedService)
template = get_current_template_by_name(shared_service_template_name, template_repo, ResourceType.SharedService, is_update=is_update)
return parse_obj_as(SharedServiceTemplateInResponse, template)
except EntityDoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.SHARED_SERVICE_TEMPLATE_DOES_NOT_EXIST)
Expand Down
26 changes: 26 additions & 0 deletions api_app/db/migrations/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging
import uuid
from azure.cosmos import CosmosClient
from db.errors import EntityDoesNotExist
from db.repositories.operations import OperationRepository
from db.repositories.resources import ResourceRepository


class ResourceMigration(ResourceRepository):
def __init__(self, client: CosmosClient):
super().__init__(client)

def add_deployment_status_field(self, operations_repository: OperationRepository) -> int:

i = 0
for op in operations_repository.query("SELECT * from c ORDER BY c._ts ASC"):
try:
resource = self.get_resource_by_id(uuid.UUID(op['resourceId']))
resource.deploymentStatus = op['status']
self.update_item(resource)
i = i + 1
except EntityDoesNotExist:
logging.info(f'Resource Id {op["resourceId"]} not found')
# ignore errors and try the next one

return i
9 changes: 4 additions & 5 deletions api_app/db/repositories/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic import UUID4, parse_obj_as
import copy
from models.domain.authentication import User

from azure.cosmos.exceptions import CosmosResourceNotFoundError
from core import config
from db.errors import EntityDoesNotExist
from db.repositories.base import BaseRepository
Expand Down Expand Up @@ -55,12 +55,11 @@ def get_resource_dict_by_id(self, resource_id: UUID4) -> dict:
return resources[0]

def get_resource_by_id(self, resource_id: UUID4) -> Resource:
query = self._active_resources_by_id_query(str(resource_id))
resources = self.query(query=query)
if not resources:
try:
resource = self.read_item_by_id(str(resource_id))
except CosmosResourceNotFoundError:
raise EntityDoesNotExist

resource = resources[0]
if resource["resourceType"] == ResourceType.SharedService:
return parse_obj_as(SharedService, resource)
if resource["resourceType"] == ResourceType.Workspace:
Expand Down
7 changes: 5 additions & 2 deletions api_app/tests_ma/test_api/test_routes/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,18 @@ def _prepare(self, app, admin_user):

# [POST] /migrations/
@ patch("api.routes.migrations.logging.info")
@ patch("api.routes.migrations.OperationRepository")
@ patch("api.routes.migrations.ResourceMigration.add_deployment_status_field")
@ patch("api.routes.migrations.ResourceRepository.rename_field_name")
@ patch("api.routes.migrations.SharedServiceMigration.deleteDuplicatedSharedServices")
@ patch("api.routes.migrations.WorkspaceMigration.moveAuthInformationToProperties")
async def test_post_migrations_returns_202_on_successful(self, workspace_migration, shared_services_migration, resources_repo, logging, client, app):
async def test_post_migrations_returns_202_on_successful(self, workspace_migration, shared_services_migration, rename_field, add_deployment_field, _, logging, client, app):
response = await client.post(app.url_path_for(strings.API_MIGRATE_DATABASE))

shared_services_migration.assert_called_once()
workspace_migration.assert_called_once()
resources_repo.assert_called()
rename_field.assert_called()
add_deployment_field.assert_called()
logging.assert_called()
assert response.status_code == status.HTTP_202_ACCEPTED

Expand Down
2 changes: 1 addition & 1 deletion ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const App: React.FunctionComponent = () => {
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect}>
<CreateUpdateResourceContext.Provider value={{
openCreateForm: (createFormResource: CreateFormResource) => {
setCreateFormResource(JSON.parse(JSON.stringify(createFormResource)));
setCreateFormResource(createFormResource);
setCreateFormOpen(true);
}
}} >
Expand Down
17 changes: 11 additions & 6 deletions ui/app/src/components/root/RootDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Resource } from '../../models/resource';
import { PrimaryButton, Stack } from '@fluentui/react';
import { ResourceType } from '../../models/resourceType';
import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
import { RoleName } from '../../models/roleNames';
import { SecuredByRole } from '../shared/SecuredByRole';

interface RootDashboardProps {
selectWorkspace?: (workspace: Workspace) => void,
Expand All @@ -24,12 +26,15 @@ export const RootDashboard: React.FunctionComponent<RootDashboardProps> = (props
<Stack.Item>
<Stack horizontal horizontalAlign="space-between">
<Stack.Item><h1>Workspaces</h1></Stack.Item>
<Stack.Item style={{ width: 200, textAlign: 'right' }}><PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.Workspace,
onAdd: (r: Resource) => props.addWorkspace(r as Workspace)
})
}} />
<Stack.Item style={{ width: 200, textAlign: 'right' }}>
<SecuredByRole allowedRoles={[RoleName.TREAdmin]} workspaceAuth={false} element={
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.Workspace,
onAdd: (r: Resource) => props.addWorkspace(r as Workspace)
})
}} />
} />
</Stack.Item>
</Stack>
</Stack.Item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,8 @@ export const CreateUpdateResource: React.FunctionComponent<CreateUpdateResourceP
}

!props.isOpen && clearState();
}, [props.isOpen]);

useEffect(() => {
props.updateResource && props.updateResource.templateName && selectTemplate(props.updateResource.templateName);
}, [props.updateResource])
props.isOpen && props.updateResource && props.updateResource.templateName && selectTemplate(props.updateResource.templateName);
}, [props.isOpen, props.updateResource]);

// Render a panel title depending on sub-page
const pageTitles: PageTitle = {
Expand Down
3 changes: 2 additions & 1 deletion ui/app/src/components/shared/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import moment from 'moment';
import { ResourceContextMenu } from './ResourceContextMenu';
import { useComponentManager } from '../../hooks/useComponentManager';
import { StatusBadge } from './StatusBadge';
import { successStates } from '../../models/operation';

interface ResourceCardProps {
resource: Resource,
Expand Down Expand Up @@ -69,7 +70,7 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
{
connectUri &&
<Stack.Item style={connectStyles}>
<PrimaryButton onClick={() => window.open(connectUri)} disabled={!props.resource.isEnabled} title={!props.resource.isEnabled ? 'Enable resource to connect' : 'Connect to resource'}>Connect</PrimaryButton>
<PrimaryButton onClick={() => window.open(connectUri)} disabled={!props.resource.isEnabled || successStates.indexOf(props.resource.deploymentStatus) === -1} title={!props.resource.isEnabled || successStates.indexOf(props.resource.deploymentStatus) === -1 ? 'Resource must be enabled and successfully deployed to connect' : 'Connect to resource'}>Connect</PrimaryButton>
</Stack.Item>
}
<Stack.Item style={footerStyles}>
Expand Down
20 changes: 18 additions & 2 deletions ui/app/src/components/shared/ResourceContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
action && action.operation && opsWriteContext.current.addOperations([action.operation]);
}

const shouldDisable = () => {
return props.componentAction === ComponentAction.Lock || successStates.indexOf(props.resource.deploymentStatus) === -1 || !props.resource.isEnabled;
}

// context menu
let menuItems: Array<any> = [];
let roles: Array<string> = [];
Expand All @@ -81,9 +85,21 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
workspaceClientId: workspaceCtx.workspaceClientId,
}), disabled:(props.componentAction === ComponentAction.Lock) },
{ key: 'disable', text: props.resource.isEnabled ? 'Disable' : 'Enable', iconProps: { iconName: props.resource.isEnabled ? 'CirclePause' : 'PlayResume' }, onClick: () => setShowDisable(true), disabled:(props.componentAction === ComponentAction.Lock) },
{ 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 || props.componentAction === ComponentAction.Lock) },
{ key: 'delete', text: 'Delete', title: props.resource.isEnabled ? 'Resource must be disabled before deleting' : 'Delete this resource', iconProps: { iconName: 'Delete' }, onClick: () => setShowDelete(true), disabled: (props.resource.isEnabled || props.componentAction === ComponentAction.Lock) },
];

// add 'connect' button if we have a URL to connect to
if (props.resource.properties.connection_uri) {
menuItems.push({
key: 'connect',
text: 'Connect',
title: shouldDisable() ? 'Resource must be deployed and enabled to connect' : 'Connect to resource',
iconProps: { iconName: 'PlugConnected' },
onClick: () => { window.open(props.resource.properties.connection_uri, '_blank') },
disabled: shouldDisable()
})
}

// add custom actions if we have any
if (resourceTemplate && resourceTemplate.customActions && resourceTemplate.customActions.length > 0) {
let customActions: Array<IContextualMenuItem> = [];
Expand All @@ -92,7 +108,7 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
{ 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' }, disabled:props.componentAction === ComponentAction.Lock || successStates.indexOf(props.resource.deploymentStatus) === -1 || !props.resource.isEnabled, subMenuProps: { items: customActions } });
menuItems.push({ key: 'custom-actions', text: 'Actions', title: shouldDisable() ? 'Resource must be deployed and enabled to perform actions': 'Custom Actions', iconProps: { iconName: 'Asterisk' }, disabled:shouldDisable(), subMenuProps: { items: customActions } });
}

switch (props.resource.resourceType) {
Expand Down
16 changes: 10 additions & 6 deletions ui/app/src/components/shared/SharedServices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { SharedService } from '../../models/sharedService';
import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
import { ApiEndpoint } from '../../models/apiEndpoints';
import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
import { RoleName } from '../../models/roleNames';
import { SecuredByRole } from './SecuredByRole';

export const SharedServices: React.FunctionComponent = () => {
const createFormCtx = useContext(CreateUpdateResourceContext);
Expand Down Expand Up @@ -47,12 +49,14 @@ export const SharedServices: React.FunctionComponent = () => {
<Stack.Item>
<Stack horizontal horizontalAlign="space-between">
<h1>Shared Services</h1>
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.SharedService,
onAdd: (r: Resource) => addSharedService(r as SharedService)
})
}} />
<SecuredByRole allowedRoles={[RoleName.TREAdmin]} workspaceAuth={false} element={
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.SharedService,
onAdd: (r: Resource) => addSharedService(r as SharedService)
})
}} />
} />
</Stack>
</Stack.Item>
<Stack.Item>
Expand Down
24 changes: 1 addition & 23 deletions ui/app/src/components/workspaces/WorkspaceLeftNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@ import { Nav, INavLinkGroup, INavStyles } from '@fluentui/react/lib/Nav';
import { useNavigate } from 'react-router-dom';
import { ApiEndpoint } from '../../models/apiEndpoints';
import { WorkspaceService } from '../../models/workspaceService';
import { ResourceType } from '../../models/resourceType';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { Resource } from '../../models/resource';
import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
import { successStates } from '../../models/operation';

// TODO:
// - we lose the selected styling when navigating into a user resource. This may not matter as the user resource page might die away.
// - loading placeholders / error content(?)
// - active item is sometimes lost

interface WorkspaceLeftNavProps {
workspaceServices: Array<WorkspaceService>,
Expand All @@ -24,7 +19,6 @@ export const WorkspaceLeftNav: React.FunctionComponent<WorkspaceLeftNavProps> =
const emptyLinks: INavLinkGroup[] = [{links:[]}];
const [serviceLinks, setServiceLinks] = useState(emptyLinks);
const workspaceCtx = useContext(WorkspaceContext);
const createFormCtx = useContext(CreateUpdateResourceContext);

useEffect(() => {
const getWorkspaceServices = async () => {
Expand All @@ -40,14 +34,6 @@ export const WorkspaceLeftNav: React.FunctionComponent<WorkspaceLeftNavProps> =
});
});

// Add Create New link at the bottom of services links
serviceLinkArray.push({
name: "Create new",
icon: "Add",
key: "create",
disabled: successStates.indexOf(workspaceCtx.workspace.deploymentStatus) === -1 || !workspaceCtx.workspace.isEnabled
});

const seviceNavLinks: INavLinkGroup[] = [
{
links: [
Expand Down Expand Up @@ -78,14 +64,6 @@ export const WorkspaceLeftNav: React.FunctionComponent<WorkspaceLeftNavProps> =
<Nav
onLinkClick={(e, item) => {
e?.preventDefault();
if (item?.key === "create") {
createFormCtx.openCreateForm({
resourceType: ResourceType.WorkspaceService,
resourceParent: workspaceCtx.workspace,
onAdd: (r: Resource) => props.addWorkspaceService(r as WorkspaceService),
workspaceClientId: workspaceCtx.workspaceClientId
})
};
if (!item || !item.url) return;
let selectedService = props.workspaceServices.find((w) => item.key?.indexOf(w.id.toString()) !== -1);
if (selectedService) {
Expand Down
22 changes: 13 additions & 9 deletions ui/app/src/components/workspaces/WorkspaceServiceItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResource
import { successStates } from '../../models/operation';
import { UserResourceItem } from './UserResourceItem';
import { ResourceBody } from '../shared/ResourceBody';
import { SecuredByRole } from '../shared/SecuredByRole';
import { WorkspaceRoleName } from '../../models/roleNames';

interface WorkspaceServiceItemProps {
workspaceService?: WorkspaceService,
Expand Down Expand Up @@ -105,15 +107,17 @@ export const WorkspaceServiceItem: React.FunctionComponent<WorkspaceServiceItemP
<Stack.Item>
<Stack horizontal horizontalAlign="space-between">
<h1>User Resources</h1>
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" disabled={!workspaceService.isEnabled || latestUpdate.componentAction === ComponentAction.Lock || successStates.indexOf(workspaceService.deploymentStatus) === -1} title={!workspaceService.isEnabled ? 'Service must be enabled first' : 'Create a User Resource'}
onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.UserResource,
resourceParent: workspaceService,
onAdd: (r: Resource) => addUserResource(r as UserResource),
workspaceClientId: workspaceCtx.workspaceClientId
})
}} />
<SecuredByRole allowedRoles={[WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]} workspaceAuth={true} element={
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" disabled={!workspaceService.isEnabled || latestUpdate.componentAction === ComponentAction.Lock || successStates.indexOf(workspaceService.deploymentStatus) === -1} title={!workspaceService.isEnabled ? 'Service must be enabled first' : 'Create a User Resource'}
onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.UserResource,
resourceParent: workspaceService,
onAdd: (r: Resource) => addUserResource(r as UserResource),
workspaceClientId: workspaceCtx.workspaceClientId
})
}} />
} />
</Stack>
</Stack.Item>
<Stack.Item>
Expand Down
Loading

0 comments on commit 5523113

Please sign in to comment.