Skip to content

Commit

Permalink
Dev UI: Add Build Steps dependency graph
Browse files Browse the repository at this point in the history
Signed-off-by: Phillip Kruger <[email protected]>
  • Loading branch information
phillip-kruger committed Jul 26, 2023
1 parent e017719 commit a96a9e3
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ public BuildMetricsTest() {

@Test
public void testGetBuildStepsMetrics() throws Exception {
JsonNode buildStepsMetricsResponse = super.executeJsonRPCMethod("getBuildStepsMetrics");
JsonNode buildStepsMetricsResponse = super.executeJsonRPCMethod("getBuildMetrics");
Assertions.assertNotNull(buildStepsMetricsResponse);

int duration = buildStepsMetricsResponse.get("duration").asInt();
Assertions.assertTrue(duration > 0);

boolean dependencyGraphsIncluded = buildStepsMetricsResponse.get("dependencyGraphs").isObject();
Assertions.assertTrue(dependencyGraphsIncluded);
boolean recordsIncluded = buildStepsMetricsResponse.get("records").isArray();
Assertions.assertTrue(recordsIncluded);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { LitElement, html, css} from 'lit';
import { JsonRpc } from 'jsonrpc';
import 'echarts-force-graph';
import '@vaadin/button';
import '@vaadin/checkbox';
import '@vaadin/checkbox-group';
import '@vaadin/progress-bar';

/**
* This component shows the Build Step Graph
*/
export class QwcBuildStepGraph extends LitElement {

static styles = css`
.top-bar {
display: flex;
align-items: baseline;
gap: 20px;
padding-left: 20px;
justify-content: space-between;
padding-right: 20px;
}
.top-bar h4 {
color: var(--lumo-contrast-60pct);
}
`;

static properties = {
stepId: {type: String},
extensionName: {type: String}, // TODO: Add 'pane' concept in router to register internal extension pages.
_edgeLength: {type: Number, state: true},
_dependencyGraph: {state: true},
_categories: {state: false},
_colors: {state: false},
_nodes: {state: true},
_links: {state: false},
_showSimpleDescription: {state: false}
};

constructor() {
super();
this.stepId = null;
this._dependencyGraph = null;
this._categories = ['root' , 'direct dependencies', 'direct dependents'];
this._categoriesEnum = ['root' , 'directDependency' , 'directDependent'];
this._colors = ['#ee6666', '#5470c6' , '#fac858'];
this._edgeLength = 120;
this._nodes = null;
this._links = null;
this._showSimpleDescription = [];
}

connectedCallback() {
super.connectedCallback();
this.jsonRpc = new JsonRpc(this.extensionName);
this._fetchDependencyGraph();
}

_fetchDependencyGraph(){
if(this.stepId){
this.jsonRpc.getDependencyGraph({buildStepId: this.stepId}).then(jsonRpcResponse => {
this._dependencyGraph = jsonRpcResponse.result;
this._createNodes();
});
}
}

_createNodes(){
if(this._dependencyGraph){

let dependencyGraphNodes = this._dependencyGraph.nodes;
let dependencyGraphLinks = this._dependencyGraph.links;

this._links = []
this._nodes = []
for (var l = 0; l < dependencyGraphLinks.length; l++) {
let link = new Object();
link.source = dependencyGraphNodes.findIndex(item => item.stepId === dependencyGraphLinks[l].source);
link.target = dependencyGraphNodes.findIndex(item => item.stepId === dependencyGraphLinks[l].target);
let catindex = this._categoriesEnum.indexOf(dependencyGraphLinks[l].type);

this._addToNodes(dependencyGraphNodes[link.source],catindex);
this._addToNodes(dependencyGraphNodes[link.target],catindex);
this._links.push(link);
}
}

}

_addToNodes(dependencyGraphNode, catindex){
let newNode = this._createNode(dependencyGraphNode);
let index = this._nodes.findIndex(item => item.name === newNode.name);
if (index < 0 ) {
if(dependencyGraphNode.stepId === this.stepId){
newNode.category = 0; // Root
}else {
newNode.category = catindex;
}
this._nodes.push(newNode);
}
}

_createNode(node){
let nodeObject = new Object();
if(this._showSimpleDescription.length>0){
nodeObject.name = node.simpleName;
}else{
nodeObject.name = node.stepId;
}

nodeObject.value = 1;
nodeObject.id = node.stepId;
nodeObject.description = node.simpleName;
return nodeObject;
}

render() {
if(this.stepId && this._dependencyGraph){
return html`${this._renderTopBar()}
<echarts-force-graph width="400px" height="400px"
edgeLength=${this._edgeLength}
categories="${JSON.stringify(this._categories)}"
colors="${JSON.stringify(this._colors)}"
nodes="${JSON.stringify(this._nodes)}"
links="${JSON.stringify(this._links)}"
@echarts-click=${this._echartClicked}>
</echarts-force-graph>`;
} else if(this.stepId) {
return html`
<div style="color: var(--lumo-secondary-text-color);width: 95%;" >
<div>Loading Dependency Graph...</div>
<vaadin-progress-bar indeterminate></vaadin-progress-bar>
</div>
`;
} else {
return html`<span>No build step provided</span>`;
}
}

_renderTopBar(){
return html`
<div class="top-bar">
<vaadin-button @click="${this._backAction}">
<vaadin-icon icon="font-awesome-solid:caret-left" slot="prefix"></vaadin-icon>
Back
</vaadin-button>
<h4>${this.stepId}</h4>
<div>
${this._renderCheckbox()}
<vaadin-button theme="icon" aria-label="Zoom in" @click=${this._zoomIn}>
<vaadin-icon icon="font-awesome-solid:magnifying-glass-plus"></vaadin-icon>
</vaadin-button>
<vaadin-button theme="icon" aria-label="Zoom out" @click=${this._zoomOut}>
<vaadin-icon icon="font-awesome-solid:magnifying-glass-minus"></vaadin-icon>
</vaadin-button>
</div>
</div>`;
}

_renderCheckbox(){
return html`<vaadin-checkbox-group
.value="${this._showSimpleDescription}"
@value-changed="${(event) => {
this._showSimpleDescription = event.detail.value;
this._createNodes();
}}">
<vaadin-checkbox value="0" label="Simple description"></vaadin-checkbox>
</vaadin-checkbox-group>`;
}

_backAction(){
const back = new CustomEvent("build-steps-graph-back", {
detail: {},
bubbles: true,
cancelable: true,
composed: false,
});
this.dispatchEvent(back);
}

_zoomIn(){
if(this._edgeLength>10){
this._edgeLength = this._edgeLength - 10;
}else{
this._edgeLength = 10;
}
}

_zoomOut(){
this._edgeLength = this._edgeLength + 10;
}

_echartClicked(e){
this.stepId = e.detail.id;
this._fetchDependencyGraph();
}
}
customElements.define('qwc-build-step-graph', QwcBuildStepGraph);
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import '@vaadin/text-field';
import '@vaadin/vertical-layout';
import '@vaadin/horizontal-layout';
import '@vaadin/progress-bar';
import './qwc-build-step-graph.js';


/**
* This component shows the Build Steps
*/
Expand All @@ -35,27 +38,44 @@ export class QwcBuildSteps extends QwcHotReloadElement {
.datatable {
width: 100%;
}`;
}
.graph-icon {
font-size: small;
color: var(--lumo-contrast-50pct);
cursor: pointer;
}
.graph {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
}
`;

static properties = {
_buildStepsMetrics: { state: true },
_buildMetrics: { state: true },
_selectedBuildStep: {state: true},
_filtered: {state: true, type: Array}
};

constructor() {
super();
this._buildMetrics = null;
this._selectedBuildStep = null;
this.hotReload();
}

hotReload(){
this.jsonRpc.getBuildStepsMetrics().then(e => {
this._buildStepsMetrics = e.result;
this._filtered = this._buildStepsMetrics.records;
this.jsonRpc.getBuildMetrics().then(e => {
this._buildMetrics = e.result;
this._filtered = this._buildMetrics.records;
});
}

render() {
if (this._buildStepsMetrics && this._filtered) {
if (this._buildMetrics && this._filtered) {
return this._render();
}else {
return html`
Expand All @@ -77,18 +97,27 @@ export class QwcBuildSteps extends QwcHotReloadElement {
_filter(e) {
const searchTerm = (e.detail.value || '').trim();
if (searchTerm === '') {
this._filtered = this._buildStepsMetrics.records;
this._filtered = this._buildMetrics.records;
return;
}

this._filtered = this._buildStepsMetrics.records.filter((record) => {
this._filtered = this._buildMetrics.records.filter((record) => {
return this._match(record.stepId, searchTerm);
});
}

_render() {
if(this._selectedBuildStep){
return this._renderBuildStepGraph();
}else{
return this._renderBuildStepList();
}
}

_renderBuildStepList(){

return html`<div class="build-steps">
<div class="summary">Executed <strong>${this._buildStepsMetrics.records.length}</strong> build steps on <strong>${Object.keys(this._buildStepsMetrics.threadSlotRecords).length}</strong> threads in <strong>${this._buildStepsMetrics.duration} ms</strong>.</div>
<div class="summary">Executed <strong>${this._buildMetrics.records.length}</strong> build steps on <strong>${this._buildMetrics.numberOfThreads}</strong> threads in <strong>${this._buildMetrics.duration} ms</strong>.</div>
<vaadin-text-field
placeholder="Filter"
style="width: 100%;"
Expand Down Expand Up @@ -116,11 +145,41 @@ export class QwcBuildSteps extends QwcHotReloadElement {
header="Thread"
path="thread">
</vaadin-grid-sort-column>
<vaadin-grid-column
frozen-to-end
auto-width
flex-grow="0"
${columnBodyRenderer(this._graphIconRenderer, [])}
></vaadin-grid-column>
</vaadin-grid></div>`;
}

_renderBuildStepGraph(){

return html`<qwc-build-step-graph class="graph"
stepId="${this._selectedBuildStep.stepId}"
extensionName="${this.jsonRpc.getExtensionName()}"
@build-steps-graph-back=${this._showBuildStepsList}></qwc-build-step-graph>`;

}

_stepIdRenderer(record) {
return html`<code>${record.stepId}</code>`;
}

_graphIconRenderer(buildStep){
return html`<vaadin-icon class="graph-icon" icon="font-awesome-solid:diagram-project" @click=${() => this._showGraph(buildStep)}></vaadin-icon>`;
}

_showGraph(buildStep){
this._selectedBuildStep = buildStep;
}

_showBuildStepsList(){
this._selectedBuildStep = null;
}

}
customElements.define('qwc-build-steps', QwcBuildSteps);
Loading

0 comments on commit a96a9e3

Please sign in to comment.