Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Resource Status + lifecycles #1972

Merged
merged 6 commits into from
Jun 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api_app/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.3.6"
__version__ = "0.3.7"
7 changes: 7 additions & 0 deletions api_app/models/domain/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def is_failure(self) -> bool:
Status.Failed
)

def is_action(self) -> bool:
return self.status in (
Status.ActionSucceeded,
Status.ActionFailed,
Status.InvokingAction
)


class Operation(AzureTREModel):
"""
Expand Down
3 changes: 2 additions & 1 deletion api_app/models/domain/resource.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import List
from typing import List, Optional
from pydantic import Field
from models.domain.azuretremodel import AzureTREModel
from models.domain.request_action import RequestAction
Expand Down Expand Up @@ -38,6 +38,7 @@ class Resource(AzureTREModel):
isActive: bool = True # When False, hides resource document from list views
isEnabled: bool = True # Must be set before a resource can be deleted
resourceType: ResourceType
deploymentStatus: Optional[str] = Field(title="Deployment Status", description="Overall deployment status of the resource")
etag: str = Field(title="_etag", description="eTag of the document", alias="_etag")
resourcePath: str = ""
resourceVersion: int = 0
Expand Down
5 changes: 5 additions & 0 deletions api_app/service_bus/deployment_status_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ async def update_status_in_database(resource_repo: ResourceRepository, operation
# save the operation
operations_repo.update_item(operation)

# copy the step status to the resource item, for convenience
resource = resource_repo.get_resource_by_id(uuid.UUID(step_to_update.resourceId))
resource.deploymentStatus = step_to_update.status
resource_repo.update_item(resource)

# if the step failed, or this queue message is an intermediary ("now deploying..."), return here.
if not step_to_update.is_success():
return True
Expand Down
21 changes: 21 additions & 0 deletions ui/app/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,24 @@ ul.tre-notifications-steps-list li {
input[readonly]{
background-color:#efefef;
}

.tre-badge{
border-radius:4px;
background-color: #efefef;
padding:2px 6px;
text-transform: capitalize;
display:inline-block;
font-size:12px;
}
.tre-badge-in-progress{
background-color: #ce7b00;
color: #fff;
}
.tre-badge-failed{
background-color: #990000;
color: #fff;
}
.tre-badge-success{
background-color: #006600;
color: #fff;
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const CreateUpdateResource: React.FunctionComponent<CreateUpdateResourceP
<Icon iconName="CloudAdd" className={creatingIconClass} />
<h1>{props.updateResource?.id ? 'Updating' : 'Creating'} {props.resourceType}...</h1>
<p>Check the notifications panel for deployment progress.</p>
<PrimaryButton text="Go to resource" onClick={() => navigate(deployOperation.resourcePath)} />
<PrimaryButton text="Go to resource" onClick={() => {navigate(deployOperation.resourcePath); props.onClose();}} />
</div>; break;
}

Expand Down
182 changes: 97 additions & 85 deletions ui/app/src/components/shared/CreateUpdateResource/ResourceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,102 +5,114 @@ import { HttpMethod, ResultType, useAuthApiCall } from "../../../hooks/useAuthAp
import Form from "@rjsf/fluent-ui";
import { Operation } from "../../../models/operation";
import { Resource } from "../../../models/resource";
import { ResourceType } from "../../../models/resourceType";

interface ResourceFormProps {
templateName: string,
templatePath: string,
resourcePath: string,
updateResource?: Resource,
onCreateResource: (operation: Operation) => void,
workspaceClientId?: string
templateName: string,
templatePath: string,
resourcePath: string,
updateResource?: Resource,
onCreateResource: (operation: Operation) => void,
workspaceClientId?: string
}

export const ResourceForm: React.FunctionComponent<ResourceFormProps> = (props: ResourceFormProps) => {
const [template, setTemplate] = useState<any | null>(null);
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(LoadingState.Loading as LoadingState);
const [deployError, setDeployError] = useState(false);
const apiCall = useAuthApiCall();
const [template, setTemplate] = useState<any | null>(null);
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(LoadingState.Loading as LoadingState);
const [deployError, setDeployError] = useState(false);
const [sendingData, setSendingData] = useState(false);
const apiCall = useAuthApiCall();

useEffect(() => {
const getFullTemplate = async () => {
try {
// Get the full resource template containing the required parameters
const templateResponse = await apiCall(props.updateResource ? `${props.templatePath}?is_update=true` : props.templatePath, HttpMethod.Get);
useEffect(() => {
const getFullTemplate = async () => {
try {
// Get the full resource template containing the required parameters
const templateResponse = await apiCall(props.updateResource ? `${props.templatePath}?is_update=true` : props.templatePath, HttpMethod.Get);

// if it's an update, populate the form with the props that are available in the template
if (props.updateResource) {
let d:any = {};
for(let prop in templateResponse.properties){
d[prop] = props.updateResource?.properties[prop];
}
setFormData(d);
}

setTemplate(templateResponse);
setLoading(LoadingState.Ok);
} catch {
setLoading(LoadingState.Error);
}
};

// Fetch full resource template only if not in state
if (!template) {
getFullTemplate();
}
}, [apiCall, props.templatePath, template, props.updateResource]);

const createUpdateResource = async (formData: any) => {
setDeployError(false);
let response;
if(props.updateResource) {
// only send the properties we're allowed to send
let d:any = {}
for(let prop in template.properties) {
if (!template.properties[prop].readOnly) d[prop] = formData[prop];
// if it's an update, populate the form with the props that are available in the template
if (props.updateResource) {
let d: any = {};
for (let prop in templateResponse.properties) {
d[prop] = props.updateResource?.properties[prop];
}
console.log("patching d", d);
response = await apiCall(props.updateResource.resourcePath, HttpMethod.Patch, props.workspaceClientId, { properties: d }, ResultType.JSON, undefined, undefined, props.updateResource._etag);
} else {
const resource = { templateName: props.templateName, properties: formData };
console.log(resource);
response = await apiCall(props.resourcePath, HttpMethod.Post, props.workspaceClientId, resource, ResultType.JSON);
setFormData(d);
}

if (response) {
props.onCreateResource(response.operation);
} else {
setDeployError(true);
}
setTemplate(templateResponse);
setLoading(LoadingState.Ok);
} catch {
setLoading(LoadingState.Error);
}
};

// Fetch full resource template only if not in state
if (!template) {
getFullTemplate();
}
}, [apiCall, props.templatePath, template, props.updateResource]);

switch (loading) {
case LoadingState.Ok:
return (
template ? <div style={{ marginTop: 20 }}>
<Form schema={template} formData={formData} onSubmit={(e: any) => createUpdateResource(e.formData)}/>
{
deployError ? <MessageBar messageBarType={MessageBarType.error}>
<p>The API returned an error. Check the console for details or retry.</p>
</MessageBar> : null
}
</div> : null
)
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving template</h3>
<p>There was an error retrieving the full resource template. Please see the browser console for details.</p>
</MessageBar>
);
default:
return (
<div style={{ marginTop: 20 }}>
<Spinner label="Loading template" ariaLive="assertive" labelPosition="top" size={SpinnerSize.large} />
</div>
)
const createUpdateResource = async (formData: any) => {
setDeployError(false);
setSendingData(true);
let response;
if (props.updateResource) {
// only send the properties we're allowed to send
let d: any = {}
for (let prop in template.properties) {
if (!template.properties[prop].readOnly) d[prop] = formData[prop];
}
console.log("patching resource", d);
let wsAuth = props.updateResource.resourceType === ResourceType.WorkspaceService || props.updateResource.resourceType === ResourceType.UserResource;
response = await apiCall(props.updateResource.resourcePath, HttpMethod.Patch, wsAuth ? props.workspaceClientId : undefined, { properties: d }, ResultType.JSON, undefined, undefined, props.updateResource._etag);
} else {
const resource = { templateName: props.templateName, properties: formData };
console.log(resource);
response = await apiCall(props.resourcePath, HttpMethod.Post, props.workspaceClientId, resource, ResultType.JSON);
}

setSendingData(false);
if (response) {
props.onCreateResource(response.operation);
} else {
setDeployError(true);
}
}

switch (loading) {
case LoadingState.Ok:
return (
template &&
<div style={{ marginTop: 20 }}>
{
sendingData ?
<Spinner label="Sending request" ariaLive="assertive" labelPosition="bottom" size={SpinnerSize.large} />
:
<Form schema={template} formData={formData} onSubmit={(e: any) => createUpdateResource(e.formData)} />
}
{
deployError &&
<MessageBar messageBarType={MessageBarType.error}>
<p>The API returned an error. Check the console for details or retry.</p>
</MessageBar>
}
</div>
)
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving template</h3>
<p>There was an error retrieving the full resource template. Please see the browser console for details.</p>
</MessageBar>
);
default:
return (
<div style={{ marginTop: 20 }}>
<Spinner label="Loading template" ariaLive="assertive" labelPosition="top" size={SpinnerSize.large} />
</div>
)
}
}
30 changes: 19 additions & 11 deletions ui/app/src/components/shared/ResourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Link } from 'react-router-dom';
import moment from 'moment';
import { ResourceContextMenu } from './ResourceContextMenu';
import { useComponentManager } from '../../hooks/useComponentManager';
import { StatusBadge } from './StatusBadge';

interface ResourceCardProps {
resource: Resource,
Expand All @@ -17,10 +18,10 @@ interface ResourceCardProps {
export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props: ResourceCardProps) => {
const [loading] = useState(false);
const [showInfo, setShowInfo] = useState(false);
const componentAction = useComponentManager(
const latestUpdate = useComponentManager(
props.resource,
(r: Resource) => {props.onUpdate(r)},
(r: Resource) => {props.onDelete(r)}
(r: Resource) => { props.onUpdate(r) },
(r: Resource) => { props.onDelete(r) }
);

let connectUri = props.resource.properties && props.resource.properties.connection_uri;
Expand Down Expand Up @@ -57,7 +58,7 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
<Stack.Item>
<ResourceContextMenu
resource={props.resource}
componentAction={componentAction} />
componentAction={latestUpdate.componentAction} />
</Stack.Item>
</Stack>
</Stack.Item>
Expand All @@ -72,12 +73,19 @@ export const ResourceCard: React.FunctionComponent<ResourceCardProps> = (props:
</Stack.Item>
}
<Stack.Item style={footerStyles}>
{
componentAction === ComponentAction.Lock &&
<ProgressIndicator
barHeight={4}
description='Resource is locked for changes whilst it updates.' />
}
<Stack horizontal>
<Stack.Item grow={1}>
{
latestUpdate.componentAction === ComponentAction.Lock &&
<ProgressIndicator
barHeight={4}
description='Resource is locked for changes whilst it updates.' />
}
</Stack.Item>
<Stack.Item style={{paddingTop: 5, paddingLeft:10}}>
<StatusBadge status={latestUpdate.operation ? latestUpdate.operation?.status : props.resource.deploymentStatus} />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
}
Expand Down Expand Up @@ -120,7 +128,7 @@ const cardStyles: React.CSSProperties = {
width: '100%',
borderRadius: '2px',
border: '1px #ccc solid',
// boxShadow: '1px 0px 4px 0px #dddddd'
// boxShadow: '1px 0px 4px 0px #dddddd'
}

const headerStyles: React.CSSProperties = {
Expand Down
3 changes: 2 additions & 1 deletion ui/app/src/components/shared/ResourceContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ConfirmDisableEnableResource } from './ConfirmDisableEnableResource';
import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResourceContext';
import { Workspace } from '../../models/workspace';
import { WorkspaceService } from '../../models/workspaceService';
import { successStates } from '../../models/operation';

interface ResourceContextMenuProps {
resource: Resource,
Expand Down Expand Up @@ -91,7 +92,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, subMenuProps: { items: customActions } });
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 } });
}

switch (props.resource.resourceType) {
Expand Down
Loading