Lazy load phase tree in airshipui

Improves performance of the phase component in the UI by
loading initial phase list, and only loading individual phase
(kustomize) file trees when needed.

Change-Id: I00b57021c182ff482ed0f0d341025e40d4b8ba3f
This commit is contained in:
Matthew Fuller 2020-11-17 00:04:32 +00:00
parent 1de746fa77
commit d4d8e0174a
8 changed files with 95 additions and 28 deletions

View File

@ -39,6 +39,11 @@
flex-direction: row;
}
.unloaded-phase {
display: flex;
flex-direction: row;
}
.docless-phase-btn {
padding-left: 20px;
padding-right: 0px;

View File

@ -20,9 +20,31 @@
<button *ngIf="node.hasError" mat-icon-button>
<mat-icon class="error-icon" svgIcon="error"></mat-icon> {{node.name}}
</button>
<div *ngIf="node.isPhaseNode && !node.hasError" class="docless-phase">
<div *ngIf="node.isPhaseNode && !node.hasError && node.hasDocuments" class="unloaded-phase">
<button mat-button (click)="loadPhase(node)">
<mat-icon class="mat-icon-rtl-mirror">chevron_right</mat-icon>{{node.name}}
</button>
<button class="menu-button" *ngIf="!node.running" mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon class="grey-icon" svgIcon="settings"></mat-icon>
</button>
<mat-spinner *ngIf="node.running" class="spinner" [diameter]="20"></mat-spinner>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="getPhase(node.phaseId)">
<mat-icon class="grey-icon" svgIcon="open_in_new"></mat-icon>
<span>View</span>
</button>
<button mat-menu-item (click)="validatePhase(node.phaseId)">
<mat-icon>check_circle_icon</mat-icon>
<span>Validate</span>
</button>
<button mat-menu-item (click)="confirmRunPhase(node)">
<mat-icon>play_circle_outline</mat-icon>
<span>Run</span>
</button>
</mat-menu>
</div>
<div *ngIf="node.isPhaseNode && !node.hasError && !node.hasDocuments" class="docless-phase">
<button mat-button class="docless-phase-btn">{{node.name}}</button>
<span class="spacer"></span>
<button class="menu-button" *ngIf="!node.running" mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon class="grey-icon" svgIcon="settings"></mat-icon>
</button>

View File

@ -44,6 +44,7 @@ export class PhaseComponent implements WsReceiver {
activeLink = 'overview';
phaseTree: KustomNode[] = [];
clickedNode: KustomNode;
treeControl = new NestedTreeControl<KustomNode>(node => node.children);
dataSource = new MatTreeNestedDataSource<KustomNode>();
@ -84,6 +85,9 @@ export class PhaseComponent implements WsReceiver {
case WsConstants.GET_PHASE:
this.handleGetPhase(message);
break;
case WsConstants.GET_PHASE_SOURCE_FILES:
this.handleGetPhaseSourceFiles(message);
break;
case WsConstants.GET_YAML:
this.handleGetYaml(message);
break;
@ -109,6 +113,13 @@ export class PhaseComponent implements WsReceiver {
}
}
handleGetPhaseSourceFiles(message: WsMessage): void {
this.clickedNode.running = false;
const data: KustomNode[] = [];
Object.assign(data, message.data);
this.updateTree(data);
}
handleValidatePhase(message: WsMessage): void {
this.websocketService.printIfToast(message);
}
@ -288,4 +299,30 @@ export class PhaseComponent implements WsReceiver {
}
}
}
getPhaseSourceFiles(node: KustomNode): void {
const msg = new WsMessage(this.type, this.component, WsConstants.GET_PHASE_SOURCE_FILES);
msg.id = JSON.stringify(node.phaseId);
this.websocketService.sendMessage(msg);
}
refreshTreeData(): void {
const tmpdata = this.dataSource.data;
this.dataSource.data = null;
this.dataSource.data = tmpdata;
}
loadPhase(node: KustomNode): void {
this.clickedNode = node;
this.clickedNode.running = true;
this.getPhaseSourceFiles(this.clickedNode);
}
updateTree(data: KustomNode[]): void {
if (this.clickedNode !== undefined) {
this.clickedNode.children = data;
this.clickedNode.hasDocuments = false;
this.refreshTreeData();
}
}
}

View File

@ -16,7 +16,7 @@ export class KustomNode {
id: string;
phaseId: { Name: string, Namespace: string};
name: string;
canLoadChildren: boolean;
hasDocuments: boolean;
children: KustomNode[];
isPhaseNode: boolean;
running: boolean;

View File

@ -81,6 +81,7 @@ export class WsConstants {
public static readonly GET_DOCUMENT_BY_SELECTOR = 'getDocumentsBySelector';
public static readonly GET_EXECUTOR_DOC = 'getExecutorDoc';
public static readonly GET_PHASE = 'getPhase';
public static readonly GET_PHASE_SOURCE_FILES = 'getPhaseSourceFiles';
public static readonly GET_PHASE_TREE = 'getPhaseTree';
public static readonly GET_TARGET = 'getTarget';
public static readonly GET_YAML = 'getYaml';

View File

@ -178,7 +178,7 @@ const (
GetYaml WsSubComponentType = "getYaml"
GetRendered WsSubComponentType = "getRendered"
GetPhaseTree WsSubComponentType = "getPhaseTree"
GetPhaseSourceFiles WsSubComponentType = "getPhaseSource"
GetPhaseSourceFiles WsSubComponentType = "getPhaseSourceFiles"
GetDocumentsBySelector WsSubComponentType = "getDocumentsBySelector"
GetPhase WsSubComponentType = "getPhase"
GetExecutorDoc WsSubComponentType = "getExecutorDoc"

View File

@ -89,6 +89,8 @@ func HandlePhaseRequest(user *string, request configs.WsMessage) configs.WsMessa
s := "rendered"
message = &s
response.Name, response.YAML, err = client.GetExecutorDoc(id)
case configs.GetPhaseSourceFiles:
response.Data, err = client.getPhaseSource(id)
default:
err = fmt.Errorf("Subcomponent %s not found", request.SubComponent)
}
@ -103,6 +105,17 @@ func HandlePhaseRequest(user *string, request configs.WsMessage) configs.WsMessa
return response
}
func (c *Client) getPhaseSource(id string) ([]KustomNode, error) {
phaseID := ifc.ID{}
err := json.Unmarshal([]byte(id), &phaseID)
if err != nil {
return nil, err
}
return c.GetPhaseSourceFiles(phaseID)
}
// this helper function will likely disappear once a clear workflow for
// phase validation takes shape in UI. For now, it simply returns a
// string message to be displayed as a toast in frontend client

View File

@ -74,22 +74,10 @@ func (client *Client) GetPhaseTree() ([]KustomNode, error) {
PhaseID: ifc.ID{Name: p.Name, Namespace: p.Namespace},
Name: fmt.Sprintf("Phase: %s", p.Name),
IsPhaseNode: true,
HasDocuments: p.Config.DocumentEntryPoint != "",
Children: []KustomNode{},
}
// some phases don't have any associated documents, so don't look
// for children unless a DocumentEntryPoint has been specified
if p.Config.DocumentEntryPoint != "" {
children, err := client.GetPhaseSourceFiles(pNode.PhaseID)
if err != nil {
// TODO(mfuller): push an error to UI so it can be handled by
// toastr service, pending refactor of webservice and configs pkgs
log.Errorf("Error building tree for phase '%s': %s", p.Name, err)
pNode.HasError = true
} else {
pNode.Children = children
}
}
nodes = append(nodes, pNode)
}
@ -181,11 +169,12 @@ func (client *Client) GetPhaseSourceFiles(id ifc.ID) ([]KustomNode, error) {
// KustomNode structure to represent the kustomization tree for a given phase
// bundle to be consumed by the UI frontend
type KustomNode struct {
ID string `json:"id"` // UUID for backend node index
ID string `json:"id"`
PhaseID ifc.ID `json:"phaseId"`
Name string `json:"name"` // name used for display purposes (cli, ui)
Name string `json:"name"`
IsPhaseNode bool `json:"isPhaseNode"`
HasError bool `json:"hasError"`
HasDocuments bool `json:"hasDocuments"`
Children []KustomNode `json:"children"`
}