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

Dev UI: Add Build Steps dependency graph #35012

Merged
merged 1 commit into from
Jul 26, 2023
Merged
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
Dev UI: Add Build Steps dependency graph
Signed-off-by: Phillip Kruger <phillip.kruger@gmail.com>
  • Loading branch information
phillip-kruger committed Jul 26, 2023
commit a96a9e3ddeb17c1b27ce358a37f196c427ce4b0d
Original file line number Diff line number Diff line change
@@ -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
@@ -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
*/
@@ -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`
@@ -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%;"
@@ -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