2024-02-12 15:50:35 -06:00
|
|
|
// Copyright 2023 Jeremy Wall
|
2024-02-11 19:08:15 -06:00
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
2024-02-14 19:45:28 -06:00
|
|
|
|
2024-03-05 20:32:04 -05:00
|
|
|
/**
|
|
|
|
* Map ansi terminal codes to html color codes.
|
|
|
|
* @param {string} line
|
|
|
|
*/
|
|
|
|
function ansiToHtml(line) {
|
|
|
|
const ansiToHtmlMap = {
|
|
|
|
// Map ANSI color codes to HTML color names or hex values
|
|
|
|
// We don't necessarily handle all the colors but this is enough to start.
|
|
|
|
"30": "black",
|
|
|
|
"31": "red",
|
|
|
|
"32": "green",
|
|
|
|
"33": "yellow",
|
|
|
|
"34": "blue",
|
|
|
|
"35": "magenta",
|
|
|
|
"36": "cyan",
|
|
|
|
"37": "white",
|
|
|
|
"39": "initial"
|
|
|
|
};
|
|
|
|
|
|
|
|
// NOTE(zaphar): Yes this is gross and I should really do a better parser but I'm lazy.
|
|
|
|
// Replace ANSI codes with HTML span elements styled with the corresponding color
|
|
|
|
return line.replace(/\x1b\[([0-9;]*)m/g, (match, p1) => {
|
|
|
|
const parts = p1.split(';'); // ANSI codes can be compounded, e.g., "1;31" for bold red
|
|
|
|
let styles = '';
|
|
|
|
for (let part of parts) {
|
|
|
|
if (ansiToHtmlMap[part]) {
|
|
|
|
// If the code is a color, map it to a CSS color
|
|
|
|
styles += `color: ${ansiToHtmlMap[part]};`;
|
|
|
|
}
|
|
|
|
// TODO(zaphar): Add more conditions here to handle other styles like bold or underline?
|
|
|
|
}
|
|
|
|
return styles ? `<span style="${styles}">` : '</span>';
|
|
|
|
}) + '</span>';
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Get's a css variable's value from the document.
|
|
|
|
* @param {string} variableName - Name of the variable to get `--var-name`
|
|
|
|
* @returns string
|
|
|
|
*/
|
2024-02-22 19:49:48 -05:00
|
|
|
function getCssVariableValue(variableName) {
|
|
|
|
return getComputedStyle(document.documentElement).getPropertyValue(variableName);
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Custom element for showing a plotly graph.
|
|
|
|
*
|
|
|
|
* @extends HTMLElement
|
|
|
|
*/
|
2024-03-03 18:15:41 -05:00
|
|
|
export class GraphPlot extends HTMLElement {
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?string} */
|
2024-02-07 19:18:30 -06:00
|
|
|
#uri;
|
2024-03-13 09:00:04 -04:00
|
|
|
/** @type {?boolean} */
|
|
|
|
#allowUriFilters;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?number} */
|
2024-02-08 19:00:41 -06:00
|
|
|
#width;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?number} */
|
2024-02-08 19:00:41 -06:00
|
|
|
#height;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?number} */
|
2024-02-09 14:56:58 -06:00
|
|
|
#intervalId;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?number} */
|
2024-02-09 14:56:58 -06:00
|
|
|
#pollSeconds;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?string} */
|
2024-02-14 19:45:28 -06:00
|
|
|
#end;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?number} */
|
2024-02-13 18:43:58 -06:00
|
|
|
#duration;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?string} */
|
2024-02-13 18:43:58 -06:00
|
|
|
#step_duration;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?string} */
|
2024-02-16 15:01:22 -05:00
|
|
|
#d3TickFormat = "~s";
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?HTMLDivElement} */
|
2024-02-10 16:51:57 -06:00
|
|
|
#targetNode = null;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {?HTMLElement} */
|
2024-02-19 22:43:17 -05:00
|
|
|
#menuContainer = null;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {Object<string, HTMLSelectElement>} */
|
2024-02-19 22:43:17 -05:00
|
|
|
#filterSelectElements = {};
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {Object<string, Array<string>>} */
|
2024-02-19 22:43:17 -05:00
|
|
|
#filterLabels = {};
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {Object<string, Array<string>>} */
|
2024-02-19 22:43:17 -05:00
|
|
|
#filteredLabelSets = {};
|
2024-03-03 15:23:31 -05:00
|
|
|
|
2024-02-08 19:00:41 -06:00
|
|
|
constructor() {
|
|
|
|
super();
|
2024-02-10 14:19:19 -06:00
|
|
|
this.#width = 800;
|
|
|
|
this.#height = 600;
|
|
|
|
this.#pollSeconds = 30;
|
2024-02-19 22:43:17 -05:00
|
|
|
this.#menuContainer = this.appendChild(document.createElement('div'));
|
2024-02-21 15:48:15 -05:00
|
|
|
// TODO(jwall): These should probably be done as template clones so we have less places
|
|
|
|
// to look for class attributes.
|
|
|
|
this.#menuContainer.setAttribute("class", "row-flex");
|
2024-02-10 16:51:57 -06:00
|
|
|
this.#targetNode = this.appendChild(document.createElement("div"));
|
2024-02-08 19:00:41 -06:00
|
|
|
}
|
|
|
|
|
2024-03-13 09:00:04 -04:00
|
|
|
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format', 'allow-uri-filter'];
|
2024-02-08 19:00:41 -06:00
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Callback for attributes changes.
|
|
|
|
*
|
|
|
|
* @param {string} name - The name of the attribute.
|
|
|
|
* @param {?string} _oldValue - The old value for the attribute
|
|
|
|
* @param {?string} newValue - The new value for the attribute
|
|
|
|
*/
|
2024-02-14 19:45:28 -06:00
|
|
|
attributeChangedCallback(name, _oldValue, newValue) {
|
2024-02-08 19:00:41 -06:00
|
|
|
switch (name) {
|
2024-02-12 16:22:57 -06:00
|
|
|
case 'uri':
|
2024-02-08 19:00:41 -06:00
|
|
|
this.#uri = newValue;
|
|
|
|
break;
|
2024-02-12 16:22:57 -06:00
|
|
|
case 'width':
|
2024-03-03 15:23:31 -05:00
|
|
|
this.#width = Number(newValue);
|
2024-02-08 19:00:41 -06:00
|
|
|
break;
|
2024-02-12 16:22:57 -06:00
|
|
|
case 'height':
|
2024-03-03 15:23:31 -05:00
|
|
|
this.#height = Number(newValue);
|
2024-02-08 19:00:41 -06:00
|
|
|
break;
|
2024-02-12 16:22:57 -06:00
|
|
|
case 'poll-seconds':
|
2024-03-03 15:23:31 -05:00
|
|
|
this.#pollSeconds = Number(newValue);
|
2024-02-09 14:56:58 -06:00
|
|
|
break;
|
2024-02-14 19:45:28 -06:00
|
|
|
case 'end':
|
|
|
|
this.#end = newValue;
|
2024-02-13 18:43:58 -06:00
|
|
|
break;
|
|
|
|
case 'duration':
|
2024-03-03 15:23:31 -05:00
|
|
|
this.#duration = Number(newValue);
|
2024-02-13 18:43:58 -06:00
|
|
|
break;
|
|
|
|
case 'step-duration':
|
|
|
|
this.#step_duration = newValue;
|
|
|
|
break;
|
2024-02-16 15:01:22 -05:00
|
|
|
case 'd3-tick-format':
|
|
|
|
this.#d3TickFormat = newValue;
|
|
|
|
break;
|
2024-03-13 09:00:04 -04:00
|
|
|
case 'allow-uri-filters':
|
|
|
|
this.#allowUriFilters = Boolean(newValue);
|
|
|
|
break;
|
2024-02-12 16:22:57 -06:00
|
|
|
default: // do nothing;
|
2024-02-08 19:00:41 -06:00
|
|
|
break;
|
|
|
|
}
|
2024-02-19 22:43:17 -05:00
|
|
|
this.reset();
|
2024-02-08 19:00:41 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback() {
|
2024-02-10 16:35:11 -06:00
|
|
|
this.#uri = this.getAttribute('uri') || this.#uri;
|
2024-03-03 15:23:31 -05:00
|
|
|
this.#width = Number(this.getAttribute('width') || this.#width);
|
|
|
|
this.#height = Number(this.getAttribute('height') || this.#height);
|
|
|
|
this.#pollSeconds = Number(this.getAttribute('poll-seconds') || this.#pollSeconds);
|
2024-02-14 19:45:28 -06:00
|
|
|
this.#end = this.getAttribute('end') || null;
|
2024-03-03 15:23:31 -05:00
|
|
|
this.#duration = Number(this.getAttribute('duration')) || null;
|
2024-02-13 18:43:58 -06:00
|
|
|
this.#step_duration = this.getAttribute('step-duration') || null;
|
2024-02-16 15:01:22 -05:00
|
|
|
this.#d3TickFormat = this.getAttribute('d3-tick-format') || this.#d3TickFormat;
|
2024-03-13 09:00:04 -04:00
|
|
|
this.#allowUriFilters = Boolean(this.getAttribute('allow-uri-filters'));
|
2024-03-21 18:57:46 -04:00
|
|
|
this.reset(true);
|
2024-02-08 19:00:41 -06:00
|
|
|
}
|
2024-02-25 09:03:18 -05:00
|
|
|
|
2024-02-08 19:00:41 -06:00
|
|
|
disconnectedCallback() {
|
2024-02-10 16:35:11 -06:00
|
|
|
this.stopInterval()
|
2024-02-07 19:18:30 -06:00
|
|
|
}
|
|
|
|
|
2024-02-27 18:13:26 -05:00
|
|
|
static elementName = "graph-plot";
|
2024-02-08 19:00:41 -06:00
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/*
|
|
|
|
* Get's the target node for placing the plotly graph.
|
|
|
|
*
|
|
|
|
* @returns {?HTMLDivElement}
|
|
|
|
*/
|
2024-02-09 14:56:58 -06:00
|
|
|
getTargetNode() {
|
2024-02-10 16:51:57 -06:00
|
|
|
return this.#targetNode;
|
2024-02-09 14:56:58 -06:00
|
|
|
}
|
2024-02-12 16:22:57 -06:00
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
*/
|
2024-02-09 14:56:58 -06:00
|
|
|
stopInterval() {
|
|
|
|
if (this.#intervalId) {
|
|
|
|
clearInterval(this.#intervalId);
|
|
|
|
this.#intervalId = null;
|
|
|
|
}
|
|
|
|
}
|
2024-02-12 16:22:57 -06:00
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Resets the entire graph and then restarts polling.
|
|
|
|
* @param {boolean=} updateOnly
|
|
|
|
*/
|
2024-02-19 22:43:17 -05:00
|
|
|
reset(updateOnly) {
|
|
|
|
var self = this;
|
|
|
|
self.stopInterval()
|
|
|
|
self.fetchData().then((data) => {
|
|
|
|
if (!updateOnly) {
|
2024-04-04 16:12:31 -04:00
|
|
|
self.getLabelsForData(data.Metrics || data.Logs.lines);
|
2024-02-19 22:43:17 -05:00
|
|
|
self.buildFilterMenu();
|
|
|
|
}
|
|
|
|
self.updateGraph(data).then(() => {
|
|
|
|
self.#intervalId = setInterval(() => self.updateGraph(), 1000 * self.#pollSeconds);
|
|
|
|
});
|
|
|
|
});
|
2024-02-09 14:56:58 -06:00
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/** Registers the custom element if it doesn't already exist */
|
2024-02-08 19:00:41 -06:00
|
|
|
static registerElement() {
|
2024-02-27 18:13:26 -05:00
|
|
|
if (!customElements.get(GraphPlot.elementName)) {
|
|
|
|
customElements.define(GraphPlot.elementName, GraphPlot);
|
2024-02-08 19:00:41 -06:00
|
|
|
}
|
|
|
|
}
|
2024-02-12 16:22:57 -06:00
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Returns the uri formatted with any query strings if necessary.
|
|
|
|
*
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
2024-02-13 18:43:58 -06:00
|
|
|
getUri() {
|
2024-03-13 09:00:04 -04:00
|
|
|
//var uriParts = [this.#uri];
|
|
|
|
var uriParts = [];
|
2024-02-14 19:45:28 -06:00
|
|
|
if (this.#end && this.#duration && this.#step_duration) {
|
2024-03-13 09:00:04 -04:00
|
|
|
uriParts.push("end=" + this.#end);
|
|
|
|
uriParts.push("duration=" + this.#duration);
|
|
|
|
uriParts.push("step_duration=" + this.#step_duration);
|
|
|
|
}
|
|
|
|
if (this.#allowUriFilters) {
|
|
|
|
for (const filterName in this.#filteredLabelSets) {
|
|
|
|
const filterVals = this.#filteredLabelSets[filterName].join("|");
|
|
|
|
uriParts.push(`filter-${filterName}=${filterVals}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (uriParts) {
|
|
|
|
return this.#uri + "?" + uriParts.join('&');
|
2024-02-13 18:43:58 -06:00
|
|
|
} else {
|
|
|
|
return this.#uri;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Returns the data from an api call.
|
|
|
|
*
|
2024-03-21 19:59:01 -04:00
|
|
|
* @return {Promise<QueryPayload>}
|
2024-03-03 15:23:31 -05:00
|
|
|
*/
|
2024-02-07 19:18:30 -06:00
|
|
|
async fetchData() {
|
2024-02-19 22:43:17 -05:00
|
|
|
// TODO(zaphar): Can we do some massaging on these
|
|
|
|
// to get the full set of labels and possible values?
|
2024-02-13 18:43:58 -06:00
|
|
|
const response = await fetch(this.getUri());
|
2024-02-07 19:18:30 -06:00
|
|
|
const data = await response.json();
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Formats the name for the plot trace.
|
2024-03-10 21:05:49 -04:00
|
|
|
* @param {PlotMeta} meta
|
2024-03-03 15:23:31 -05:00
|
|
|
* @param {Map<string, string>} labels
|
|
|
|
* @return string
|
|
|
|
*/
|
2024-02-19 19:53:31 -05:00
|
|
|
formatName(meta, labels) {
|
2024-02-25 09:03:18 -05:00
|
|
|
var name = "";
|
|
|
|
const formatter = meta.name_format
|
|
|
|
if (formatter) {
|
|
|
|
name = eval(formatter);
|
|
|
|
} else {
|
|
|
|
var names = [];
|
|
|
|
for (const value of labels) {
|
|
|
|
names.push(value);
|
|
|
|
}
|
|
|
|
name = names.join(" ");
|
|
|
|
}
|
|
|
|
return name;
|
2024-02-19 19:53:31 -05:00
|
|
|
}
|
|
|
|
|
2024-04-07 16:25:08 -04:00
|
|
|
getFilterLabels() {
|
|
|
|
return this.#filterLabels;
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* @param {Object<string, string>} labels
|
|
|
|
*/
|
2024-02-19 22:43:17 -05:00
|
|
|
populateFilterData(labels) {
|
|
|
|
for (var key in labels) {
|
|
|
|
const label = this.#filterLabels[key];
|
|
|
|
if (label) {
|
|
|
|
if (!label.includes(labels[key])) {
|
|
|
|
this.#filterLabels[key].push(labels[key]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.#filterLabels[key] = [labels[key]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
* @returns {HTMLDivElement}
|
|
|
|
*/
|
2024-02-19 22:43:17 -05:00
|
|
|
buildSelectElement(key) {
|
|
|
|
var id = key + "-select" + Math.random();
|
|
|
|
const element = document.createElement("div");
|
|
|
|
const select = document.createElement("select");
|
|
|
|
select.setAttribute("name", id);
|
2024-03-10 20:45:43 -04:00
|
|
|
// TODO(jwall): This is how you set boolean attributes. Use the attribute name... :-(
|
2024-03-03 15:23:31 -05:00
|
|
|
select.setAttribute("multiple", "multiple");
|
2024-03-10 20:45:43 -04:00
|
|
|
select.setAttribute("size", "3");
|
2024-02-19 22:43:17 -05:00
|
|
|
const optElement = document.createElement("option");
|
2024-03-06 15:43:34 -05:00
|
|
|
const optValue = "Select All: " + key;
|
2024-02-19 22:43:17 -05:00
|
|
|
optElement.innerText = optValue;
|
|
|
|
select.appendChild(optElement);
|
|
|
|
for (var opt of this.#filterLabels[key]) {
|
|
|
|
const optElement = document.createElement("option");
|
|
|
|
optElement.setAttribute("value", opt);
|
2024-03-03 15:23:31 -05:00
|
|
|
optElement.setAttribute("selected", "selected");
|
2024-03-06 15:43:34 -05:00
|
|
|
optElement.selected = true;
|
2024-02-19 22:43:17 -05:00
|
|
|
optElement.innerText = opt;
|
|
|
|
select.appendChild(optElement);
|
|
|
|
}
|
2024-02-25 09:03:18 -05:00
|
|
|
|
2024-02-19 22:43:17 -05:00
|
|
|
var self = this;
|
|
|
|
select.onchange = function(evt) {
|
|
|
|
evt.stopPropagation();
|
|
|
|
var filteredValues = [];
|
2024-03-06 15:43:34 -05:00
|
|
|
const selectElement = /** @type {HTMLSelectElement} */(evt.target);
|
|
|
|
var selectAll = /** @type {?HTMLOptionElement}*/(null);
|
|
|
|
for (const optEl of selectElement.selectedOptions) {
|
|
|
|
if (optEl.value && optEl.value.startsWith("Select All: ")) {
|
|
|
|
selectAll = optEl;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const o of selectElement.options) {
|
|
|
|
if (selectAll) {
|
|
|
|
if (o != selectAll) {
|
|
|
|
o.setAttribute("selected", "selected");
|
|
|
|
o.selected = true;
|
|
|
|
filteredValues.push(o.value);
|
|
|
|
} else {
|
|
|
|
o.removeAttribute("selected");
|
|
|
|
}
|
|
|
|
} else if (!o.selected) {
|
|
|
|
o.removeAttribute("selected");
|
|
|
|
} else {
|
|
|
|
o.setAttribute("selected", "selected");
|
|
|
|
filteredValues.push(o.value);
|
|
|
|
}
|
2024-02-19 22:43:17 -05:00
|
|
|
}
|
|
|
|
self.#filteredLabelSets[key] = filteredValues;
|
|
|
|
self.reset(true);
|
|
|
|
};
|
|
|
|
element.appendChild(select);
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
|
|
|
|
buildFilterMenu() {
|
|
|
|
// We need to maintain a stable order for these
|
|
|
|
var children = [];
|
|
|
|
for (var key of Object.keys(this.#filterLabels).sort()) {
|
2024-02-25 09:03:18 -05:00
|
|
|
// If there are multiple items to filter by then show the selectElement.
|
|
|
|
// otherwise there is no point.
|
|
|
|
if (this.#filterLabels[key].length > 1) {
|
|
|
|
const element = this.#filterSelectElements[key] || this.buildSelectElement(key);
|
|
|
|
children.push(element);
|
|
|
|
}
|
2024-02-19 22:43:17 -05:00
|
|
|
}
|
|
|
|
this.#menuContainer.replaceChildren(...children);
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
2024-03-21 19:59:01 -04:00
|
|
|
* @param {QueryData|LogLineList} graph
|
2024-03-03 15:23:31 -05:00
|
|
|
*/
|
2024-02-24 19:53:25 -05:00
|
|
|
getLabelsForData(graph) {
|
2024-04-04 16:12:31 -04:00
|
|
|
if (/** @type {QueryData} */(graph).plots) {
|
|
|
|
this.getLabelsForQueryData(/** @type {QueryData} */(graph));
|
|
|
|
} else {
|
|
|
|
this.getLabelsForLogLines(/** @type {LogLineList} */(graph));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {LogLineList} graph
|
|
|
|
*/
|
|
|
|
getLabelsForLogLines(graph) {
|
|
|
|
if (graph.Stream) {
|
|
|
|
for (const pair of graph.Stream) {
|
|
|
|
const labels = pair[0];
|
|
|
|
this.populateFilterData(labels);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (graph.StreamInstant) {
|
|
|
|
// TODO(zaphar): Handle this?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {QueryData} graph
|
|
|
|
*/
|
|
|
|
getLabelsForQueryData(graph) {
|
2024-02-24 19:53:25 -05:00
|
|
|
const data = graph.plots;
|
2024-02-19 22:43:17 -05:00
|
|
|
for (var subplot of data) {
|
|
|
|
if (subplot.Series) {
|
|
|
|
for (const triple of subplot.Series) {
|
|
|
|
const labels = triple[0];
|
|
|
|
this.populateFilterData(labels);
|
|
|
|
}
|
|
|
|
}
|
2024-02-21 16:22:02 -05:00
|
|
|
if (subplot.Scalar) {
|
|
|
|
for (const triple of subplot.Scalar) {
|
|
|
|
const labels = triple[0];
|
|
|
|
this.populateFilterData(labels);
|
|
|
|
}
|
|
|
|
}
|
2024-02-19 22:43:17 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-24 19:53:25 -05:00
|
|
|
yaxisNameGenerator() {
|
|
|
|
var counter = 1;
|
|
|
|
return function() {
|
|
|
|
var name = "yaxis";
|
|
|
|
if (counter != 1) {
|
2024-02-25 09:03:18 -05:00
|
|
|
name = "yaxis" + counter;
|
2024-02-24 19:53:25 -05:00
|
|
|
}
|
|
|
|
counter++;
|
|
|
|
return name;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-03-10 21:11:32 -04:00
|
|
|
/**
|
|
|
|
* @param {any} triple
|
|
|
|
*/
|
2024-03-10 21:05:49 -04:00
|
|
|
buildSeriesPlot(triple) {
|
|
|
|
const labels = /** @type {Map<String, String>} */(triple[0]);
|
|
|
|
for (var label in labels) {
|
|
|
|
var show = this.#filteredLabelSets[label];
|
|
|
|
if (show && !show.includes(labels[label])) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const meta = /** @type {PlotMeta} */(triple[1]);
|
|
|
|
var yaxis = meta.yaxis || "y";
|
|
|
|
// https://plotly.com/javascript/reference/layout/yaxis/
|
|
|
|
const series = triple[2];
|
|
|
|
const trace = /** @type GraphTrace */({
|
|
|
|
type: "scatter",
|
|
|
|
mode: "lines+text",
|
|
|
|
x: [],
|
|
|
|
y: [],
|
|
|
|
// We always share the x axis for timeseries graphs.
|
|
|
|
xaxis: "x",
|
|
|
|
yaxis: yaxis,
|
|
|
|
//yhoverformat: yaxis.tickformat,
|
|
|
|
});
|
|
|
|
if (meta.fill) {
|
|
|
|
trace.fill = meta.fill;
|
|
|
|
}
|
|
|
|
var name = this.formatName(meta, labels);
|
|
|
|
if (name) { trace.name = name; }
|
|
|
|
for (const point of series) {
|
|
|
|
trace.x.push(new Date(point.timestamp * 1000));
|
|
|
|
trace.y.push(point.value);
|
|
|
|
}
|
|
|
|
return trace;
|
|
|
|
}
|
|
|
|
|
2024-03-10 21:11:32 -04:00
|
|
|
/**
|
|
|
|
* @param {any} triple
|
|
|
|
*/
|
|
|
|
buildScalarPlot(triple) {
|
|
|
|
const labels = /** @type {Map<String,String>} */(triple[0]);
|
|
|
|
for (var label in labels) {
|
|
|
|
var show = this.#filteredLabelSets[label];
|
|
|
|
if (show && !show.includes(labels[label])) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const meta = /** @type {PlotMeta} */(triple[1]);
|
|
|
|
const series = triple[2];
|
|
|
|
const trace = /** @type GraphTrace */({
|
|
|
|
type: "bar",
|
|
|
|
x: [],
|
|
|
|
y: [],
|
|
|
|
yhoverformat: meta["d3_tick_format"],
|
|
|
|
});
|
|
|
|
var name = this.formatName(meta, labels);
|
|
|
|
if (name) { trace.name = name; }
|
|
|
|
trace.y.push(series.value);
|
|
|
|
trace.x.push(trace.name);
|
|
|
|
return trace;
|
|
|
|
}
|
|
|
|
|
2024-03-11 15:34:16 -04:00
|
|
|
/**
|
|
|
|
* @param {Array} stream
|
2024-03-21 19:59:01 -04:00
|
|
|
*
|
|
|
|
* @returns {{dates: Array<string>, meta: Array<string>, lines: Array<string>}}
|
2024-03-11 15:34:16 -04:00
|
|
|
*/
|
|
|
|
buildStreamPlot(stream) {
|
|
|
|
const dateColumn = [];
|
|
|
|
const metaColumn = [];
|
|
|
|
const logColumn = [];
|
|
|
|
|
|
|
|
loopStream: for (const pair of stream) {
|
|
|
|
const labels = pair[0];
|
|
|
|
var labelList = [];
|
|
|
|
for (var label in labels) {
|
|
|
|
var show = this.#filteredLabelSets[label];
|
|
|
|
if (show && !show.includes(labels[label])) {
|
|
|
|
continue loopStream;
|
|
|
|
}
|
|
|
|
labelList.push(`${label}:${labels[label]}`);
|
|
|
|
}
|
|
|
|
const labelsName = labelList.join("<br>");
|
|
|
|
const lines = pair[1];
|
|
|
|
for (const line of lines) {
|
|
|
|
// For streams the timestamps are in nanoseconds
|
|
|
|
let timestamp = new Date(line.timestamp / 1000000);
|
|
|
|
dateColumn.push(timestamp.toISOString());
|
|
|
|
metaColumn.push(labelsName);
|
|
|
|
logColumn.push(ansiToHtml(line.line));
|
|
|
|
}
|
|
|
|
}
|
2024-03-21 19:59:01 -04:00
|
|
|
return { dates: dateColumn, meta: metaColumn, lines: logColumn };
|
2024-03-11 15:34:16 -04:00
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/**
|
|
|
|
* Update the graph with new data.
|
|
|
|
*
|
2024-03-21 19:59:01 -04:00
|
|
|
* @param {?QueryPayload=} maybeGraph
|
2024-03-03 15:23:31 -05:00
|
|
|
*/
|
2024-02-24 19:53:25 -05:00
|
|
|
async updateGraph(maybeGraph) {
|
|
|
|
var graph = maybeGraph;
|
|
|
|
if (!graph) {
|
|
|
|
graph = await this.fetchData();
|
2024-02-19 22:43:17 -05:00
|
|
|
}
|
2024-03-21 19:59:01 -04:00
|
|
|
if (graph.Metrics) {
|
|
|
|
this.updateMetricsGraph(graph.Metrics);
|
|
|
|
} else if (graph.Logs) {
|
|
|
|
this.updateLogsView(graph.Logs.lines);
|
|
|
|
} else {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the logs view with new data.
|
|
|
|
*
|
|
|
|
* @param {?LogLineList=} logLineList
|
|
|
|
*/
|
|
|
|
updateLogsView(logLineList) {
|
|
|
|
var layout = {
|
|
|
|
displayModeBar: false,
|
|
|
|
responsive: true,
|
|
|
|
plot_bgcolor: getCssVariableValue('--plot-background-color').trim(),
|
|
|
|
paper_bgcolor: getCssVariableValue('--paper-background-color').trim(),
|
|
|
|
font: {
|
|
|
|
color: getCssVariableValue('--text-color').trim()
|
|
|
|
},
|
|
|
|
xaxis: {
|
|
|
|
gridcolor: getCssVariableValue("--grid-line-color")
|
|
|
|
},
|
|
|
|
legend: {
|
|
|
|
orientation: 'v'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
var traces = [];
|
|
|
|
if (logLineList.Stream) {
|
|
|
|
// TODO(jwall): It's possible that this should actually be a separate custom
|
|
|
|
// element.
|
|
|
|
const trace = /** @type TableTrace */ ({
|
|
|
|
type: "table",
|
|
|
|
columnwidth: [15, 20, 70],
|
|
|
|
header: {
|
|
|
|
align: "left",
|
|
|
|
values: ["Timestamp", "Labels", "Log"],
|
|
|
|
fill: { color: layout.xaxis.paper_bgcolor },
|
|
|
|
font: { color: getCssVariableValue('--text-color').trim() }
|
|
|
|
},
|
|
|
|
cells: {
|
|
|
|
align: "left",
|
|
|
|
values: [],
|
|
|
|
fill: { color: layout.plot_bgcolor }
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const columns = this.buildStreamPlot(logLineList.Stream);
|
|
|
|
trace.cells.values.push(columns.dates);
|
|
|
|
trace.cells.values.push(columns.meta);
|
|
|
|
trace.cells.values.push(columns.lines);
|
|
|
|
traces.push(trace);
|
|
|
|
} else if (logLineList.StreamInstant) {
|
|
|
|
// TODO(zaphar): Handle this?
|
|
|
|
}
|
|
|
|
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
|
|
|
// @ts-ignore
|
|
|
|
Plotly.react(this.getTargetNode(), traces, layout, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the metrics graph with new data.
|
|
|
|
*
|
|
|
|
* @param {?QueryData=} graph
|
|
|
|
*/
|
|
|
|
updateMetricsGraph(graph) {
|
2024-02-24 19:53:25 -05:00
|
|
|
var data = graph.plots;
|
|
|
|
var yaxes = graph.yaxes;
|
2024-02-16 17:23:11 -05:00
|
|
|
var layout = {
|
2024-02-12 16:22:47 -06:00
|
|
|
displayModeBar: false,
|
2024-02-16 15:01:22 -05:00
|
|
|
responsive: true,
|
2024-03-05 20:39:03 -05:00
|
|
|
plot_bgcolor: getCssVariableValue('--plot-background-color').trim(),
|
2024-02-22 19:49:48 -05:00
|
|
|
paper_bgcolor: getCssVariableValue('--paper-background-color').trim(),
|
|
|
|
font: {
|
|
|
|
color: getCssVariableValue('--text-color').trim()
|
2024-02-22 20:19:54 -05:00
|
|
|
},
|
|
|
|
xaxis: {
|
2024-03-05 21:10:21 -05:00
|
|
|
gridcolor: getCssVariableValue("--grid-line-color")
|
2024-02-25 16:40:59 -05:00
|
|
|
},
|
|
|
|
legend: {
|
|
|
|
orientation: 'v'
|
2024-02-22 19:49:48 -05:00
|
|
|
}
|
2024-02-12 16:22:47 -06:00
|
|
|
};
|
2024-02-25 16:40:59 -05:00
|
|
|
if (graph.legend_orientation) {
|
|
|
|
layout.legend.orientation = graph.legend_orientation;
|
|
|
|
}
|
2024-02-24 19:53:25 -05:00
|
|
|
var nextYaxis = this.yaxisNameGenerator();
|
|
|
|
for (const yaxis of yaxes) {
|
|
|
|
yaxis.tickformat = yaxis.tickformat || this.#d3TickFormat;
|
2024-03-05 21:10:21 -05:00
|
|
|
yaxis.gridColor = getCssVariableValue("--grid-line-color");
|
2024-02-24 19:53:25 -05:00
|
|
|
layout[nextYaxis()] = yaxis;
|
|
|
|
}
|
2024-03-03 15:23:31 -05:00
|
|
|
var traces = /** @type {Array<PlotTrace>} */ ([]);
|
2024-02-16 17:23:11 -05:00
|
|
|
for (var subplot_idx in data) {
|
|
|
|
const subplot = data[subplot_idx];
|
2024-02-24 19:53:25 -05:00
|
|
|
var nextYaxis = this.yaxisNameGenerator();
|
2024-02-16 17:23:11 -05:00
|
|
|
if (subplot.Series) {
|
|
|
|
// https://plotly.com/javascript/reference/scatter/
|
2024-03-11 15:34:16 -04:00
|
|
|
for (const triple of subplot.Series) {
|
2024-03-10 21:05:49 -04:00
|
|
|
const trace = this.buildSeriesPlot(triple);
|
2024-03-10 21:11:32 -04:00
|
|
|
if (trace) {
|
2024-03-10 21:05:49 -04:00
|
|
|
traces.push(trace);
|
2024-02-16 17:23:11 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (subplot.Scalar) {
|
|
|
|
// https://plotly.com/javascript/reference/bar/
|
2024-03-11 15:34:16 -04:00
|
|
|
for (const triple of subplot.Scalar) {
|
2024-03-10 21:11:32 -04:00
|
|
|
const trace = this.buildScalarPlot(triple);
|
|
|
|
if (trace) {
|
|
|
|
traces.push(trace);
|
2024-02-21 17:20:07 -05:00
|
|
|
}
|
2024-02-07 19:18:30 -06:00
|
|
|
}
|
2024-03-04 20:52:36 -05:00
|
|
|
}
|
2024-02-07 19:18:30 -06:00
|
|
|
}
|
2024-02-16 17:23:11 -05:00
|
|
|
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
2024-03-03 15:23:31 -05:00
|
|
|
// @ts-ignore
|
2024-02-25 16:40:59 -05:00
|
|
|
Plotly.react(this.getTargetNode(), traces, layout, null);
|
2024-02-07 19:18:30 -06:00
|
|
|
}
|
|
|
|
}
|
2024-02-08 19:00:41 -06:00
|
|
|
|
2024-02-27 18:13:26 -05:00
|
|
|
GraphPlot.registerElement();
|
2024-02-14 19:45:28 -06:00
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/** Custom Element for selecting a timespan for the dashboard. */
|
2024-03-03 18:15:41 -05:00
|
|
|
export class SpanSelector extends HTMLElement {
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {HTMLElement} */
|
2024-02-14 19:45:28 -06:00
|
|
|
#targetNode = null;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {HTMLInputElement} */
|
2024-02-14 19:45:28 -06:00
|
|
|
#endInput = null;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {HTMLInputElement} */
|
2024-02-14 19:45:28 -06:00
|
|
|
#durationInput = null;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {HTMLInputElement} */
|
2024-02-14 19:45:28 -06:00
|
|
|
#stepDurationInput = null;
|
2024-03-03 15:23:31 -05:00
|
|
|
/** @type {HTMLButtonElement} */
|
2024-02-14 19:45:28 -06:00
|
|
|
#updateInput = null
|
2024-02-16 17:23:11 -05:00
|
|
|
|
2024-02-14 19:45:28 -06:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.#targetNode = this.appendChild(document.createElement('div'));
|
2024-02-16 17:23:11 -05:00
|
|
|
|
2024-02-14 19:45:28 -06:00
|
|
|
this.#targetNode.appendChild(document.createElement('span')).innerText = "end: ";
|
|
|
|
this.#endInput = this.#targetNode.appendChild(document.createElement('input'));
|
2024-02-16 17:23:11 -05:00
|
|
|
|
2024-02-14 19:45:28 -06:00
|
|
|
this.#targetNode.appendChild(document.createElement('span')).innerText = "duration: ";
|
|
|
|
this.#durationInput = this.#targetNode.appendChild(document.createElement('input'));
|
2024-02-16 17:23:11 -05:00
|
|
|
|
2024-02-14 19:45:28 -06:00
|
|
|
this.#targetNode.appendChild(document.createElement('span')).innerText = "step duration: ";
|
|
|
|
this.#stepDurationInput = this.#targetNode.appendChild(document.createElement('input'));
|
2024-02-16 17:23:11 -05:00
|
|
|
|
2024-02-14 19:45:28 -06:00
|
|
|
this.#updateInput = this.#targetNode.appendChild(document.createElement('button'));
|
|
|
|
this.#updateInput.innerText = "Update";
|
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
const self = this;
|
2024-02-22 20:19:54 -05:00
|
|
|
// TODO(jwall): We should probably show a loading indicator of some kind.
|
2024-02-14 19:45:28 -06:00
|
|
|
self.#updateInput.onclick = function(_evt) {
|
|
|
|
self.updateGraphs()
|
|
|
|
};
|
|
|
|
}
|
2024-02-16 17:23:11 -05:00
|
|
|
|
2024-02-14 19:45:28 -06:00
|
|
|
disconnectedCallback() {
|
|
|
|
this.#updateInput.onclick = undefined;
|
|
|
|
}
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/** Updates all the graphs on the dashboard with the new timespan. */
|
2024-02-14 19:45:28 -06:00
|
|
|
updateGraphs() {
|
2024-02-27 18:13:26 -05:00
|
|
|
for (var node of document.getElementsByTagName(GraphPlot.elementName)) {
|
2024-02-14 19:45:28 -06:00
|
|
|
node.setAttribute('end', this.#endInput.value);
|
|
|
|
node.setAttribute('duration', this.#durationInput.value);
|
|
|
|
node.setAttribute('step-duration', this.#stepDurationInput.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static elementName = "span-selector";
|
|
|
|
|
2024-03-03 15:23:31 -05:00
|
|
|
/** Register the element if it doesn't exist */
|
2024-02-14 19:45:28 -06:00
|
|
|
static registerElement() {
|
|
|
|
if (!customElements.get(SpanSelector.elementName)) {
|
|
|
|
customElements.define(SpanSelector.elementName, SpanSelector);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
SpanSelector.registerElement();
|
2024-03-03 18:15:41 -05:00
|
|
|
|