.
+function plot_series(series_list, root_node, batch_count) {
+ for (const series of series_list.values()) {
+ let options = {
+ title: {
+ text: series.name,
+ },
+ chart: {
+ id: series.name,
+ group: "devhub",
+ type: "line",
+ height: "400px",
+ animations: {
+ enabled: false,
+ },
+ events: {
+ dataPointSelection: (event, chartContext, { dataPointIndex }) => {
+ window.open(
+ "https://github.com/mozilla/glean/commit/" +
+ series.git_commit[dataPointIndex],
+ );
+ },
+ },
+ },
+ markers: {
+ size: 4,
+ },
+ series: [{
+ name: series.name,
+ data: series.value,
+ }],
+ xaxis: {
+ categories: Array(series.value[series.value.length - 1][0]).fill("")
+ .concat(
+ series.timestamp.map((timestamp) =>
+ format_date_day(new Date(timestamp * 1000))
+ ).reverse(),
+ ),
+ min: 0,
+ max: batch_count,
+ tickAmount: 15,
+ axisTicks: {
+ show: false,
+ },
+ tooltip: {
+ enabled: false,
+ },
+ },
+ tooltip: {
+ enabled: true,
+ shared: false,
+ intersect: true,
+ x: {
+ formatter: function (val, { dataPointIndex }) {
+ const formattedDate = format_date_day_time(
+ new Date(series.timestamp[dataPointIndex] * 1000),
+ );
+ return `${
+ series.git_commit[dataPointIndex]
+ }
${formattedDate}
`;
+ },
+ },
+ },
+ };
+
+ if (series.unit === "bytes") {
+ options.yaxis = {
+ labels: {
+ formatter: format_bytes,
+ },
+ };
+ }
+
+ if (series.unit === "ms") {
+ options.yaxis = {
+ labels: {
+ formatter: format_duration,
+ },
+ };
+ }
+
+ const div = document.createElement("div");
+ root_node.append(div);
+ const chart = new ApexCharts(div, options);
+ chart.render();
+ }
+}
+
+function format_bytes(bytes) {
+ if (bytes === 0) return "0 Bytes";
+
+ const k = 1024;
+ const sizes = [
+ "Bytes",
+ "KiB",
+ "MiB",
+ "GiB",
+ "TiB",
+ "PiB",
+ "EiB",
+ "ZiB",
+ "YiB",
+ ];
+
+ let i = 0;
+ while (i != sizes.length - 1 && Math.pow(k, i + 1) < bytes) {
+ i += 1;
+ }
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
+}
+
+function format_date_day(date) {
+ return format_date(date, false);
+}
+
+function format_date_day_time(date) {
+ return format_date(date, true);
+}
+
+function format_date(date, include_time) {
+ const pad = (number) => String(number).padStart(2, "0");
+
+ const year = date.getFullYear();
+ const month = pad(date.getMonth() + 1); // Months are 0-based.
+ const day = pad(date.getDate());
+ const hours = pad(date.getHours());
+ const minutes = pad(date.getMinutes());
+ const seconds = pad(date.getSeconds());
+ return include_time
+ ? `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+ : `${year}-${month}-${day}`;
+}
diff --git a/tools/devhub/www/index.html b/tools/devhub/www/index.html
new file mode 100644
index 0000000000..212743f8af
--- /dev/null
+++ b/tools/devhub/www/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ Glean DevHub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/devhub/www/style.css b/tools/devhub/www/style.css
new file mode 100644
index 0000000000..bf30510ac6
--- /dev/null
+++ b/tools/devhub/www/style.css
@@ -0,0 +1,127 @@
+:root {
+ --red-10: #DC3E42;
+ --lime-10: #93C926;
+ --amber-10: #FFA01C;
+ --indigo-10: #3358D4;
+ --gray-1: #FCFCFC;
+ --gray-4: #E8E8E8;
+ --gray-11: #646464;
+ --gray-12: #202020;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --red-10: #EC5D5E;
+ --lime-10: #CF0;
+ --amber-10: #FFCB47;
+ --indigo-10: #5472E4;
+ --gray-1: #111111;
+ --gray-4: #2A2A2A;
+ --gray-11: #B4B4B4;
+ --gray-12: #EEEEEE;
+ }
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background-color: var(--gray-1);
+ color: var(--gray-12);
+ font-family: system-ui;
+ -webkit-font-smoothing: antialiased;
+ font-size: 14px;
+ font-size-adjust: 0.53;
+}
+
+a {
+ color: var(--indigo-10);
+ text-underline-offset: 0.1em;
+}
+
+h2 {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+}
+
+h2 a {
+ font-size: 14px;
+ font-weight: normal;
+}
+
+h3 {
+ font-size: 14px;
+ line-height: 24px;
+ padding: 4px 16px;
+ border-bottom: 1px solid var(--gray-4);
+}
+
+nav {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid var(--gray-4);
+ height: 56px;
+ padding: 8px 16px;
+}
+
+main {
+ display: flex;
+ flex-direction: column;
+ gap: 48px;
+ padding: 24px;
+}
+
+section#top {
+ display: flex;
+ flex-direction: row;
+ gap: 24px;
+ flex-wrap: wrap;
+
+ section {
+ height: min-content;
+ border: 1px solid var(--gray-4);
+ border-radius: 6px;
+ }
+
+ #links div {
+ padding: 8px 16px;
+
+ p {
+ line-height: 24px;
+ }
+ }
+}
+
+section#metrics {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ #charts {
+ border: 1px solid var(--gray-4);
+ border-radius: 6px;
+ display: flex;
+ flex-wrap: wrap;
+ padding-top: 8px;
+ gap: 8px;
+ justify-items: stretch;
+
+ >div {
+ width: 600px;
+ }
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ #charts {
+ color: var(--gray-1);
+
+ >div {
+ filter: invert(1);
+ }
+ }
+}