mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-23 04:29:48 -04:00
refactor: Split the log and graph elements
This commit is contained in:
parent
ae669767c8
commit
9c904a3c62
@ -154,7 +154,7 @@ pub fn log_component(dash_idx: usize, log_idx: usize, log: &LogStream) -> Markup
|
|||||||
html! {
|
html! {
|
||||||
div {
|
div {
|
||||||
h2 { (log.title) " - " a href=(log_embed_uri) { "embed url" } }
|
h2 { (log.title) " - " a href=(log_embed_uri) { "embed url" } }
|
||||||
graph-plot uri=(log_data_uri) id=(log_id) { }
|
log-plot uri=(log_data_uri) id=(log_id) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
662
static/lib.mjs
662
static/lib.mjs
@ -12,6 +12,19 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
function yaxisNameGenerator() {
|
||||||
|
var counter = 1;
|
||||||
|
return function() {
|
||||||
|
var name = "yaxis";
|
||||||
|
if (counter != 1) {
|
||||||
|
name = "yaxis" + counter;
|
||||||
|
}
|
||||||
|
counter++;
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map ansi terminal codes to html color codes.
|
* Map ansi terminal codes to html color codes.
|
||||||
* @param {string} line
|
* @param {string} line
|
||||||
@ -33,7 +46,7 @@ function ansiToHtml(line) {
|
|||||||
|
|
||||||
// NOTE(zaphar): Yes this is gross and I should really do a better parser but I'm lazy.
|
// 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
|
// Replace ANSI codes with HTML span elements styled with the corresponding color
|
||||||
return line.replace(/\x1b\[([0-9;]*)m/g, (match, p1) => {
|
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
|
const parts = p1.split(';'); // ANSI codes can be compounded, e.g., "1;31" for bold red
|
||||||
let styles = '';
|
let styles = '';
|
||||||
for (let part of parts) {
|
for (let part of parts) {
|
||||||
@ -47,6 +60,27 @@ function ansiToHtml(line) {
|
|||||||
}) + '</span>';
|
}) + '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the name for the plot trace.
|
||||||
|
* @param {PlotConfig} config
|
||||||
|
* @param {Map<string, string>} labels
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function formatName(config, labels) {
|
||||||
|
var name = "";
|
||||||
|
const formatter = config.name_format
|
||||||
|
if (formatter) {
|
||||||
|
name = eval(formatter);
|
||||||
|
} else {
|
||||||
|
var names = [];
|
||||||
|
for (const value of labels) {
|
||||||
|
names.push(value);
|
||||||
|
}
|
||||||
|
name = names.join(" ");
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get's a css variable's value from the document.
|
* Get's a css variable's value from the document.
|
||||||
* @param {string} variableName - Name of the variable to get `--var-name`
|
* @param {string} variableName - Name of the variable to get `--var-name`
|
||||||
@ -56,159 +90,77 @@ function getCssVariableValue(variableName) {
|
|||||||
return getComputedStyle(document.documentElement).getPropertyValue(variableName);
|
return getComputedStyle(document.documentElement).getPropertyValue(variableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
class ElementConfig {
|
||||||
* Custom element for showing a plotly graph.
|
uri;
|
||||||
*
|
|
||||||
* @extends HTMLElement
|
|
||||||
*/
|
|
||||||
export class GraphPlot extends HTMLElement {
|
|
||||||
/** @type {?string} */
|
|
||||||
#uri;
|
|
||||||
/** @type {?boolean} */
|
/** @type {?boolean} */
|
||||||
#allowUriFilters;
|
allowUriFilters;
|
||||||
/** @type {?number} */
|
/** @type {?number} */
|
||||||
#width;
|
width;
|
||||||
/** @type {?number} */
|
/** @type {?number} */
|
||||||
#height;
|
height;
|
||||||
/** @type {?number} */
|
/** @type {?number} */
|
||||||
#intervalId;
|
intervalId;
|
||||||
/** @type {?number} */
|
/** @type {?number} */
|
||||||
#pollSeconds;
|
pollSeconds;
|
||||||
/** @type {?string} */
|
/** @type {?string} */
|
||||||
#end;
|
end;
|
||||||
/** @type {?number} */
|
/** @type {?number} */
|
||||||
#duration;
|
duration;
|
||||||
/** @type {?string} */
|
/** @type {?string} */
|
||||||
#step_duration;
|
step_duration;
|
||||||
/** @type {?string} */
|
/** @type {?string} */
|
||||||
#d3TickFormat = "~s";
|
d3TickFormat = "~s";
|
||||||
/** @type {?HTMLDivElement} */
|
/** @type {?HTMLDivElement} */
|
||||||
#targetNode = null;
|
targetNode = null;
|
||||||
/** @type {?HTMLElement} */
|
/** @type {?HTMLElement} */
|
||||||
#menuContainer = null;
|
menuContainer = null;
|
||||||
/** @type {Object<string, HTMLSelectElement>} */
|
/** @type {Object<string, HTMLSelectElement>} */
|
||||||
#filterSelectElements = {};
|
filterSelectElements = {};
|
||||||
/** @type {Object<string, Array<string>>} */
|
/** @type {Object<string, Array<string>>} */
|
||||||
#filterLabels = {};
|
filterLabels = {};
|
||||||
/** @type {Object<string, Array<string>>} */
|
/** @type {Object<string, Array<string>>} */
|
||||||
#filteredLabelSets = {};
|
filteredLabelSets = {};
|
||||||
|
/** @type {?HTMLElement} */
|
||||||
|
#container = null;
|
||||||
|
|
||||||
constructor() {
|
constructor(/** @type {?HTMLElement} */ container) {
|
||||||
super();
|
this.#container = container;
|
||||||
this.#width = 800;
|
this.width = 800;
|
||||||
this.#height = 600;
|
this.height = 600;
|
||||||
this.#pollSeconds = 30;
|
this.pollSeconds = 30;
|
||||||
this.#menuContainer = this.appendChild(document.createElement('div'));
|
this.menuContainer = this.#container.appendChild(document.createElement('div'));
|
||||||
// TODO(jwall): These should probably be done as template clones so we have less places
|
// TODO(jwall): These should probably be done as template clones so we have less places
|
||||||
// to look for class attributes.
|
// to look for class attributes.
|
||||||
this.#menuContainer.setAttribute("class", "row-flex");
|
this.menuContainer.setAttribute("class", "row-flex");
|
||||||
this.#targetNode = this.appendChild(document.createElement("div"));
|
this.targetNode = this.#container.appendChild(document.createElement("div"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format', 'allow-uri-filter'];
|
connectedHandler(/** @type {HtmlElement} */ element) {
|
||||||
|
this.uri = element.getAttribute('uri') || this.uri;
|
||||||
|
this.width = Number(element.getAttribute('width') || this.width);
|
||||||
|
this.height = Number(element.getAttribute('height') || this.height);
|
||||||
|
this.pollSeconds = Number(element.getAttribute('poll-seconds') || this.pollSeconds);
|
||||||
|
this.end = element.getAttribute('end') || null;
|
||||||
|
this.duration = Number(element.getAttribute('duration')) || null;
|
||||||
|
this.step_duration = element.getAttribute('step-duration') || null;
|
||||||
|
this.d3TickFormat = element.getAttribute('d3-tick-format') || this.d3TickFormat;
|
||||||
|
this.allowUriFilters = Boolean(element.getAttribute('allow-uri-filters'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
stopInterval() {
|
||||||
* Callback for attributes changes.
|
if (this.intervalId) {
|
||||||
*
|
clearInterval(this.intervalId);
|
||||||
* @param {string} name - The name of the attribute.
|
this.intervalId = null;
|
||||||
* @param {?string} _oldValue - The old value for the attribute
|
|
||||||
* @param {?string} newValue - The new value for the attribute
|
|
||||||
*/
|
|
||||||
attributeChangedCallback(name, _oldValue, newValue) {
|
|
||||||
switch (name) {
|
|
||||||
case 'uri':
|
|
||||||
this.#uri = newValue;
|
|
||||||
break;
|
|
||||||
case 'width':
|
|
||||||
this.#width = Number(newValue);
|
|
||||||
break;
|
|
||||||
case 'height':
|
|
||||||
this.#height = Number(newValue);
|
|
||||||
break;
|
|
||||||
case 'poll-seconds':
|
|
||||||
this.#pollSeconds = Number(newValue);
|
|
||||||
break;
|
|
||||||
case 'end':
|
|
||||||
this.#end = newValue;
|
|
||||||
break;
|
|
||||||
case 'duration':
|
|
||||||
this.#duration = Number(newValue);
|
|
||||||
break;
|
|
||||||
case 'step-duration':
|
|
||||||
this.#step_duration = newValue;
|
|
||||||
break;
|
|
||||||
case 'd3-tick-format':
|
|
||||||
this.#d3TickFormat = newValue;
|
|
||||||
break;
|
|
||||||
case 'allow-uri-filters':
|
|
||||||
this.#allowUriFilters = Boolean(newValue);
|
|
||||||
break;
|
|
||||||
default: // do nothing;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
this.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.#uri = this.getAttribute('uri') || this.#uri;
|
|
||||||
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);
|
|
||||||
this.#end = this.getAttribute('end') || null;
|
|
||||||
this.#duration = Number(this.getAttribute('duration')) || null;
|
|
||||||
this.#step_duration = this.getAttribute('step-duration') || null;
|
|
||||||
this.#d3TickFormat = this.getAttribute('d3-tick-format') || this.#d3TickFormat;
|
|
||||||
this.#allowUriFilters = Boolean(this.getAttribute('allow-uri-filters'));
|
|
||||||
this.reset(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
this.stopInterval()
|
|
||||||
}
|
|
||||||
|
|
||||||
static elementName = "graph-plot";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get's the target node for placing the plotly graph.
|
* Get's the target node for placing the plotly graph.
|
||||||
*
|
*
|
||||||
* @returns {?HTMLDivElement}
|
* @returns {?HTMLDivElement}
|
||||||
*/
|
*/
|
||||||
getTargetNode() {
|
getTargetNode() {
|
||||||
return this.#targetNode;
|
return this.targetNode;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
stopInterval() {
|
|
||||||
if (this.#intervalId) {
|
|
||||||
clearInterval(this.#intervalId);
|
|
||||||
this.#intervalId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the entire graph and then restarts polling.
|
|
||||||
* @param {boolean=} updateOnly
|
|
||||||
*/
|
|
||||||
reset(updateOnly) {
|
|
||||||
var self = this;
|
|
||||||
self.stopInterval()
|
|
||||||
self.fetchData().then((data) => {
|
|
||||||
if (!updateOnly) {
|
|
||||||
self.getLabelsForData(data.Metrics || data.Logs.lines);
|
|
||||||
self.buildFilterMenu();
|
|
||||||
}
|
|
||||||
self.updateGraph(data).then(() => {
|
|
||||||
self.#intervalId = setInterval(() => self.updateGraph(), 1000 * self.#pollSeconds);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Registers the custom element if it doesn't already exist */
|
|
||||||
static registerElement() {
|
|
||||||
if (!customElements.get(GraphPlot.elementName)) {
|
|
||||||
customElements.define(GraphPlot.elementName, GraphPlot);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,21 +171,21 @@ export class GraphPlot extends HTMLElement {
|
|||||||
getUri() {
|
getUri() {
|
||||||
//var uriParts = [this.#uri];
|
//var uriParts = [this.#uri];
|
||||||
var uriParts = [];
|
var uriParts = [];
|
||||||
if (this.#end && this.#duration && this.#step_duration) {
|
if (this.end && this.duration && this.step_duration) {
|
||||||
uriParts.push("end=" + this.#end);
|
uriParts.push("end=" + this.end);
|
||||||
uriParts.push("duration=" + this.#duration);
|
uriParts.push("duration=" + this.duration);
|
||||||
uriParts.push("step_duration=" + this.#step_duration);
|
uriParts.push("step_duration=" + this.step_duration);
|
||||||
}
|
}
|
||||||
if (this.#allowUriFilters) {
|
if (this.allowUriFilters) {
|
||||||
for (const filterName in this.#filteredLabelSets) {
|
for (const filterName in this.filteredLabelSets) {
|
||||||
const filterVals = this.#filteredLabelSets[filterName].join("|");
|
const filterVals = this.filteredLabelSets[filterName].join("|");
|
||||||
uriParts.push(`filter-${filterName}=${filterVals}`)
|
uriParts.push(`filter-${filterName}=${filterVals}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uriParts) {
|
if (uriParts) {
|
||||||
return this.#uri + "?" + uriParts.join('&');
|
return this.uri + "?" + uriParts.join('&');
|
||||||
} else {
|
} else {
|
||||||
return this.#uri;
|
return this.uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,29 +202,8 @@ export class GraphPlot extends HTMLElement {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the name for the plot trace.
|
|
||||||
* @param {PlotConfig} config
|
|
||||||
* @param {Map<string, string>} labels
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
formatName(config, labels) {
|
|
||||||
var name = "";
|
|
||||||
const formatter = config.name_format
|
|
||||||
if (formatter) {
|
|
||||||
name = eval(formatter);
|
|
||||||
} else {
|
|
||||||
var names = [];
|
|
||||||
for (const value of labels) {
|
|
||||||
names.push(value);
|
|
||||||
}
|
|
||||||
name = names.join(" ");
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilterLabels() {
|
getFilterLabels() {
|
||||||
return this.#filterLabels;
|
return this.filterLabels;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -280,13 +211,13 @@ export class GraphPlot extends HTMLElement {
|
|||||||
*/
|
*/
|
||||||
populateFilterData(labels) {
|
populateFilterData(labels) {
|
||||||
for (var key in labels) {
|
for (var key in labels) {
|
||||||
const label = this.#filterLabels[key];
|
const label = this.filterLabels[key];
|
||||||
if (label) {
|
if (label) {
|
||||||
if (!label.includes(labels[key])) {
|
if (!label.includes(labels[key])) {
|
||||||
this.#filterLabels[key].push(labels[key]);
|
this.filterLabels[key].push(labels[key]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.#filterLabels[key] = [labels[key]];
|
this.filterLabels[key] = [labels[key]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,7 +226,7 @@ export class GraphPlot extends HTMLElement {
|
|||||||
* @param {string} key
|
* @param {string} key
|
||||||
* @returns {HTMLDivElement}
|
* @returns {HTMLDivElement}
|
||||||
*/
|
*/
|
||||||
buildSelectElement(key) {
|
buildSelectElement(key, me) {
|
||||||
var id = key + "-select" + Math.random();
|
var id = key + "-select" + Math.random();
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
const select = document.createElement("select");
|
const select = document.createElement("select");
|
||||||
@ -307,7 +238,7 @@ export class GraphPlot extends HTMLElement {
|
|||||||
const optValue = "Select All: " + key;
|
const optValue = "Select All: " + key;
|
||||||
optElement.innerText = optValue;
|
optElement.innerText = optValue;
|
||||||
select.appendChild(optElement);
|
select.appendChild(optElement);
|
||||||
for (var opt of this.#filterLabels[key]) {
|
for (var opt of this.filterLabels[key]) {
|
||||||
const optElement = document.createElement("option");
|
const optElement = document.createElement("option");
|
||||||
optElement.setAttribute("value", opt);
|
optElement.setAttribute("value", opt);
|
||||||
optElement.setAttribute("selected", "selected");
|
optElement.setAttribute("selected", "selected");
|
||||||
@ -344,36 +275,126 @@ export class GraphPlot extends HTMLElement {
|
|||||||
filteredValues.push(o.value);
|
filteredValues.push(o.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.#filteredLabelSets[key] = filteredValues;
|
self.filteredLabelSets[key] = filteredValues;
|
||||||
self.reset(true);
|
me.reset(true);
|
||||||
};
|
};
|
||||||
element.appendChild(select);
|
element.appendChild(select);
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFilterMenu() {
|
// FIXME(jwall): We pass the element down but that couples a little too tightly. We should do this differently.
|
||||||
|
buildFilterMenu(me) {
|
||||||
// We need to maintain a stable order for these
|
// We need to maintain a stable order for these
|
||||||
var children = [];
|
var children = [];
|
||||||
for (var key of Object.keys(this.#filterLabels).sort()) {
|
for (var key of Object.keys(this.filterLabels).sort()) {
|
||||||
// If there are multiple items to filter by then show the selectElement.
|
// If there are multiple items to filter by then show the selectElement.
|
||||||
// otherwise there is no point.
|
// otherwise there is no point.
|
||||||
if (this.#filterLabels[key].length > 1) {
|
if (this.filterLabels[key].length > 1) {
|
||||||
const element = this.#filterSelectElements[key] || this.buildSelectElement(key);
|
const element = this.filterSelectElements[key] || this.buildSelectElement(key, me);
|
||||||
children.push(element);
|
children.push(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#menuContainer.replaceChildren(...children);
|
this.menuContainer.replaceChildren(...children);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedHandler(name, newValue) {
|
||||||
|
switch (name) {
|
||||||
|
case 'uri':
|
||||||
|
this.uri = newValue;
|
||||||
|
break;
|
||||||
|
case 'width':
|
||||||
|
this.width = Number(newValue);
|
||||||
|
break;
|
||||||
|
case 'height':
|
||||||
|
this.height = Number(newValue);
|
||||||
|
break;
|
||||||
|
case 'poll-seconds':
|
||||||
|
this.pollSeconds = Number(newValue);
|
||||||
|
break;
|
||||||
|
case 'end':
|
||||||
|
this.end = newValue;
|
||||||
|
break;
|
||||||
|
case 'duration':
|
||||||
|
this.config.duration = Number(newValue);
|
||||||
|
break;
|
||||||
|
case 'step-duration':
|
||||||
|
this.step_duration = newValue;
|
||||||
|
break;
|
||||||
|
case 'd3-tick-format':
|
||||||
|
this.config.d3TickFormat = newValue;
|
||||||
|
break;
|
||||||
|
case 'allow-uri-filters':
|
||||||
|
this.allowUriFilters = Boolean(newValue);
|
||||||
|
break;
|
||||||
|
default: // do nothing;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom element for showing Log Output.
|
||||||
|
*
|
||||||
|
* @extends HTMLElement
|
||||||
|
*/
|
||||||
|
export class LogPlot extends HTMLElement {
|
||||||
|
/** @type {?ElementConfig} */
|
||||||
|
#config;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#config = new ElementConfig(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format', 'allow-uri-filter'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
attributeChangedCallback(name, _oldValue, newValue) {
|
||||||
|
this.#config.attributeChangedHandler(name, newValue);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.#config.connectedHandler(this);
|
||||||
|
this.reset(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.#config.stopInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
static elementName = "log-plot";
|
||||||
|
|
||||||
|
/** Registers the custom element if it doesn't already exist */
|
||||||
|
static registerElement() {
|
||||||
|
if (!customElements.get(LogPlot.elementName)) {
|
||||||
|
customElements.define(LogPlot.elementName, LogPlot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {QueryData|LogLineList} graph
|
* Resets the entire graph and then restarts polling.
|
||||||
*/
|
* @param {boolean=} updateOnly
|
||||||
getLabelsForData(graph) {
|
*/
|
||||||
if (/** @type {QueryData} */(graph).plots) {
|
reset(updateOnly) {
|
||||||
this.getLabelsForQueryData(/** @type {QueryData} */(graph));
|
var self = this;
|
||||||
} else {
|
self.#config.stopInterval()
|
||||||
this.getLabelsForLogLines(/** @type {LogLineList} */(graph));
|
self.#config.fetchData().then((data) => {
|
||||||
}
|
if (!updateOnly) {
|
||||||
|
self.getLabelsForLogLines(data.Metrics || data.Logs.lines);
|
||||||
|
self.#config.buildFilterMenu(this);
|
||||||
|
}
|
||||||
|
self.updateGraph(data).then(() => {
|
||||||
|
self.#config.intervalId = setInterval(() => self.updateGraph(), 1000 * self.#config.pollSeconds);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -383,7 +404,7 @@ export class GraphPlot extends HTMLElement {
|
|||||||
if (graph.Stream) {
|
if (graph.Stream) {
|
||||||
for (const pair of graph.Stream) {
|
for (const pair of graph.Stream) {
|
||||||
const labels = pair[0];
|
const labels = pair[0];
|
||||||
this.populateFilterData(labels);
|
this.#config.populateFilterData(labels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (graph.StreamInstant) {
|
if (graph.StreamInstant) {
|
||||||
@ -391,102 +412,6 @@ export class GraphPlot extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {QueryData} graph
|
|
||||||
*/
|
|
||||||
getLabelsForQueryData(graph) {
|
|
||||||
const data = graph.plots;
|
|
||||||
for (var subplot of data) {
|
|
||||||
if (subplot.Series) {
|
|
||||||
for (const triple of subplot.Series) {
|
|
||||||
const labels = triple[0];
|
|
||||||
this.populateFilterData(labels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (subplot.Scalar) {
|
|
||||||
for (const triple of subplot.Scalar) {
|
|
||||||
const labels = triple[0];
|
|
||||||
this.populateFilterData(labels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yaxisNameGenerator() {
|
|
||||||
var counter = 1;
|
|
||||||
return function() {
|
|
||||||
var name = "yaxis";
|
|
||||||
if (counter != 1) {
|
|
||||||
name = "yaxis" + counter;
|
|
||||||
}
|
|
||||||
counter++;
|
|
||||||
return name;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} triple
|
|
||||||
*/
|
|
||||||
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 config = /** @type {PlotConfig} */(triple[1]);
|
|
||||||
var yaxis = config.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 (config.fill) {
|
|
||||||
trace.fill = config.fill;
|
|
||||||
}
|
|
||||||
var name = this.formatName(config, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 config = /** @type {PlotConfig} */(triple[1]);
|
|
||||||
const series = triple[2];
|
|
||||||
const trace = /** @type GraphTrace */({
|
|
||||||
type: "bar",
|
|
||||||
x: [],
|
|
||||||
y: [],
|
|
||||||
yhoverformat: config["d3_tick_format"],
|
|
||||||
});
|
|
||||||
var name = this.formatName(config, labels);
|
|
||||||
if (name) { trace.name = name; }
|
|
||||||
trace.y.push(series.value);
|
|
||||||
trace.x.push(trace.name);
|
|
||||||
return trace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array} stream
|
* @param {Array} stream
|
||||||
*
|
*
|
||||||
@ -501,7 +426,7 @@ export class GraphPlot extends HTMLElement {
|
|||||||
const labels = pair[0];
|
const labels = pair[0];
|
||||||
var labelList = [];
|
var labelList = [];
|
||||||
for (var label in labels) {
|
for (var label in labels) {
|
||||||
var show = this.#filteredLabelSets[label];
|
var show = this.#config.filteredLabelSets[label];
|
||||||
if (show && !show.includes(labels[label])) {
|
if (show && !show.includes(labels[label])) {
|
||||||
continue loopStream;
|
continue loopStream;
|
||||||
}
|
}
|
||||||
@ -528,10 +453,10 @@ export class GraphPlot extends HTMLElement {
|
|||||||
async updateGraph(maybeGraph) {
|
async updateGraph(maybeGraph) {
|
||||||
var graph = maybeGraph;
|
var graph = maybeGraph;
|
||||||
if (!graph) {
|
if (!graph) {
|
||||||
graph = await this.fetchData();
|
graph = await this.#config.fetchData();
|
||||||
}
|
}
|
||||||
if (graph.Metrics) {
|
if (graph.Metrics) {
|
||||||
this.updateMetricsGraph(graph.Metrics);
|
// FIXME(zaphar): Log an Error;
|
||||||
} else if (graph.Logs) {
|
} else if (graph.Logs) {
|
||||||
this.updateLogsView(graph.Logs.lines);
|
this.updateLogsView(graph.Logs.lines);
|
||||||
} else {
|
} else {
|
||||||
@ -588,7 +513,177 @@ export class GraphPlot extends HTMLElement {
|
|||||||
}
|
}
|
||||||
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
Plotly.react(this.getTargetNode(), traces, layout, null);
|
Plotly.react(this.#config.getTargetNode(), traces, layout, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LogPlot.registerElement();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom element for showing a plotly graph.
|
||||||
|
*
|
||||||
|
* @extends HTMLElement
|
||||||
|
*/
|
||||||
|
export class GraphPlot extends HTMLElement {
|
||||||
|
/** @type {?ElementConfig} */
|
||||||
|
#config;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#config = new ElementConfig(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format', 'allow-uri-filter'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
attributeChangedCallback(name, _oldValue, newValue) {
|
||||||
|
this.#config.attributeChangedHandler(name, newValue);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.#config.connectedHandler(this);
|
||||||
|
this.reset(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.#config.stopInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
static elementName = "graph-plot";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the entire graph and then restarts polling.
|
||||||
|
* @param {boolean=} updateOnly
|
||||||
|
*/
|
||||||
|
reset(updateOnly) {
|
||||||
|
var self = this;
|
||||||
|
self.#config.stopInterval()
|
||||||
|
self.#config.fetchData().then((data) => {
|
||||||
|
if (!updateOnly) {
|
||||||
|
self.getLabelsForQueryData(data.Metrics || data.Logs.lines);
|
||||||
|
self.#config.buildFilterMenu(this);
|
||||||
|
}
|
||||||
|
self.updateGraph(data).then(() => {
|
||||||
|
self.#config.intervalId = setInterval(() => self.updateGraph(), 1000 * self.#config.pollSeconds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers the custom element if it doesn't already exist */
|
||||||
|
static registerElement() {
|
||||||
|
if (!customElements.get(GraphPlot.elementName)) {
|
||||||
|
customElements.define(GraphPlot.elementName, GraphPlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {QueryData} graph
|
||||||
|
*/
|
||||||
|
getLabelsForQueryData(graph) {
|
||||||
|
const data = graph.plots;
|
||||||
|
for (var subplot of data) {
|
||||||
|
if (subplot.Series) {
|
||||||
|
for (const triple of subplot.Series) {
|
||||||
|
const labels = triple[0];
|
||||||
|
this.#config.populateFilterData(labels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subplot.Scalar) {
|
||||||
|
for (const triple of subplot.Scalar) {
|
||||||
|
const labels = triple[0];
|
||||||
|
this.#config.populateFilterData(labels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} triple
|
||||||
|
*/
|
||||||
|
buildSeriesPlot(triple) {
|
||||||
|
const labels = /** @type {Map<String, String>} */(triple[0]);
|
||||||
|
for (var label in labels) {
|
||||||
|
var show = this.#config.filteredLabelSets[label];
|
||||||
|
if (show && !show.includes(labels[label])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const config = /** @type {PlotConfig} */(triple[1]);
|
||||||
|
var yaxis = config.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 (config.fill) {
|
||||||
|
trace.fill = config.fill;
|
||||||
|
}
|
||||||
|
var name = formatName(config, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} triple
|
||||||
|
*/
|
||||||
|
buildScalarPlot(triple) {
|
||||||
|
const labels = /** @type {Map<String,String>} */(triple[0]);
|
||||||
|
for (var label in labels) {
|
||||||
|
var show = this.#config.filteredLabelSets[label];
|
||||||
|
if (show && !show.includes(labels[label])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const config = /** @type {PlotConfig} */(triple[1]);
|
||||||
|
const series = triple[2];
|
||||||
|
const trace = /** @type GraphTrace */({
|
||||||
|
type: "bar",
|
||||||
|
x: [],
|
||||||
|
y: [],
|
||||||
|
yhoverformat: config["d3_tick_format"],
|
||||||
|
});
|
||||||
|
var name = formatName(config, labels);
|
||||||
|
if (name) { trace.name = name; }
|
||||||
|
trace.y.push(series.value);
|
||||||
|
trace.x.push(trace.name);
|
||||||
|
return trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the graph with new data.
|
||||||
|
*
|
||||||
|
* @param {?QueryPayload=} maybeGraph
|
||||||
|
*/
|
||||||
|
async updateGraph(maybeGraph) {
|
||||||
|
var graph = maybeGraph;
|
||||||
|
if (!graph) {
|
||||||
|
graph = await this.#config.fetchData();
|
||||||
|
}
|
||||||
|
if (graph.Metrics) {
|
||||||
|
this.updateMetricsGraph(graph.Metrics);
|
||||||
|
} else if (graph.Logs) {
|
||||||
|
// FIXME(zaphar): Log an Error;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -617,16 +712,16 @@ export class GraphPlot extends HTMLElement {
|
|||||||
if (graph.legend_orientation) {
|
if (graph.legend_orientation) {
|
||||||
layout.legend.orientation = graph.legend_orientation;
|
layout.legend.orientation = graph.legend_orientation;
|
||||||
}
|
}
|
||||||
var nextYaxis = this.yaxisNameGenerator();
|
var nextYaxis = yaxisNameGenerator();
|
||||||
for (const yaxis of yaxes) {
|
for (const yaxis of yaxes) {
|
||||||
yaxis.tickformat = yaxis.tickformat || this.#d3TickFormat;
|
yaxis.tickformat = yaxis.tickformat || this.#config.d3TickFormat;
|
||||||
yaxis.gridColor = getCssVariableValue("--grid-line-color");
|
yaxis.gridColor = getCssVariableValue("--grid-line-color");
|
||||||
layout[nextYaxis()] = yaxis;
|
layout[nextYaxis()] = yaxis;
|
||||||
}
|
}
|
||||||
var traces = /** @type {Array<PlotTrace>} */ ([]);
|
var traces = /** @type {Array<PlotTrace>} */ ([]);
|
||||||
for (var subplot_idx in data) {
|
for (var subplot_idx in data) {
|
||||||
const subplot = data[subplot_idx];
|
const subplot = data[subplot_idx];
|
||||||
var nextYaxis = this.yaxisNameGenerator();
|
var nextYaxis = yaxisNameGenerator();
|
||||||
if (subplot.Series) {
|
if (subplot.Series) {
|
||||||
// https://plotly.com/javascript/reference/scatter/
|
// https://plotly.com/javascript/reference/scatter/
|
||||||
for (const triple of subplot.Series) {
|
for (const triple of subplot.Series) {
|
||||||
@ -647,7 +742,7 @@ export class GraphPlot extends HTMLElement {
|
|||||||
}
|
}
|
||||||
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
Plotly.react(this.getTargetNode(), traces, layout, null);
|
Plotly.react(this.#config.getTargetNode(), traces, layout, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -702,6 +797,11 @@ export class SpanSelector extends HTMLElement {
|
|||||||
node.setAttribute('duration', this.#durationInput.value);
|
node.setAttribute('duration', this.#durationInput.value);
|
||||||
node.setAttribute('step-duration', this.#stepDurationInput.value);
|
node.setAttribute('step-duration', this.#stepDurationInput.value);
|
||||||
}
|
}
|
||||||
|
for (var node of document.getElementsByTagName(LogPlot.elementName)) {
|
||||||
|
node.setAttribute('end', this.#endInput.value);
|
||||||
|
node.setAttribute('duration', this.#durationInput.value);
|
||||||
|
node.setAttribute('step-duration', this.#stepDurationInput.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static elementName = "span-selector";
|
static elementName = "span-selector";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user