Skip to content

Commit 7cc580e

Browse files
committed
Add UI templates and Billboard.JS for Overview and Data Quality Panel
Signed-off-by: Sampurna Pyne <sampurnapyne1710@gmail.com>
1 parent 76b9540 commit 7cc580e

7 files changed

Lines changed: 257 additions & 7 deletions

File tree

insights/static/insights/css/insights.css

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@
197197

198198
.card-chart-title {
199199
border-bottom: 1px solid #e8e8e8;
200-
padding-bottom: 0.75rem;
201-
margin-bottom: 1.25rem;
200+
padding-bottom: 0.5rem;
201+
margin-bottom: 0.75rem;
202202
}
203203

204204
/* Package Panel Specific Styles */
@@ -305,4 +305,26 @@
305305
display: flex;
306306
align-items: center;
307307
margin-left: auto;
308+
}
309+
310+
/* Overview Panel Specific Styles */
311+
.kpi-card {
312+
border-top: 4px solid var(--bulma-link);
313+
height: 100%;
314+
margin-bottom: 0 !important;
315+
}
316+
317+
.kpi-card:hover {
318+
transform: translateY(-2px);
319+
transition: transform 0.2s ease-in-out;
320+
}
321+
322+
.delta-positive {
323+
color: var(--bulma-success);
324+
font-weight: bold;
325+
}
326+
327+
.delta-negative {
328+
color: var(--bulma-danger);
329+
font-weight: bold;
308330
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Copyright (c) nexB Inc. and others. All rights reserved.
3+
// VulnerableCode is a trademark of nexB Inc.
4+
// SPDX-License-Identifier: Apache-2.0
5+
// See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
// See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
// See https://aboutcode.org for more information about nexB OSS projects.
8+
//
9+
10+
import { initDropdownChart } from './core.js';
11+
12+
document.addEventListener('insightsDataLoaded', (e) => {
13+
if (e.detail.panelId !== "data_quality_panel") return;
14+
15+
const { snapshotData } = e.detail;
16+
if (!snapshotData) return;
17+
18+
initDropdownChart("dq-issue-type-bar", snapshotData["dq-issue-type-bar"], "All Datasources");
19+
initDropdownChart("dq-importer-contribution-donut", snapshotData["dq-importer-contribution-donut"], "All Issue Types");
20+
initDropdownChart("dq-resolution-timeline", snapshotData["dq-resolution-timeline"], "All Importers");
21+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// Copyright (c) nexB Inc. and others. All rights reserved.
3+
// VulnerableCode is a trademark of nexB Inc.
4+
// SPDX-License-Identifier: Apache-2.0
5+
// See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
// See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
// See https://aboutcode.org for more information about nexB OSS projects.
8+
//
9+
10+
import { renderChartWithData } from './core.js';
11+
12+
function formatDelta(delta) {
13+
if (delta > 0) return `<span class="delta-positive">+${delta.toLocaleString()}</span> from last snapshot`;
14+
if (delta < 0) return `<span class="delta-negative">${delta.toLocaleString()}</span> from last snapshot`;
15+
return "No change from last snapshot";
16+
}
17+
18+
document.addEventListener('insightsDataLoaded', (e) => {
19+
if (e.detail.panelId !== "overview_panel") return;
20+
21+
const snapshotData = e.detail.snapshotData;
22+
if (!snapshotData || !snapshotData["overview-stats"]) return;
23+
24+
const overviewData = snapshotData["overview-stats"];
25+
26+
const kpis = overviewData.kpis;
27+
if (kpis) {
28+
document.getElementById("kpi-total-advisories").textContent = kpis.total_advisories.toLocaleString();
29+
document.getElementById("kpi-delta-advisories").innerHTML = formatDelta(kpis.delta_advisories);
30+
31+
document.getElementById("kpi-total-packages").textContent = kpis.total_packages.toLocaleString();
32+
document.getElementById("kpi-delta-packages").innerHTML = formatDelta(kpis.delta_packages);
33+
34+
document.getElementById("kpi-total-sources").textContent = kpis.total_data_sources.toLocaleString();
35+
document.getElementById("kpi-delta-sources").innerHTML = formatDelta(kpis.delta_data_sources);
36+
}
37+
38+
renderChartWithData("overview-historical", "global", snapshotData["overview-historical"]);
39+
renderChartWithData("overview-cross-validation", "global", snapshotData["overview-cross-validation"]);
40+
renderChartWithData("overview-growth-trend", "global", snapshotData["overview-growth-trend"]);
41+
});

insights/static/insights/js/renderers.js

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const paletteVars = [
2222
"--bulma-primary-dark",
2323
];
2424

25-
const getPalette = () => paletteVars.map(getCssVar).filter(Boolean);
25+
export const getPalette = () => paletteVars.map(getCssVar).filter(Boolean);
2626

2727
const getBucketColors = () => [
2828
getCssVar("--bulma-success"), getCssVar("--bulma-success"), getCssVar("--bulma-success"), getCssVar("--bulma-success"), // 0-3: Success
@@ -62,19 +62,115 @@ export const renderers = {
6262

6363
colored_bar(id, config) {
6464
const monoColor = config.color || getCssVar("--bulma-link");
65+
const isRotated = config.rotated !== undefined ? config.rotated : true;
6566
bb.generate({
6667
bindto: `#chart-${id}`,
68+
padding: { right: 30 },
6769
data: { x: "x", columns: config.columns, type: "bar", color: () => monoColor },
6870
axis: {
69-
rotated: true,
70-
x: { type: "category", label: { text: config.x_label || "CWE", position: "outer-middle" } },
71-
y: { label: { text: config.y_label || "Advisories", position: "outer-center" }, tick: { format: formatWholeNumbersOnly } }
71+
rotated: isRotated,
72+
x: {
73+
type: "category",
74+
label: { text: config.x_label || "CWE", position: isRotated ? "outer-middle" : "outer-center" },
75+
tick: {
76+
culling: isRotated ? false : { max: config.x_tick_culling !== undefined ? config.x_tick_culling : 8 },
77+
multiline: false
78+
}
79+
},
80+
y: {
81+
label: { text: config.y_label || "Advisories", position: isRotated ? "outer-center" : "outer-middle" },
82+
tick: { format: formatWholeNumbersOnly }
83+
}
7284
},
7385
tooltip: { format: { title: x => config.full_labels?.[x] || config.columns[0][x + 1], value: val => val.toLocaleString() } },
7486
legend: { show: false }
7587
});
7688
},
7789

90+
stacked_bar(id, config) {
91+
bb.generate({
92+
bindto: `#chart-${id}`,
93+
data: {
94+
x: "x",
95+
columns: config.columns,
96+
type: "bar",
97+
groups: config.groups,
98+
colors: {
99+
"VulnerableCode": getCssVar("--bulma-link"),
100+
},
101+
},
102+
color: { pattern: getPalette() },
103+
axis: {
104+
rotated: true,
105+
x: { type: "category" },
106+
y: {
107+
label: { text: config.y_label || "Advisories", position: "outer-center" },
108+
tick: { count: 6, format: formatWholeNumbersOnly },
109+
},
110+
},
111+
tooltip: {
112+
contents(dataPoints, defaultTitle, defaultVal, color) {
113+
const nonZeroPoints = dataPoints.filter((point) => point.value > 0);
114+
if (!nonZeroPoints.length) return "";
115+
return this.internal.getTooltipContent(nonZeroPoints, defaultTitle, defaultVal, color);
116+
},
117+
format: { value: (val) => val.toLocaleString() },
118+
},
119+
});
120+
},
121+
122+
123+
line(id, config) {
124+
const isMultiSeries = config.columns.length > 2;
125+
const color = config.color || getCssVar("--bulma-primary");
126+
127+
bb.generate({
128+
bindto: `#chart-${id}`,
129+
data: {
130+
x: "x",
131+
columns: config.columns,
132+
type: "line",
133+
colors: {
134+
"Open": getCssVar("--bulma-danger"),
135+
"Resolved": getCssVar("--bulma-primary"),
136+
"Total Advisories": color,
137+
"Advisories Ingested": color,
138+
},
139+
},
140+
axis: {
141+
x: {
142+
type: "timeseries",
143+
tick: {
144+
format: isMultiSeries ? "%Y-%m" : "%b %d",
145+
fit: true,
146+
count: 6,
147+
},
148+
},
149+
y: {
150+
min: 0,
151+
padding: { bottom: 0 },
152+
label: { text: config.y_label || "Count", position: "outer-middle" },
153+
tick: { format: formatWholeNumbersOnly },
154+
},
155+
},
156+
point: { r: 3, focus: { expand: { r: 5 } } },
157+
legend: { show: isMultiSeries },
158+
tooltip: {
159+
format: {
160+
value(val, ratio, id, index) {
161+
const added =
162+
id === "Open"
163+
? config.new_open_counts?.[index]
164+
: config.new_resolved_counts?.[index];
165+
return added !== undefined
166+
? `${val.toLocaleString()} (+${added.toLocaleString()} new)`
167+
: val.toLocaleString();
168+
},
169+
},
170+
},
171+
});
172+
},
173+
78174
scatter(id, config) {
79175
const [, ...buckets] = config.columns[0];
80176
const [, ...counts] = config.columns[1];
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="overview-grid">
2+
<div class="overview-row" style="grid-template-columns: repeat(2, 1fr);">
3+
{% with chart=panel.charts.0 %}
4+
{% include 'insights/components/chart_card.html' %}
5+
{% endwith %}
6+
{% with chart=panel.charts.1 %}
7+
{% include 'insights/components/chart_card.html' %}
8+
{% endwith %}
9+
</div>
10+
11+
<div class="overview-row" style="grid-template-columns: 1fr;">
12+
{% with chart=panel.charts.2 %}
13+
{% include 'insights/components/chart_card.html' %}
14+
{% endwith %}
15+
</div>
16+
</div>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<div class="overview-panel">
2+
<!-- KPI Cards Row -->
3+
<div class="columns is-multiline mb-5">
4+
<div class="column is-4">
5+
<div class="chart-row card-chart-row has-text-centered p-5 kpi-card">
6+
<p class="card-chart-title is-size-6 has-text-grey" style="border-bottom: none;">Total Active Advisories</p>
7+
<p class="title is-size-2 has-text-black mb-1" id="kpi-total-advisories">--</p>
8+
<p class="is-size-7 has-text-grey" id="kpi-delta-advisories">--</p>
9+
</div>
10+
</div>
11+
<div class="column is-4">
12+
<div class="chart-row card-chart-row has-text-centered p-5 kpi-card">
13+
<p class="card-chart-title is-size-6 has-text-grey" style="border-bottom: none;">Total Packages</p>
14+
<p class="title is-size-2 has-text-black mb-1" id="kpi-total-packages">--</p>
15+
<p class="is-size-7 has-text-grey" id="kpi-delta-packages">--</p>
16+
</div>
17+
</div>
18+
<div class="column is-4">
19+
<div class="chart-row card-chart-row has-text-centered p-5 kpi-card">
20+
<p class="card-chart-title is-size-6 has-text-grey" style="border-bottom: none;">Data Sources</p>
21+
<p class="title is-size-2 has-text-black mb-1" id="kpi-total-sources">--</p>
22+
<p class="is-size-7 has-text-grey" id="kpi-delta-sources">--</p>
23+
</div>
24+
</div>
25+
</div>
26+
27+
<div class="columns is-multiline">
28+
<div class="column is-12">
29+
<div class="chart-row card-chart-row p-0 mb-4">
30+
<div class="columns is-desktop mb-0" style="margin: 0; align-items: stretch;">
31+
<div class="column is-7" style="border-right: 1px solid #e8e8e8; padding: 1.25rem 1.5rem;">
32+
<h3 class="card-chart-title has-text-centered mb-4">{{ panel.charts.3.title }}</h3>
33+
<div class="chart-container" style="min-height: 420px;" id="chart-{{ panel.charts.3.id }}" data-chart-type="{{ panel.charts.3.chart_type }}"></div>
34+
</div>
35+
<div class="column is-5" style="padding: 1.25rem 1.5rem;">
36+
<h3 class="card-chart-title has-text-centered mb-4">{{ panel.charts.2.title }}</h3>
37+
<div class="chart-container" style="min-height: 420px;" id="chart-{{ panel.charts.2.id }}" data-chart-type="{{ panel.charts.2.chart_type }}"></div>
38+
</div>
39+
</div>
40+
</div>
41+
</div>
42+
<div class="column is-12">
43+
{% with chart=panel.charts.1 %}
44+
{% include 'insights/components/chart_card.html' %}
45+
{% endwith %}
46+
</div>
47+
</div>
48+
</div>

insights/templates/insights/dashboard.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@
4242
</article>
4343

4444
<div class="chart-inner p-5 w-full">
45-
{% if panel.layout == 'split_top' %}
45+
{% if panel.id == 'overview_panel' %}
46+
{% include 'insights/components/overview_panel_content.html' with panel=panel %}
47+
{% elif panel.id == 'data_quality_panel' %}
48+
{% include 'insights/components/data_quality_panel_content.html' with panel=panel %}
49+
{% elif panel.layout == 'split_top' %}
4650
{% include 'insights/components/package_panel_donuts.html' with panel=panel %}
4751
{% for chart in panel.charts|slice:"2:" %}
4852
{% include 'insights/components/chart_card.html' with chart=chart current_panel_id=current_panel_id search_query=search_query search_error=search_error %}
@@ -68,7 +72,9 @@
6872
{{ search_results_dict|json_script:"chart-search-data" }}
6973
{% endif %}
7074
<script type="module" src="{% static 'insights/js/core.js' %}"></script>
75+
<script type="module" src="{% static 'insights/js/overview_panel.js' %}"></script>
7176
<script type="module" src="{% static 'insights/js/package_panel.js' %}"></script>
7277
<script type="module" src="{% static 'insights/js/severity_panel.js' %}"></script>
7378
<script type="module" src="{% static 'insights/js/importer_panel.js' %}"></script>
79+
<script type="module" src="{% static 'insights/js/data_quality_panel.js' %}"></script>
7480
{% endblock %}

0 commit comments

Comments
 (0)