From fb48c6900c8c10599247d7fc6d08ecfde70ec4bd Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 3 Mar 2024 15:23:31 -0500 Subject: [PATCH] maint: JSDoc type annotations and tsserver configuration --- jsconfig.json | 11 ++++ static/lib.js | 147 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 jsconfig.json diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..679d8ed --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2022", + "noImplicitThis": true, + "checkJs": true, + "allowJs": true + }, + "include": [ + "static/*.js" + ] +} diff --git a/static/lib.js b/static/lib.js index e692448..2cbce54 100644 --- a/static/lib.js +++ b/static/lib.js @@ -12,25 +12,79 @@ // See the License for the specific language governing permissions and // limitations under the License. +/** + * @typedef PlotList + * @type {object} + * @property {?Array} Series + * @property {?Array} Scalar + * @property {?Array} StreamInstant + * @property {?Array} Stream + */ + +/** + * @typedef QueryData + * @type {object} + * @property {object} yaxes + * @property {?string} legend_orientation + * @property {Array} plots + */ + +/** + * @typedef PlotTrace + * @type {object} + * @property {string=} name + * @property type {string} + * @property {string=} mode + * @property {Array} x + * @property {Array} y + * @peroperty {string=} xaxis + * @peroperty {string=} yaxis +*/ + +/** + * Get's a css variable's value from the document. + * @param {string} variableName - Name of the variable to get `--var-name` + * @returns string + */ function getCssVariableValue(variableName) { return getComputedStyle(document.documentElement).getPropertyValue(variableName); } +/** + * Custom element for showing a plotly graph. + * + * @extends HTMLElement + */ class GraphPlot extends HTMLElement { + /** @type {?string} */ #uri; + /** @type {?number} */ #width; + /** @type {?number} */ #height; + /** @type {?number} */ #intervalId; + /** @type {?number} */ #pollSeconds; + /** @type {?string} */ #end; + /** @type {?number} */ #duration; + /** @type {?string} */ #step_duration; + /** @type {?string} */ #d3TickFormat = "~s"; + /** @type {?HTMLDivElement} */ #targetNode = null; + /** @type {?HTMLElement} */ #menuContainer = null; + /** @type {Object} */ #filterSelectElements = {}; + /** @type {Object>} */ #filterLabels = {}; + /** @type {Object>} */ #filteredLabelSets = {}; + constructor() { super(); this.#width = 800; @@ -45,25 +99,32 @@ class GraphPlot extends HTMLElement { static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format']; + /** + * 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) { switch (name) { case 'uri': this.#uri = newValue; break; case 'width': - this.#width = newValue; + this.#width = Number(newValue); break; case 'height': - this.#height = newValue; + this.#height = Number(newValue); break; case 'poll-seconds': - this.#pollSeconds = newValue; + this.#pollSeconds = Number(newValue); break; case 'end': this.#end = newValue; break; case 'duration': - this.#duration = newValue; + this.#duration = Number(newValue); break; case 'step-duration': this.#step_duration = newValue; @@ -79,11 +140,11 @@ class GraphPlot extends HTMLElement { connectedCallback() { this.#uri = this.getAttribute('uri') || this.#uri; - this.#width = this.getAttribute('width') || this.#width; - this.#height = this.getAttribute('height') || this.#height; - this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds; + 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 = this.getAttribute('duration') || 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.reset(); @@ -95,10 +156,17 @@ class GraphPlot extends HTMLElement { static elementName = "graph-plot"; + /* + * Get's the target node for placing the plotly graph. + * + * @returns {?HTMLDivElement} + */ getTargetNode() { return this.#targetNode; } + /** + */ stopInterval() { if (this.#intervalId) { clearInterval(this.#intervalId); @@ -106,6 +174,10 @@ class GraphPlot extends HTMLElement { } } + /** + * Resets the entire graph and then restarts polling. + * @param {boolean=} updateOnly + */ reset(updateOnly) { var self = this; self.stopInterval() @@ -120,12 +192,18 @@ class GraphPlot extends HTMLElement { }); } + /** Registers the custom element if it doesn't already exist */ static registerElement() { if (!customElements.get(GraphPlot.elementName)) { customElements.define(GraphPlot.elementName, GraphPlot); } } + /** + * Returns the uri formatted with any query strings if necessary. + * + * @returns {string} + */ getUri() { if (this.#end && this.#duration && this.#step_duration) { return this.#uri + "?end=" + this.#end + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration; @@ -134,6 +212,11 @@ class GraphPlot extends HTMLElement { } } + /** + * Returns the data from an api call. + * + * @return {Promise} + */ async fetchData() { // TODO(zaphar): Can we do some massaging on these // to get the full set of labels and possible values? @@ -142,6 +225,12 @@ class GraphPlot extends HTMLElement { return data; } + /** + * Formats the name for the plot trace. + * @param {{name_format: ?string}} meta + * @param {Map} labels + * @return string + */ formatName(meta, labels) { var name = ""; const formatter = meta.name_format @@ -157,6 +246,9 @@ class GraphPlot extends HTMLElement { return name; } + /** + * @param {Object} labels + */ populateFilterData(labels) { for (var key in labels) { const label = this.#filterLabels[key]; @@ -170,13 +262,18 @@ class GraphPlot extends HTMLElement { } } + /** + * @param {string} key + * @returns {HTMLDivElement} + */ buildSelectElement(key) { // TODO(jwall): Should we have a select all? var id = key + "-select" + Math.random(); const element = document.createElement("div"); const select = document.createElement("select"); select.setAttribute("name", id); - select.setAttribute("multiple", true); + // TODO(jwall): This is how you set boolean attributes. Use the attribute named... :-( + select.setAttribute("multiple", "multiple"); const optElement = document.createElement("option"); const optValue = "Select " + key; optElement.innerText = optValue; @@ -184,7 +281,7 @@ class GraphPlot extends HTMLElement { for (var opt of this.#filterLabels[key]) { const optElement = document.createElement("option"); optElement.setAttribute("value", opt); - optElement.setAttribute("selected", true); + optElement.setAttribute("selected", "selected"); optElement.innerText = opt; select.appendChild(optElement); } @@ -193,7 +290,7 @@ class GraphPlot extends HTMLElement { select.onchange = function(evt) { evt.stopPropagation(); var filteredValues = []; - for (var opt of evt.target.selectedOptions) { + for (var opt of /** @type {HTMLSelectElement} */(evt.target).selectedOptions) { filteredValues.push(opt.getAttribute("value")); } self.#filteredLabelSets[key] = filteredValues; @@ -217,6 +314,9 @@ class GraphPlot extends HTMLElement { this.#menuContainer.replaceChildren(...children); } + /** + * @param {QueryData} graph + */ getLabelsForData(graph) { const data = graph.plots; for (var subplot of data) { @@ -247,6 +347,11 @@ class GraphPlot extends HTMLElement { }; } + /** + * Update the graph with new data. + * + * @param {?QueryData=} maybeGraph + */ async updateGraph(maybeGraph) { var graph = maybeGraph; if (!graph) { @@ -278,10 +383,9 @@ class GraphPlot extends HTMLElement { yaxis.gridColor = getCssVariableValue("--accent-color"); layout[nextYaxis()] = yaxis; } - var traces = []; + var traces = /** @type {Array} */ ([]); for (var subplot_idx in data) { const subplot = data[subplot_idx]; - const subplotCount = Number(subplot_idx) + 1; var nextYaxis = this.yaxisNameGenerator(); if (subplot.Series) { // https://plotly.com/javascript/reference/scatter/ @@ -297,7 +401,7 @@ class GraphPlot extends HTMLElement { var yaxis = meta.yaxis || "y"; // https://plotly.com/javascript/reference/layout/yaxis/ const series = triple[2]; - var trace = { + const trace = { type: "scatter", mode: "lines+text", x: [], @@ -330,32 +434,39 @@ class GraphPlot extends HTMLElement { } const meta = triple[1]; const series = triple[2]; - var trace = { + const trace = /** @type PlotTrace */({ 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); traces.push(trace); } - } + } // TODO(zaphar): subplot.Stream // log lines!!! } // https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact + // @ts-ignore Plotly.react(this.getTargetNode(), traces, layout, null); } } GraphPlot.registerElement(); +/** Custom Element for selecting a timespan for the dashboard. */ class SpanSelector extends HTMLElement { + /** @type {HTMLElement} */ #targetNode = null; + /** @type {HTMLInputElement} */ #endInput = null; + /** @type {HTMLInputElement} */ #durationInput = null; + /** @type {HTMLInputElement} */ #stepDurationInput = null; + /** @type {HTMLButtonElement} */ #updateInput = null constructor() { @@ -387,6 +498,7 @@ class SpanSelector extends HTMLElement { this.#updateInput.onclick = undefined; } + /** Updates all the graphs on the dashboard with the new timespan. */ updateGraphs() { for (var node of document.getElementsByTagName(GraphPlot.elementName)) { node.setAttribute('end', this.#endInput.value); @@ -397,6 +509,7 @@ class SpanSelector extends HTMLElement { static elementName = "span-selector"; + /** Register the element if it doesn't exist */ static registerElement() { if (!customElements.get(SpanSelector.elementName)) { customElements.define(SpanSelector.elementName, SpanSelector);