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:
parent
1de746fa77
commit
d4d8e0174a
@ -39,6 +39,11 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unloaded-phase {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
.docless-phase-btn {
|
.docless-phase-btn {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
|
@ -20,9 +20,31 @@
|
|||||||
<button *ngIf="node.hasError" mat-icon-button>
|
<button *ngIf="node.hasError" mat-icon-button>
|
||||||
<mat-icon class="error-icon" svgIcon="error"></mat-icon> {{node.name}}
|
<mat-icon class="error-icon" svgIcon="error"></mat-icon> {{node.name}}
|
||||||
</button>
|
</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>
|
<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">
|
<button class="menu-button" *ngIf="!node.running" mat-icon-button [matMenuTriggerFor]="menu">
|
||||||
<mat-icon class="grey-icon" svgIcon="settings"></mat-icon>
|
<mat-icon class="grey-icon" svgIcon="settings"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
@ -44,6 +44,7 @@ export class PhaseComponent implements WsReceiver {
|
|||||||
activeLink = 'overview';
|
activeLink = 'overview';
|
||||||
|
|
||||||
phaseTree: KustomNode[] = [];
|
phaseTree: KustomNode[] = [];
|
||||||
|
clickedNode: KustomNode;
|
||||||
|
|
||||||
treeControl = new NestedTreeControl<KustomNode>(node => node.children);
|
treeControl = new NestedTreeControl<KustomNode>(node => node.children);
|
||||||
dataSource = new MatTreeNestedDataSource<KustomNode>();
|
dataSource = new MatTreeNestedDataSource<KustomNode>();
|
||||||
@ -84,6 +85,9 @@ export class PhaseComponent implements WsReceiver {
|
|||||||
case WsConstants.GET_PHASE:
|
case WsConstants.GET_PHASE:
|
||||||
this.handleGetPhase(message);
|
this.handleGetPhase(message);
|
||||||
break;
|
break;
|
||||||
|
case WsConstants.GET_PHASE_SOURCE_FILES:
|
||||||
|
this.handleGetPhaseSourceFiles(message);
|
||||||
|
break;
|
||||||
case WsConstants.GET_YAML:
|
case WsConstants.GET_YAML:
|
||||||
this.handleGetYaml(message);
|
this.handleGetYaml(message);
|
||||||
break;
|
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 {
|
handleValidatePhase(message: WsMessage): void {
|
||||||
this.websocketService.printIfToast(message);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ export class KustomNode {
|
|||||||
id: string;
|
id: string;
|
||||||
phaseId: { Name: string, Namespace: string};
|
phaseId: { Name: string, Namespace: string};
|
||||||
name: string;
|
name: string;
|
||||||
canLoadChildren: boolean;
|
hasDocuments: boolean;
|
||||||
children: KustomNode[];
|
children: KustomNode[];
|
||||||
isPhaseNode: boolean;
|
isPhaseNode: boolean;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
|
@ -81,6 +81,7 @@ export class WsConstants {
|
|||||||
public static readonly GET_DOCUMENT_BY_SELECTOR = 'getDocumentsBySelector';
|
public static readonly GET_DOCUMENT_BY_SELECTOR = 'getDocumentsBySelector';
|
||||||
public static readonly GET_EXECUTOR_DOC = 'getExecutorDoc';
|
public static readonly GET_EXECUTOR_DOC = 'getExecutorDoc';
|
||||||
public static readonly GET_PHASE = 'getPhase';
|
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_PHASE_TREE = 'getPhaseTree';
|
||||||
public static readonly GET_TARGET = 'getTarget';
|
public static readonly GET_TARGET = 'getTarget';
|
||||||
public static readonly GET_YAML = 'getYaml';
|
public static readonly GET_YAML = 'getYaml';
|
||||||
|
@ -178,7 +178,7 @@ const (
|
|||||||
GetYaml WsSubComponentType = "getYaml"
|
GetYaml WsSubComponentType = "getYaml"
|
||||||
GetRendered WsSubComponentType = "getRendered"
|
GetRendered WsSubComponentType = "getRendered"
|
||||||
GetPhaseTree WsSubComponentType = "getPhaseTree"
|
GetPhaseTree WsSubComponentType = "getPhaseTree"
|
||||||
GetPhaseSourceFiles WsSubComponentType = "getPhaseSource"
|
GetPhaseSourceFiles WsSubComponentType = "getPhaseSourceFiles"
|
||||||
GetDocumentsBySelector WsSubComponentType = "getDocumentsBySelector"
|
GetDocumentsBySelector WsSubComponentType = "getDocumentsBySelector"
|
||||||
GetPhase WsSubComponentType = "getPhase"
|
GetPhase WsSubComponentType = "getPhase"
|
||||||
GetExecutorDoc WsSubComponentType = "getExecutorDoc"
|
GetExecutorDoc WsSubComponentType = "getExecutorDoc"
|
||||||
|
@ -89,6 +89,8 @@ func HandlePhaseRequest(user *string, request configs.WsMessage) configs.WsMessa
|
|||||||
s := "rendered"
|
s := "rendered"
|
||||||
message = &s
|
message = &s
|
||||||
response.Name, response.YAML, err = client.GetExecutorDoc(id)
|
response.Name, response.YAML, err = client.GetExecutorDoc(id)
|
||||||
|
case configs.GetPhaseSourceFiles:
|
||||||
|
response.Data, err = client.getPhaseSource(id)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("Subcomponent %s not found", request.SubComponent)
|
err = fmt.Errorf("Subcomponent %s not found", request.SubComponent)
|
||||||
}
|
}
|
||||||
@ -103,6 +105,17 @@ func HandlePhaseRequest(user *string, request configs.WsMessage) configs.WsMessa
|
|||||||
return response
|
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
|
// this helper function will likely disappear once a clear workflow for
|
||||||
// phase validation takes shape in UI. For now, it simply returns a
|
// phase validation takes shape in UI. For now, it simply returns a
|
||||||
// string message to be displayed as a toast in frontend client
|
// string message to be displayed as a toast in frontend client
|
||||||
|
@ -70,26 +70,14 @@ func (client *Client) GetPhaseTree() ([]KustomNode, error) {
|
|||||||
|
|
||||||
for _, p := range phases {
|
for _, p := range phases {
|
||||||
pNode := KustomNode{
|
pNode := KustomNode{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
PhaseID: ifc.ID{Name: p.Name, Namespace: p.Namespace},
|
PhaseID: ifc.ID{Name: p.Name, Namespace: p.Namespace},
|
||||||
Name: fmt.Sprintf("Phase: %s", p.Name),
|
Name: fmt.Sprintf("Phase: %s", p.Name),
|
||||||
IsPhaseNode: true,
|
IsPhaseNode: true,
|
||||||
Children: []KustomNode{},
|
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)
|
nodes = append(nodes, pNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,12 +169,13 @@ func (client *Client) GetPhaseSourceFiles(id ifc.ID) ([]KustomNode, error) {
|
|||||||
// KustomNode structure to represent the kustomization tree for a given phase
|
// KustomNode structure to represent the kustomization tree for a given phase
|
||||||
// bundle to be consumed by the UI frontend
|
// bundle to be consumed by the UI frontend
|
||||||
type KustomNode struct {
|
type KustomNode struct {
|
||||||
ID string `json:"id"` // UUID for backend node index
|
ID string `json:"id"`
|
||||||
PhaseID ifc.ID `json:"phaseId"`
|
PhaseID ifc.ID `json:"phaseId"`
|
||||||
Name string `json:"name"` // name used for display purposes (cli, ui)
|
Name string `json:"name"`
|
||||||
IsPhaseNode bool `json:"isPhaseNode"`
|
IsPhaseNode bool `json:"isPhaseNode"`
|
||||||
HasError bool `json:"hasError"`
|
HasError bool `json:"hasError"`
|
||||||
Children []KustomNode `json:"children"`
|
HasDocuments bool `json:"hasDocuments"`
|
||||||
|
Children []KustomNode `json:"children"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(dirs []string, val string) bool {
|
func contains(dirs []string, val string) bool {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user