2022-08-21 10:11:25 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< title > ClickHouse Dashboard< / title >
< link rel = "icon" href = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDkgOCI+PHN0eWxlPi5ve2ZpbGw6I2ZjMH0ucntmaWxsOnJlZH08L3N0eWxlPjxwYXRoIGQ9Ik0wLDcgaDEgdjEgaC0xIHoiIGNsYXNzPSJyIi8+PHBhdGggZD0iTTAsMCBoMSB2NyBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNMiwwIGgxIHY4IGgtMSB6IiBjbGFzcz0ibyIvPjxwYXRoIGQ9Ik00LDAgaDEgdjggaC0xIHoiIGNsYXNzPSJvIi8+PHBhdGggZD0iTTYsMCBoMSB2OCBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNOCwzLjI1IGgxIHYxLjUgaC0xIHoiIGNsYXNzPSJvIi8+PC9zdmc+" >
< script src = "https://cdn.jsdelivr.net/npm/uplot@1.6.21/dist/uPlot.iife.min.js" > < / script >
2024-01-23 15:03:15 +00:00
< script src = "https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js" > < / script >
2022-08-21 10:11:25 +00:00
< style >
:root {
--color: black;
2024-03-03 03:35:15 +00:00
--background-color-1: #00CCFF;
--background: linear-gradient(to bottom, var(--background-color-1), #00D0D0);
2022-08-21 10:11:25 +00:00
--chart-background: white;
--shadow-color: rgba(0, 0, 0, 0.25);
2023-08-21 04:23:58 +00:00
--moving-shadow-color: rgba(0, 0, 0, 0.5);
2022-08-21 10:11:25 +00:00
--input-shadow-color: rgba(0, 255, 0, 1);
2023-07-03 16:42:51 +00:00
--error-color: red;
2023-11-16 12:17:22 +00:00
--global-error-color: white;
2024-08-02 23:31:02 +00:00
--legend-background: rgba(255, 255, 0, 0.75);
2022-08-21 10:11:25 +00:00
--title-color: #666;
--text-color: black;
--edit-title-background: #FEE;
--edit-title-border: #F88;
--button-background-color: #FFCB80;
--button-text-color: black;
--new-chart-background-color: #EEE;
--new-chart-text-color: black;
--param-background-color: #EEE;
--param-text-color: black;
--input-background: white;
--chart-button-hover-color: red;
}
[data-theme="dark"] {
--color: white;
2024-03-03 03:35:15 +00:00
--background-color-1: #151C2C;
--background: var(--background-color-1);
2022-08-21 10:11:25 +00:00
--chart-background: #1b2834;
--shadow-color: rgba(0, 0, 0, 0);
2023-08-21 04:23:58 +00:00
--moving-shadow-color: rgba(255, 255, 255, 0.25);
2022-08-21 10:11:25 +00:00
--input-shadow-color: rgba(255, 128, 0, 0.25);
--error-color: #F66;
2024-08-02 23:31:02 +00:00
--legend-background: rgba(0, 96, 128, 0.75);
2022-08-21 10:11:25 +00:00
--title-color: white;
--text-color: white;
--edit-title-background: #364f69;
--edit-title-border: #333;
--button-background-color: orange;
--button-text-color: black;
--new-chart-background-color: #666;
--new-chart-text-color: white;
--param-background-color: #666;
--param-text-color: white;
--input-background: #364f69;
--chart-button-hover-color: #F40;
}
* {
box-sizing: border-box;
}
html, body {
color: var(--color);
height: 100%;
overflow: auto;
margin: 0;
}
body {
font-family: Liberation Sans, DejaVu Sans, sans-serif, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji;
padding: 1rem;
overflow-x: hidden;
background: var(--background);
display: grid;
grid-template-columns: auto;
grid-template-rows: fit-content(10%) auto;
}
input {
/* iPad, Safari */
border-radius: 0;
margin: 0;
}
#charts
{
height: 100%;
2023-04-21 20:06:12 +00:00
display: flex;
2022-08-21 10:11:25 +00:00
flex-flow: row wrap;
gap: 1rem;
}
.chart {
2024-02-25 21:36:59 +00:00
flex: 1 1 40rem;
2022-08-21 10:11:25 +00:00
min-height: 16rem;
background: var(--chart-background);
2024-02-25 21:36:59 +00:00
box-shadow: 1px 1px 0 var(--shadow-color);
2022-08-21 10:11:25 +00:00
overflow: hidden;
position: relative;
}
2023-08-21 04:23:58 +00:00
.chart-maximized {
flex: 1 100%;
height: 75vh
}
.chart-moving {
z-index: 11;
box-shadow: 0 0 2rem var(--moving-shadow-color);
}
.chart-displaced {
opacity: 75%;
filter: blur(1px);
}
2023-11-28 00:36:38 +00:00
.chart > div { position: absolute; }
2022-08-21 10:11:25 +00:00
2023-02-15 20:09:36 +00:00
.inputs {
height: auto;
width: 100%;
font-size: 14pt;
display: flex;
flex-flow: column nowrap;
justify-content: center;
2024-03-03 03:35:15 +00:00
position: sticky;
top: -1rem;
margin-top: -1rem;
margin-left: -1rem;
margin-right: -1rem;
border-top: 1rem solid var(--background-color-1);
border-left: 1rem solid var(--background-color-1);
border-right: 1rem solid var(--background-color-1);
box-sizing: content-box;
z-index: 1000;
background: var(--background-color-1);
2023-02-15 20:09:36 +00:00
}
.inputs.unconnected {
height: 100vh;
}
.unconnected #params {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
.unconnected #connection-params {
width: 50%;
display: flex;
2023-02-21 18:52:28 +00:00
flex-flow: column nowrap;
2023-02-15 20:09:36 +00:00
}
.unconnected #url {
width: 100%;
}
2023-04-21 20:06:12 +00:00
.unconnected #button-options {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 0.3rem;
}
.unconnected #user {
margin-right: 0;
width: auto;
}
.unconnected #password {
width: auto;
}
2023-02-21 18:52:28 +00:00
#user {
margin-right: 0.25rem;
2023-02-15 20:09:36 +00:00
width: 50%;
}
2023-02-21 18:52:28 +00:00
#password {
2023-02-15 20:09:36 +00:00
width: 49.5%;
}
.unconnected input {
margin-bottom: 5px;
}
2023-02-21 18:52:28 +00:00
#username-password {
width: 100%;
display: flex;
2023-04-21 20:06:12 +00:00
flex-flow: row nowrap;
}
.unconnected #username-password {
width: 100%;
gap: 0.3rem;
display: grid;
grid-template-columns: 1fr 1fr;
2023-02-21 18:52:28 +00:00
}
2023-02-15 20:09:36 +00:00
.inputs #chart-params {
display: block;
}
.inputs.unconnected #chart-params {
display: none;
}
2022-08-21 10:11:25 +00:00
#connection-params {
margin-bottom: 0.5rem;
display: grid;
2023-02-21 18:52:28 +00:00
grid-template-columns: 69.77% 30%;
2022-08-21 10:11:25 +00:00
column-gap: 0.25rem;
}
.inputs input {
2024-02-25 21:36:59 +00:00
box-shadow: 1px 1px 0 var(--shadow-color);
2022-08-21 10:11:25 +00:00
padding: 0.25rem;
}
#chart-params input {
margin-right: 0.25rem;
}
2023-11-14 23:03:06 +00:00
#chart-params .param {
width: 6%;
}
2022-08-21 10:11:25 +00:00
input {
font-family: Liberation Sans, DejaVu Sans, sans-serif, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji;
outline: none;
border: none;
font-size: 14pt;
background-color: var(--input-background);
color: var(--text-color);
}
.themes {
float: right;
font-size: 20pt;
2023-04-21 20:06:12 +00:00
gap: 0.3rem;
display: flex;
justify-content: center;
2022-08-21 10:11:25 +00:00
}
#toggle-dark, #toggle-light {
padding-right: 0.5rem;
user-select: none;
cursor: pointer;
}
#toggle-dark:hover, #toggle-light:hover {
display: inline-block;
transform: translate(1px, 1px);
filter: brightness(125%);
}
#run {
background: var(--button-background-color);
color: var(--button-text-color);
font-weight: bold;
user-select: none;
cursor: pointer;
}
#run:hover {
filter: contrast(125%);
}
2023-11-14 23:03:06 +00:00
#add, #reload, #edit, #search {
2023-08-20 07:16:38 +00:00
padding: 0.25rem 0.5rem;
2023-04-21 20:06:12 +00:00
text-align: center;
2022-08-21 10:11:25 +00:00
font-weight: bold;
user-select: none;
cursor: pointer;
background: var(--new-chart-background-color);
color: var(--new-chart-text-color);
float: right;
2023-04-21 20:06:12 +00:00
margin-right: 1rem !important;
margin-left: 0rem;
2022-08-21 10:11:25 +00:00
margin-bottom: 1rem;
2023-08-20 07:16:38 +00:00
height: 3ex;
2022-08-21 10:11:25 +00:00
}
2023-11-14 23:03:06 +00:00
#add:hover, #reload:hover, #edit:hover, #search:hover {
2022-08-21 10:11:25 +00:00
background: var(--button-background-color);
}
2023-11-14 23:03:06 +00:00
#search-query {
float: right;
width: 36%;
}
2023-11-16 12:17:22 +00:00
#global-error {
2023-04-21 20:06:12 +00:00
align-self: center;
width: 60%;
padding: .5rem;
2023-11-16 12:17:22 +00:00
color: var(--global-error-color);
2023-02-01 21:45:40 +00:00
display: flex;
flex-flow: row nowrap;
justify-content: center;
}
2022-08-21 10:11:25 +00:00
form {
display: inline;
}
form .param_name {
font-size: 14pt;
padding: 0.25rem;
background: var(--param-background-color);
color: var(--param-text-color);
display: inline-block;
2024-02-25 21:36:59 +00:00
box-shadow: 1px 1px 0 var(--shadow-color);
2022-08-21 10:11:25 +00:00
margin-bottom: 1rem;
}
input:focus {
box-shadow: 0 0 1rem var(--input-shadow-color);
}
.title {
left: 50%;
top: 0.25em;
transform: translate(-50%, 0);
font-size: 16pt;
font-weight: bold;
color: var(--title-color);
z-index: 10;
}
.chart-buttons {
cursor: pointer;
display: none;
position: absolute;
top: 0.25rem;
right: 0.25rem;
font-size: 200%;
color: #888;
z-index: 10;
}
.chart-buttons a {
margin-right: 0.25rem;
2023-08-21 04:23:58 +00:00
user-select: none;
2022-08-21 10:11:25 +00:00
}
.chart-buttons a:hover {
color: var(--chart-button-hover-color);
}
2023-02-15 20:09:36 +00:00
.disabled {
opacity: 0.5;
}
2022-08-21 10:11:25 +00:00
.query-editor {
display: none;
grid-template-columns: auto fit-content(10%);
grid-template-rows: auto fit-content(10%);
z-index: 11;
position: absolute;
width: 100%;
height: 100%;
}
.query-error {
display: none;
z-index: 10;
position: absolute;
color: var(--error-color);
padding: 2rem;
}
2023-08-20 07:16:38 +00:00
textarea {
2022-08-21 10:11:25 +00:00
padding: 0.5rem;
outline: none;
border: none;
font-size: 12pt;
background: var(--chart-background);
color: var(--text-color);
resize: none;
2023-08-20 07:16:38 +00:00
}
.query-editor textarea {
grid-row: 1;
grid-column: 1 / span 2;
z-index: 11;
border-bottom: 1px solid var(--edit-title-border);
2022-08-21 10:56:23 +00:00
margin: 0;
2022-08-21 10:11:25 +00:00
}
.query-editor input {
grid-row: 2;
padding: 0.5rem;
}
.edit-title {
background: var(--edit-title-background);
}
.edit-confirm {
background: var(--button-background-color);
color: var(--button-text-color);
font-weight: bold;
cursor: pointer;
}
.edit-confirm:hover {
filter: contrast(125%);
}
2023-08-20 07:16:38 +00:00
.edit-cancel {
cursor: pointer;
background: var(--new-chart-background-color);
}
.edit-cancel:hover {
filter: contrast(125%);
}
2022-08-21 10:11:25 +00:00
.nowrap {
white-space: pre;
}
2023-08-20 07:16:38 +00:00
#mass-editor {
display: none;
grid-template-columns: auto fit-content(10%) fit-content(10%);
grid-template-rows: auto fit-content(10%);
row-gap: 1rem;
column-gap: 1rem;
}
#mass-editor-textarea {
width: 100%;
height: 100%;
grid-row: 1;
grid-column: 1 / span 3;
}
#mass-editor input {
padding: 0.5rem;
}
#mass-editor-message {
2023-11-16 12:17:22 +00:00
color: var(--global-error-color);
2023-08-20 07:16:38 +00:00
}
2023-10-16 02:57:42 +00:00
#charts > div:only-child .display-only-if-more-than-one-chart {
display: none;
}
2023-11-28 01:18:41 +00:00
.u-series {
line-height: 0.8;
}
.u-series.footer {
font-size: 8px;
padding-top: 0;
margin-top: 0;
}
2022-08-21 10:11:25 +00:00
/* Source: https://cdn.jsdelivr.net/npm/uplot@1.6.21/dist/uPlot.min.css
* It is copy-pasted to lower the number of requests.
*/
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
< / style >
< / head >
< body >
2023-02-15 20:09:36 +00:00
< div class = "inputs unconnected" >
2022-08-21 10:11:25 +00:00
< form id = "params" >
< div id = "connection-params" >
< input spellcheck = "false" id = "url" type = "text" value = "" placeholder = "URL" / >
2023-02-21 18:52:28 +00:00
< div id = "username-password" >
< input spellcheck = "false" id = "user" type = "text" value = "" placeholder = "user" / >
< input spellcheck = "false" id = "password" type = "password" placeholder = "password" / >
2023-12-16 13:49:14 +00:00
< input id = "hidden-submit" type = "submit" hidden = "true" / >
2023-02-21 18:52:28 +00:00
< / div >
2022-08-21 10:11:25 +00:00
< / div >
2023-04-21 20:06:12 +00:00
< div id = "button-options" >
2022-08-21 10:11:25 +00:00
< span class = "nowrap themes" > < span id = "toggle-dark" > 🌚< / span > < span id = "toggle-light" > 🌞< / span > < / span >
2023-08-20 06:07:11 +00:00
< input id = "edit" type = "button" value = "✎" style = "display: none;" >
2023-04-21 20:06:12 +00:00
< input id = "add" type = "button" value = "Add chart" style = "display: none;" >
< input id = "reload" type = "button" value = "Reload" >
2023-11-14 23:03:06 +00:00
< span id = "search-span" class = "nowrap" style = "display: none;" > < input id = "search" type = "button" value = "🔎" title = "Run query to obtain list of charts from ClickHouse" > < input id = "search-query" name = "search" type = "text" spellcheck = "false" > < / span >
2022-08-21 10:11:25 +00:00
< div id = "chart-params" > < / div >
< / div >
< / form >
2023-11-16 12:17:22 +00:00
< div id = "global-error" > < / div >
2022-08-21 10:11:25 +00:00
< / div >
< div id = "charts" > < / div >
2023-08-20 07:16:38 +00:00
< div id = "mass-editor" >
2023-08-20 11:42:00 +00:00
< textarea id = "mass-editor-textarea" spellcheck = "false" data-gramm = "false" > < / textarea >
2023-08-20 07:16:38 +00:00
< span id = "mass-editor-message" > < / span >
< input type = "submit" id = "mass-editor-cancel" class = "edit-cancel" value = "Cancel" >
< input type = "submit" id = "mass-editor-confirm" class = "edit-confirm" value = "Apply" >
< / div >
2022-08-21 10:11:25 +00:00
< script >
2022-08-21 11:26:32 +00:00
/** Implementation note: it might be more natural to use some reactive framework.
* But for now it is small enough to avoid it. As a bonus we have less number of dependencies,
* which is better for maintainability.
*
* TODO:
* - zoom on the graphs should work on touch devices;
* - footer with "about" or a link to source code;
* - allow to configure a table on a server to save the dashboards;
* - if a query returned one value, display this value instead of a diagram;
* - if a query returned something unusual, display the table;
*/
2024-02-25 21:36:59 +00:00
let host = location.protocol != 'file:' ? location.origin : 'http://localhost:8123/';
let user = 'default';
2022-08-21 10:11:25 +00:00
let password = '';
2024-02-25 21:36:59 +00:00
let add_http_cors_header = (location.protocol != 'file:');
2022-08-21 10:11:25 +00:00
2024-06-22 13:26:46 +00:00
const current_url = new URL(window.location);
/// Substitute user name if it's specified in the query string
const user_from_url = current_url.searchParams.get('user');
if (user_from_url) {
user = user_from_url;
}
2023-02-01 21:45:40 +00:00
const errorCodeMessageMap = {
516: 'Error authenticating with database. Please check your connection params and try again.'
}
2023-04-21 20:06:12 +00:00
const errorMessages = [
{
regex: /TypeError: Failed to fetch/,
messageFunc: () => 'Error authenticating with database. Please check your connection url and try again.',
},
{
regex: /Code: (\d+)/,
messageFunc: (match) => {
return errorCodeMessageMap[match[1]]
}
}
]
2023-02-01 21:45:40 +00:00
2022-08-21 11:07:46 +00:00
2023-11-14 23:03:06 +00:00
/// Query to fill `queries` list for the dashboard
2024-02-14 01:17:23 +00:00
let search_query = `SELECT title, query FROM system.dashboards WHERE dashboard = 'Overview'`;
2023-11-16 11:38:32 +00:00
let customized = false;
2023-11-14 23:03:06 +00:00
let queries = [];
2022-08-21 10:11:25 +00:00
/// Query parameters with predefined default values.
/// All other parameters will be automatically found in the queries.
2024-02-08 12:02:28 +00:00
let default_params = {
2023-11-28 00:36:38 +00:00
'rounding': '60',
'seconds': '86400'
2022-08-21 10:11:25 +00:00
};
2024-02-08 12:02:28 +00:00
let params = default_params;
2022-08-21 10:11:25 +00:00
2023-11-28 00:36:38 +00:00
/// Palette generation for charts
2024-02-26 00:02:38 +00:00
function generatePalette(numColors) {
2024-05-06 12:05:37 +00:00
// oklch() does not work in firefox< =125 inside < canvas > element so we convert it back to rgb for now.
// Based on https://github.com/color-js/color.js/blob/main/src/spaces/oklch.js
const multiplyMatrices = (A, B) => {
return [
A[0]*B[0] + A[1]*B[1] + A[2]*B[2],
A[3]*B[0] + A[4]*B[1] + A[5]*B[2],
A[6]*B[0] + A[7]*B[1] + A[8]*B[2]
];
}
const oklch2oklab = ([l, c, h]) => [
l,
isNaN(h) ? 0 : c * Math.cos(h * Math.PI / 180),
isNaN(h) ? 0 : c * Math.sin(h * Math.PI / 180)
]
const srgbLinear2rgb = rgb => rgb.map(c =>
Math.abs(c) > 0.0031308 ?
(c < 0 ? -1 : 1 ) * ( 1 . 055 * ( Math . abs ( c ) * * ( 1 / 2 . 4 ) ) - 0 . 055 ) :
12.92 * c
)
const oklab2xyz = lab => {
const LMSg = multiplyMatrices([
1, 0.3963377773761749, 0.2158037573099136,
1, -0.1055613458156586, -0.0638541728258133,
1, -0.0894841775298119, -1.2914855480194092,
], lab)
const LMS = LMSg.map(val => val ** 3)
return multiplyMatrices([
1.2268798758459243, -0.5578149944602171, 0.2813910456659647,
-0.0405757452148008, 1.1122868032803170, -0.0717110580655164,
-0.0763729366746601, -0.4214933324022432, 1.5869240198367816
], LMS)
}
const xyz2rgbLinear = xyz => {
return multiplyMatrices([
3.2409699419045226, -1.537383177570094, -0.4986107602930034,
-0.9692436362808796, 1.8759675015077202, 0.04155505740717559,
0.05563007969699366, -0.20397695888897652, 1.0569715142428786
], xyz)
}
const oklch2rgb = lch => srgbLinear2rgb(xyz2rgbLinear(oklab2xyz(oklch2oklab(lch))))
2024-02-26 00:02:38 +00:00
palette = [];
2023-11-28 00:36:38 +00:00
for (let i = 0; i < numColors ; i + + ) {
2024-05-06 12:05:37 +00:00
//palette.push(`oklch(${theme != 'dark' ? 0.75 : 0.5}, 0.15, ${360 * i / numColors})`);
let rgb = oklch2rgb([theme != 'dark' ? 0.75 : 0.5, 0.15, 360 * i / numColors]);
palette.push(`rgb(${rgb[0] * 255}, ${rgb[1] * 255}, ${rgb[2] * 255})`);
2023-11-28 00:36:38 +00:00
}
return palette;
}
2022-08-21 10:11:25 +00:00
let theme = 'light';
function setTheme(new_theme) {
theme = new_theme;
document.documentElement.setAttribute('data-theme', theme);
window.localStorage.setItem('theme', theme);
drawAll();
}
document.getElementById('toggle-light').addEventListener('click', e => setTheme('light'));
document.getElementById('toggle-dark').addEventListener('click', e => setTheme('dark'));
/// uPlot objects will go here.
let plots = [];
/// chart div's will be here.
let charts = document.getElementById('charts');
/// This is not quite correct (we cannot really parse SQL with regexp) but tolerable.
2024-02-08 12:02:28 +00:00
const query_param_regexp = /\{(\w+):([^}]+)\}/g;
2022-08-21 10:11:25 +00:00
/// Automatically parse more parameters from the queries.
function findParamsInQuery(query, new_params) {
2024-02-08 12:02:28 +00:00
const typeDefault = (type) => type.includes('Int') ? '0'
: (type.includes('Float') ? '0.0'
: (type.includes('Bool') ? 'false'
: (type.includes('Date') ? new Date().toISOString().slice(0, 10)
: (type.includes('UUID') ? '00000000-0000-0000-0000-000000000000'
: ''))));
2022-08-21 10:11:25 +00:00
for (let match of query.matchAll(query_param_regexp)) {
const name = match[1];
2024-02-08 12:02:28 +00:00
new_params[name] = params[name] || default_params[name] || typeDefault(match[2]);
2022-08-21 10:11:25 +00:00
}
}
function findParamsInQueries() {
2023-11-14 23:03:06 +00:00
let new_params = {};
2022-08-21 10:11:25 +00:00
queries.forEach(q => findParamsInQuery(q.query, new_params));
params = new_params;
}
function insertParam(name, value) {
let param_wrapper = document.createElement('span');
param_wrapper.className = 'nowrap';
let param_name = document.createElement('span');
param_name.className = 'param_name';
let param_name_text = document.createTextNode(`${name}: `);
param_name.appendChild(param_name_text);
let param_value = document.createElement('input');
param_value.className = 'param';
param_value.name = `${name}`;
param_value.type = 'text';
param_value.value = value;
param_value.spellcheck = false;
param_wrapper.appendChild(param_name);
param_wrapper.appendChild(param_value);
document.getElementById('chart-params').appendChild(param_wrapper);
}
function buildParams() {
let params_elem = document.getElementById('chart-params');
while (params_elem.firstChild) {
params_elem.removeChild(params_elem.lastChild);
}
for (let [name, value] of Object.entries(params)) {
insertParam(name, value);
}
let run = document.createElement('input');
run.id = 'run';
run.type = 'submit';
run.value = 'Ok';
document.getElementById('chart-params').appendChild(run);
}
function updateParams() {
[...document.getElementsByClassName('param')].forEach(e => { params[e.name] = e.value });
}
function getParamsForURL() {
let url = '';
for (let [name, value] of Object.entries(params)) {
url += `¶m_${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
};
return url;
}
function insertChart(i) {
let q = queries[i];
let chart = document.createElement('div');
chart.className = 'chart';
let chart_title = document.createElement('div');
let title_text = document.createTextNode('');
chart_title.appendChild(title_text);
chart_title.className = 'title';
chart.appendChild(chart_title);
let query_error = document.createElement('div');
query_error.className = 'query-error';
query_error.appendChild(document.createTextNode(''));
chart.appendChild(query_error);
let query_editor = document.createElement('div');
query_editor.className = 'query-editor';
let query_editor_textarea = document.createElement('textarea');
query_editor_textarea.spellcheck = false;
query_editor_textarea.value = q.query;
query_editor_textarea.placeholder = 'Query';
2023-08-20 11:42:00 +00:00
query_editor_textarea.setAttribute('data-gramm', false);
2022-08-21 10:11:25 +00:00
query_editor.appendChild(query_editor_textarea);
let query_editor_title = document.createElement('input');
query_editor_title.type = 'text';
query_editor_title.value = q.title;
query_editor_title.placeholder = 'Chart title';
query_editor_title.className = 'edit-title';
query_editor.appendChild(query_editor_title);
let query_editor_confirm = document.createElement('input');
query_editor_confirm.type = 'submit';
query_editor_confirm.value = 'Ok';
query_editor_confirm.className = 'edit-confirm';
2023-02-01 21:49:58 +00:00
function getCurrentIndex() {
/// Indices may change after deletion of other element, hence captured "i" may become incorrect.
return [...charts.querySelectorAll('.chart')].findIndex(child => chart == child);
}
2022-08-21 10:11:25 +00:00
function editConfirm() {
query_editor.style.display = 'none';
query_error.style.display = 'none';
q.title = query_editor_title.value;
q.query = query_editor_textarea.value;
title_text.data = '';
findParamsInQuery(q.query, params);
buildParams();
2023-11-16 11:38:32 +00:00
refreshCustomized(true);
saveState();
2023-02-01 21:49:58 +00:00
const idx = getCurrentIndex();
draw(idx, chart, getParamsForURL(), q.query);
2022-08-21 10:11:25 +00:00
}
query_editor_confirm.addEventListener('click', editConfirm);
/// Ctrl+Enter (or Cmd+Enter on Mac) will also confirm editing.
2023-12-17 01:43:50 +00:00
query_editor.addEventListener('keydown', event => {
2022-08-21 10:11:25 +00:00
if ((event.metaKey || event.ctrlKey) & & (event.keyCode == 13 || event.keyCode == 10)) {
editConfirm();
}
});
query_editor.addEventListener('keyup', e => {
if (e.key == 'Escape') {
query_editor.style.display = 'none';
}
});
query_editor.appendChild(query_editor_confirm);
chart.appendChild(query_editor);
let edit_buttons = document.createElement('div');
edit_buttons.className = 'chart-buttons';
2023-10-14 01:21:07 +00:00
let move = document.createElement('a');
let move_text = document.createTextNode('✥');
move.appendChild(move_text);
2023-10-16 03:25:59 +00:00
let drag_state = {
is_dragging: false,
idx: null,
offset_x: null,
offset_y: null,
displace_idx: null,
displace_chart: null
};
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
function dragStop(e) {
drag_state.is_dragging = false;
chart.className = 'chart';
chart.style.left = null;
chart.style.top = null;
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
if (drag_state.displace_idx !== null) {
const elem = queries[drag_state.idx];
queries.splice(drag_state.idx, 1);
queries.splice(drag_state.displace_idx, 0, elem);
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
drag_state.displace_chart.className = 'chart';
drawAll();
2023-10-14 01:21:07 +00:00
}
2023-10-16 03:25:59 +00:00
}
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
function dragMove(e) {
if (!drag_state.is_dragging) return;
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
let x = e.clientX - drag_state.offset_x;
let y = e.clientY - drag_state.offset_y;
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
chart.style.left = `${x}px`;
chart.style.top = `${y}px`;
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
drag_state.displace_idx = null;
drag_state.displace_chart = null;
let current_idx = -1;
for (const elem of charts.querySelectorAll('.chart')) {
++current_idx;
if (current_idx == drag_state.idx) {
continue;
}
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
const this_rect = chart.getBoundingClientRect();
const this_center_x = this_rect.left + this_rect.width / 2;
const this_center_y = this_rect.top + this_rect.height / 2;
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
const elem_rect = elem.getBoundingClientRect();
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
if (this_center_x >= elem_rect.left & & this_center_x < = elem_rect.right
& & this_center_y >= elem_rect.top & & this_center_y < = elem_rect.bottom) {
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
elem.className = 'chart chart-displaced';
drag_state.displace_idx = current_idx;
drag_state.displace_chart = elem;
} else {
elem.className = 'chart';
2023-08-21 04:23:58 +00:00
}
2023-10-14 01:21:07 +00:00
}
2023-10-16 03:25:59 +00:00
}
2023-10-14 01:21:07 +00:00
2023-10-16 03:25:59 +00:00
function dragStart(e) {
if (e.button !== 0) return; /// left button only
move.setPointerCapture(e.pointerId);
drag_state.is_dragging = true;
drag_state.idx = getCurrentIndex();
chart.className = 'chart chart-moving';
drag_state.offset_x = e.clientX;
drag_state.offset_y = e.clientY;
}
/// Read https://www.redblobgames.com/making-of/draggable/
move.addEventListener('pointerdown', dragStart);
move.addEventListener('pointermove', dragMove);
move.addEventListener('pointerup', dragStop);
move.addEventListener('pointerancel', dragStop);
move.addEventListener('touchstart', (e) => e.preventDefault());
2023-08-21 04:23:58 +00:00
2023-10-14 01:21:07 +00:00
let maximize = document.createElement('a');
let maximize_text = document.createTextNode('🗖');
maximize.appendChild(maximize_text);
maximize.addEventListener('click', e => {
const idx = getCurrentIndex();
chart.className = (chart.className == 'chart' ? 'chart chart-maximized' : 'chart');
resize();
});
2023-08-21 04:23:58 +00:00
2022-08-21 10:11:25 +00:00
let edit = document.createElement('a');
let edit_text = document.createTextNode('✎');
edit.appendChild(edit_text);
function editStart() {
query_editor.style.display = 'grid';
query_editor_textarea.focus();
}
edit.addEventListener('click', e => editStart());
if (!q.query) {
editStart();
}
let trash = document.createElement('a');
let trash_text = document.createTextNode('✕');
trash.appendChild(trash_text);
trash.addEventListener('click', e => {
2023-02-01 21:49:58 +00:00
const idx = getCurrentIndex();
2022-08-21 10:11:25 +00:00
if (plots[idx]) {
plots[idx].destroy();
plots[idx] = null;
}
plots.splice(idx, 1);
charts.removeChild(chart);
queries.splice(idx, 1);
findParamsInQueries();
buildParams();
resize();
2023-11-16 11:38:32 +00:00
refreshCustomized(true);
2022-08-21 10:11:25 +00:00
saveState();
});
2023-10-16 02:57:42 +00:00
move.classList.add('display-only-if-more-than-one-chart');
maximize.classList.add('display-only-if-more-than-one-chart');
2023-08-21 04:23:58 +00:00
edit_buttons.appendChild(move);
edit_buttons.appendChild(maximize);
2022-08-21 10:11:25 +00:00
edit_buttons.appendChild(edit);
edit_buttons.appendChild(trash);
chart.appendChild(edit_buttons);
chart.addEventListener('mouseenter', e => { edit_buttons.style.display = 'block'; });
chart.addEventListener('mouseleave', e => { edit_buttons.style.display = 'none'; });
charts.appendChild(chart);
2022-11-01 16:19:56 +00:00
return {chart: chart, textarea: query_editor_textarea};
2023-11-14 23:03:06 +00:00
}
2022-08-21 10:11:25 +00:00
document.getElementById('add').addEventListener('click', e => {
queries.push({ title: '', query: '' });
2022-11-01 16:19:56 +00:00
const {chart, textarea} = insertChart(plots.length);
chart.scrollIntoView();
textarea.focus();
2022-08-21 10:11:25 +00:00
plots.push(null);
resize();
});
2022-11-01 16:19:26 +00:00
document.getElementById('reload').addEventListener('click', e => {
2023-12-16 13:49:14 +00:00
reloadAll(queries.length == 0);
2022-11-01 16:19:26 +00:00
});
2023-11-14 23:03:06 +00:00
document.getElementById('search').addEventListener('click', e => {
reloadAll(true);
});
2023-08-20 07:16:38 +00:00
let mass_editor_active = false;
function showMassEditor() {
document.getElementById('charts').style.display = 'none';
let editor_div = document.getElementById('mass-editor');
editor_div.style.display = 'grid';
let editor = document.getElementById('mass-editor-textarea');
2023-08-20 07:32:42 +00:00
editor.value = JSON.stringify({params: params, queries: queries}, null, 2);
2023-08-20 07:16:38 +00:00
mass_editor_active = true;
}
function hideMassEditor() {
document.getElementById('mass-editor').style.display = 'none';
document.getElementById('charts').style.display = 'flex';
mass_editor_active = false;
}
function massEditorApplyChanges() {
let editor = document.getElementById('mass-editor-textarea');
2023-08-20 07:32:42 +00:00
({params, queries} = JSON.parse(editor.value));
2023-08-20 07:16:38 +00:00
hideMassEditor();
regenerate();
2023-11-16 11:38:32 +00:00
refreshCustomized(true);
2023-08-20 07:16:38 +00:00
saveState();
2023-11-16 11:38:32 +00:00
drawAll();
2023-08-20 07:16:38 +00:00
}
document.getElementById('edit').addEventListener('click', e => {
if (mass_editor_active) {
massEditorApplyChanges();
hideMassEditor();
} else {
showMassEditor();
}
});
document.getElementById('mass-editor-confirm').addEventListener('click', e => {
massEditorApplyChanges();
hideMassEditor();
});
document.getElementById('mass-editor-cancel').addEventListener('click', e => {
hideMassEditor();
});
document.getElementById('mass-editor-textarea').addEventListener('input', e => {
let message = document.getElementById('mass-editor-message').firstChild;
message.data = '';
if (e.target.value != '') {
try { JSON.parse(e.target.value) } catch (e) {
message.data = e.toString();
}
}
});
2022-08-21 10:11:25 +00:00
function legendAsTooltipPlugin({ className, style = { background: "var(--legend-background)" } } = {}) {
let legendEl;
2023-12-22 14:39:44 +00:00
let multiline;
2022-08-21 10:11:25 +00:00
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className & & legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
2024-08-02 23:31:02 +00:00
textAlign: "right",
2022-08-21 10:11:25 +00:00
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
2024-08-02 23:35:10 +00:00
zIndex: 100,
2024-08-02 23:31:02 +00:00
boxShadow: "2px 2px 10px rgba(0, 0, 0, 0.1)",
2022-08-21 10:11:25 +00:00
...style
});
2023-12-22 14:39:44 +00:00
const nodes = legendEl.querySelectorAll("th");
for (let i = 0; i < nodes.length ; i + + )
nodes[i]._order = i;
2023-11-28 00:36:38 +00:00
if (opts.series.length == 2) {
2023-12-22 14:39:44 +00:00
multiline = false;
2023-11-28 00:36:38 +00:00
for (let i = 0; i < nodes.length ; i + + )
nodes[i].style.display = "none";
} else {
2023-12-22 14:39:44 +00:00
multiline = true;
2023-11-28 00:36:38 +00:00
legendEl.querySelector("th").remove();
legendEl.querySelector("td").setAttribute('colspan', '2');
legendEl.querySelector("td").style.textAlign = 'center';
2023-11-28 01:18:41 +00:00
let footer = legendEl.insertRow().insertCell();
footer.setAttribute('colspan', '2');
footer.style.textAlign = 'center';
footer.classList.add('u-value');
footer.parentNode.classList.add('u-series','footer');
footer.textContent = ". . .";
2023-11-28 00:36:38 +00:00
}
2022-08-21 10:11:25 +00:00
const overEl = u.over;
2023-11-28 00:36:38 +00:00
overEl.style.overflow = "visible";
2022-08-21 10:11:25 +00:00
overEl.appendChild(legendEl);
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
}
2023-11-28 00:36:38 +00:00
function nodeListToArray(nodeList) {
return Array.prototype.slice.call(nodeList);
}
2022-08-21 10:11:25 +00:00
function update(u) {
let { left, top } = u.cursor;
2024-08-02 23:31:02 +00:00
/// This will make the balloon to the right of the cursor when the cursor is on the left side, and vise-versa,
/// avoiding the borders of the chart.
left -= legendEl.clientWidth * (left / u.width);
top -= legendEl.clientHeight;
2022-08-21 10:11:25 +00:00
legendEl.style.transform = "translate(" + left + "px, " + top + "px)";
2023-12-22 14:39:44 +00:00
if (multiline) {
2023-11-28 00:36:38 +00:00
let nodes = nodeListToArray(legendEl.querySelectorAll("tr"));
let header = nodes.shift();
2023-11-28 01:18:41 +00:00
let footer = nodes.pop();
2023-12-22 14:39:44 +00:00
let showLimit = Math.floor(u.height / 30);
nodes.forEach(function (node) { node._sort_key = nodes.length > showLimit ? +node.querySelector("td").textContent.replace(/,/g,'') : node._order; });
nodes.sort((a, b) => b._sort_key - a._sort_key);
2023-11-28 00:36:38 +00:00
nodes.forEach(function (node) { node.parentNode.appendChild(node); });
for (let i = 0; i < nodes.length ; i + + ) {
nodes[i].style.display = i < showLimit ? null : " none " ;
}
2023-11-28 01:18:41 +00:00
footer.parentNode.appendChild(footer);
2023-12-22 14:39:44 +00:00
footer.style.display = nodes.length > showLimit ? null : "none";
2023-11-28 00:36:38 +00:00
}
2022-08-21 10:11:25 +00:00
}
return {
hooks: {
init: init,
setCursor: update,
}
};
}
2023-11-28 00:36:38 +00:00
2023-11-14 23:03:06 +00:00
async function doFetch(query, url_params = '') {
host = document.getElementById('url').value || host;
2022-08-21 10:11:25 +00:00
user = document.getElementById('user').value;
password = document.getElementById('password').value;
2023-11-26 23:28:11 +00:00
let url = `${host}?default_format=JSONColumnsWithMetadata& enable_http_compression=1`
2022-11-01 16:20:26 +00:00
if (add_http_cors_header) {
// For debug purposes, you may set add_http_cors_header from a browser console
url += '&add_http_cors_header=1';
}
2022-08-21 10:11:25 +00:00
if (user) {
url += `&user=${encodeURIComponent(user)}`;
}
if (password) {
url += `&password=${encodeURIComponent(password)}`;
}
2023-11-26 23:28:11 +00:00
let response, reply, error;
2022-08-21 10:11:25 +00:00
try {
response = await fetch(url + url_params, { method: "POST", body: query });
2023-11-26 23:28:11 +00:00
reply = await response.text();
2022-08-21 10:11:25 +00:00
if (response.ok) {
2023-11-26 23:28:11 +00:00
reply = JSON.parse(reply);
2023-11-28 00:36:38 +00:00
if (reply.exception) {
error = reply.exception;
}
2022-08-21 10:11:25 +00:00
} else {
2023-11-26 23:28:11 +00:00
error = reply;
2022-08-21 10:11:25 +00:00
}
} catch (e) {
console.log(e);
error = e.toString();
}
2023-02-01 21:45:40 +00:00
if (error) {
2023-08-20 06:00:04 +00:00
const errorMatch = errorMessages.find(({ regex }) => error.match(regex));
if (!errorMatch) {
throw new Error(error);
}
const match = error.match(errorMatch.regex);
const message = errorMatch.messageFunc(match);
2023-07-03 16:42:51 +00:00
if (message) {
2023-08-20 06:07:11 +00:00
throw new Error(message);
2023-02-01 21:45:40 +00:00
}
}
2023-11-26 23:28:11 +00:00
return {reply, error};
2023-11-14 23:03:06 +00:00
}
async function draw(idx, chart, url_params, query) {
if (plots[idx]) {
plots[idx].destroy();
plots[idx] = null;
}
2023-11-26 23:28:11 +00:00
let {reply, error} = await doFetch(query, url_params);
2022-08-21 10:11:25 +00:00
if (!error) {
2023-11-26 23:28:11 +00:00
if (reply.rows.length == 0) {
2022-08-21 10:11:25 +00:00
error = "Query returned empty result.";
2023-11-26 23:28:11 +00:00
} else if (reply.meta.length < 2 ) {
error = "Query should return at least two columns: unix timestamp and value.";
} else {
for (let i = 0; i < reply.meta.length ; i + + ) {
let label = reply.meta[i].name;
let column = reply.data[label];
if (!Array.isArray(column) || column.length != reply.data[reply.meta[0].name].length) {
error = "Wrong data format of the query.";
break;
}
}
2022-08-21 10:11:25 +00:00
}
}
2023-11-28 00:36:38 +00:00
// Transform string-labeled data to multi-column data
function transformToColumns() {
const x = reply.meta[0].name; // time; must be ordered
const l = reply.meta[1].name; // string label column to distinguish series; must be ordered
const y = reply.meta[2].name; // values; must have single value for (x, l) pair
const labels = [...new Set(reply.data[l])].sort((a, b) => a - b);
if (labels.includes('__time__')) {
error = "The second column is not allowed to contain '__time__' values.";
return;
}
const times = [...new Set(reply.data[x])].sort((a, b) => a - b);
let new_meta = [{ name: '__time__', type: reply.meta[0].type }];
let new_data = { __time__: [] };
for (let label of labels) {
new_meta.push({ name: label, type: reply.meta[2].type });
new_data[label] = [];
}
let new_rows = 0;
function row_done(row_time) {
new_rows++;
new_data.__time__.push(row_time);
for (let label of labels) {
if (new_data[label].length < new_rows ) {
new_data[label].push(null);
}
}
}
let prev_time = reply.data[x][0];
const old_rows = reply.data[x].length;
for (let i = 0; i < old_rows ; i + + ) {
const time = reply.data[x][i];
const label = reply.data[l][i];
const value = reply.data[y][i];
if (prev_time != time) {
row_done(prev_time);
prev_time = time;
}
new_data[label].push(value);
}
row_done(prev_time);
reply.meta = new_meta;
reply.data = new_data;
reply.rows = new_rows;
}
function isStringColumn(type) {
return type === 'String' || type === 'LowCardinality(String)';
}
if (!error) {
if (reply.meta.length == 3 & & isStringColumn(reply.meta[1].type)) {
transformToColumns();
}
}
2022-08-21 10:11:25 +00:00
let error_div = chart.querySelector('.query-error');
let title_div = chart.querySelector('.title');
if (error) {
error_div.firstChild.data = error;
2023-11-14 23:03:06 +00:00
title_div.style.display = 'none';
2022-08-21 10:11:25 +00:00
error_div.style.display = 'block';
2023-02-15 20:09:36 +00:00
return false;
2022-08-21 10:11:25 +00:00
} else {
error_div.firstChild.data = '';
error_div.style.display = 'none';
title_div.style.display = 'block';
}
const [line_color, fill_color, grid_color, axes_color] = theme != 'dark'
2023-11-28 00:36:38 +00:00
? ["#ff8888", "#ffeeee", "#eeeedd", "#2c3235"]
: ["#886644", "#004455", "#2c3235", "#c7d0d9"];
2022-08-21 10:11:25 +00:00
let sync = uPlot.sync("sync");
2024-08-02 23:31:02 +00:00
function formatDateTime(t) {
return (new Date(t * 1000)).toISOString().replace('T', '\n').replace('.000Z', '');
}
function formatDateTimes(self, ticks) {
return ticks.map((t, idx) => {
let res = formatDateTime(t);
if (idx == 0 || res.substring(0, 10) != formatDateTime(ticks[idx - 1]).substring(0, 10)) {
return res;
} else {
return res.substring(11);
}
});
}
function formatValue(v) {
const a = Math.abs(v);
if (a >= 1000000000000000) { return (v / 1000000000000000) + 'P'; }
if (a >= 1000000000000) { return (v / 1000000000000) + 'T'; }
if (a >= 1000000000) { return (v / 1000000000) + 'G'; }
if (a >= 1000000) { return (v / 1000000) + 'M'; }
if (a >= 1000) { return (v / 1000) + 'K'; }
if (a > 0 & & a < 0.001 ) { return ( v * 1000000 ) + " μ " ; }
return v;
}
let axis_x = {
stroke: axes_color,
grid: { width: 1 / devicePixelRatio, stroke: grid_color },
ticks: { width: 1 / devicePixelRatio, stroke: grid_color },
values: formatDateTimes,
space: 80,
incrs: [1, 5, 10, 15, 30,
60, 60 * 5, 60 * 10, 60 * 15, 60 * 30,
3600, 3600 * 2, 3600 * 3, 3600 * 4, 3600 * 6, 3600 * 12,
3600 * 24],
};
let axis_y = {
2023-11-28 00:36:38 +00:00
stroke: axes_color,
grid: { width: 1 / devicePixelRatio, stroke: grid_color },
2024-08-02 23:31:02 +00:00
ticks: { width: 1 / devicePixelRatio, stroke: grid_color },
values: (self, ticks) => ticks.map(formatValue)
2023-11-28 00:36:38 +00:00
};
2024-08-02 23:31:02 +00:00
let axes = [axis_x, axis_y];
let series = [{ label: "time", value: (self, t) => formatDateTime(t) }];
2023-11-28 00:36:38 +00:00
let data = [reply.data[reply.meta[0].name]];
// Treat every column as series
const series_count = reply.meta.length;
const fill = series_count == 2 ? fill_color : undefined;
2024-02-26 00:02:38 +00:00
const palette = series_count == 2 ? [line_color] : generatePalette(series_count);
2023-11-26 23:28:11 +00:00
let max_value = Number.NEGATIVE_INFINITY;
2023-11-28 00:36:38 +00:00
for (let i = 1; i < series_count ; i + + ) {
2023-11-26 23:28:11 +00:00
let label = reply.meta[i].name;
2023-11-28 00:36:38 +00:00
series.push({ label, stroke: palette[i - 1], fill });
2023-11-26 23:28:11 +00:00
data.push(reply.data[label]);
max_value = Math.max(max_value, ...reply.data[label]);
}
2022-08-21 10:11:25 +00:00
const opts = {
width: chart.clientWidth,
height: chart.clientHeight,
2024-08-02 23:31:02 +00:00
scales: { x: { time: false } }, /// Because we want to split and format time on our own.
2023-11-26 23:28:11 +00:00
axes,
series,
2024-08-02 23:31:02 +00:00
padding: [ null, null, null, 3 ],
2022-08-21 10:11:25 +00:00
plugins: [ legendAsTooltipPlugin() ],
cursor: {
sync: {
key: "sync",
}
}
};
plots[idx] = new uPlot(opts, data, chart);
sync.sub(plots[idx]);
/// Set title
2023-02-01 21:45:40 +00:00
const title = queries[idx] & & queries[idx].title ? queries[idx].title.replaceAll(/\{(\w+)\}/g, (_, name) => params[name] ) : '';
2022-08-21 10:11:25 +00:00
chart.querySelector('.title').firstChild.data = title;
2023-11-14 23:03:06 +00:00
return true;
2022-08-21 10:11:25 +00:00
}
2023-11-16 12:17:22 +00:00
function showError(message) {
2023-08-20 06:07:11 +00:00
const charts = document.getElementById('charts');
2023-04-21 20:06:12 +00:00
charts.style.height = '0px';
charts.style.opacity = '0';
2023-08-20 07:16:38 +00:00
document.getElementById('add').style.display = 'none';
document.getElementById('edit').style.display = 'none';
2023-02-01 21:45:40 +00:00
2023-11-16 12:17:22 +00:00
const error = document.getElementById('global-error');
error.textContent = message;
error.style.display = 'flex';
2023-02-01 21:45:40 +00:00
}
2023-11-16 12:17:22 +00:00
function hideError() {
2023-08-20 06:07:11 +00:00
const charts = document.getElementById('charts');
2023-04-21 20:06:12 +00:00
charts.style.height = 'auto';
charts.style.opacity = '1';
2023-02-01 21:45:40 +00:00
2023-11-16 12:17:22 +00:00
const error = document.getElementById('global-error');
error.textContent = '';
error.style.display = 'none';
2023-02-01 21:45:40 +00:00
}
let firstLoad = true;
2024-02-26 00:31:29 +00:00
let is_drawing = false; // Prevent race condition leading to duplicate/dangling charts.
2022-08-21 10:11:25 +00:00
async function drawAll() {
2024-02-26 00:31:29 +00:00
if (is_drawing) return;
is_drawing = true;
2022-08-21 10:11:25 +00:00
let params = getParamsForURL();
2023-04-21 20:06:12 +00:00
const chartsArray = document.getElementsByClassName('chart');
2023-02-01 21:45:40 +00:00
if (!firstLoad) {
2023-11-16 12:17:22 +00:00
hideError();
2022-08-21 10:11:25 +00:00
}
2023-02-01 21:45:40 +00:00
await Promise.all([...Array(queries.length)].map(async (_, i) => {
2023-04-21 20:06:12 +00:00
return draw(i, chartsArray[i], params, queries[i].query).catch((e) => {
2023-02-01 21:45:40 +00:00
if (!firstLoad) {
2023-11-16 12:17:22 +00:00
showError(e.message);
2023-02-01 21:45:40 +00:00
}
2023-02-15 20:09:36 +00:00
return false;
2023-02-01 21:45:40 +00:00
});
2023-02-15 20:09:36 +00:00
})).then((results) => {
if (firstLoad) {
firstLoad = false;
} else {
2023-11-14 23:03:06 +00:00
enableButtons();
2023-02-15 20:09:36 +00:00
}
2023-07-03 16:42:51 +00:00
if (results.includes(true)) {
2023-02-15 20:09:36 +00:00
const element = document.querySelector('.inputs');
element.classList.remove('unconnected');
2023-08-20 07:16:38 +00:00
document.getElementById('add').style.display = 'inline-block';
document.getElementById('edit').style.display = 'inline-block';
2023-11-14 23:03:06 +00:00
document.getElementById('search-span').style.display = '';
2023-12-16 13:49:14 +00:00
hideError();
2024-02-26 00:31:29 +00:00
} else {
document.getElementById('charts').style.height = '0px';
2023-02-15 20:09:36 +00:00
}
2023-11-14 23:03:06 +00:00
});
2024-02-26 00:31:29 +00:00
is_drawing = false;
2022-08-21 10:11:25 +00:00
}
function resize() {
plots.forEach(plot => {
if (plot) {
let chart = plot.over.closest('.chart');
plot.setSize({ width: chart.clientWidth, height: chart.clientHeight });
}
});
}
new ResizeObserver(resize).observe(document.body);
2023-11-14 23:03:06 +00:00
function disableButtons() {
const reloadButton = document.getElementById('reload');
reloadButton.value = 'Reloading…';
reloadButton.disabled = true;
reloadButton.classList.add('disabled');
const runButton = document.getElementById('run');
2023-12-16 13:49:14 +00:00
if (runButton) {
runButton.value = 'Reloading…';
runButton.disabled = true;
runButton.classList.add('disabled');
}
2023-11-14 23:03:06 +00:00
const searchButton = document.getElementById('search');
searchButton.value = '…';
searchButton.disabled = true;
searchButton.classList.add('disabled');
2023-02-15 20:09:36 +00:00
}
2023-11-14 23:03:06 +00:00
function enableButtons() {
const reloadButton = document.getElementById('reload');
reloadButton.value = 'Reload';
reloadButton.disabled = false;
reloadButton.classList.remove('disabled');
const runButton = document.getElementById('run');
2023-12-16 13:49:14 +00:00
if (runButton) {
runButton.value = 'Ok';
runButton.disabled = false;
runButton.classList.remove('disabled');
}
2023-11-14 23:03:06 +00:00
const searchButton = document.getElementById('search');
searchButton.value = '🔎';
searchButton.disabled = false;
searchButton.classList.remove('disabled');
2023-07-03 16:42:51 +00:00
}
2023-11-14 23:03:06 +00:00
async function reloadAll(do_search) {
disableButtons();
try {
2023-11-15 19:17:02 +00:00
updateParams();
2023-11-16 11:38:32 +00:00
if (do_search) {
search_query = document.getElementById('search-query').value;
queries = [];
refreshCustomized(false);
}
2023-11-15 19:17:02 +00:00
saveState();
2023-11-14 23:03:06 +00:00
if (do_search) {
2023-11-15 19:17:02 +00:00
await searchQueries();
2023-11-14 23:03:06 +00:00
}
await drawAll();
} catch (e) {
2023-12-16 14:02:20 +00:00
showError(e.message);
2023-11-14 23:03:06 +00:00
}
enableButtons();
2022-11-01 16:19:26 +00:00
}
document.getElementById('params').onsubmit = function(event) {
2023-12-16 13:49:14 +00:00
if (document.activeElement === document.getElementById('search-query')) {
reloadAll(true);
} else {
reloadAll(queries.length == 0);
}
2022-08-21 10:11:25 +00:00
event.preventDefault();
}
2024-02-04 12:50:26 +00:00
const decodeState = (x) => JSON.parse(LZString.decompressFromEncodedURIComponent(x) || atob(x));
const encodeState = (x) => LZString.compressToEncodedURIComponent(JSON.stringify(x));
2022-08-21 10:11:25 +00:00
function saveState() {
2023-11-16 11:38:32 +00:00
const state = { host, user, queries, params, search_query, customized };
2022-08-21 10:11:25 +00:00
history.pushState(state, '',
2024-02-04 12:50:26 +00:00
window.location.pathname + (window.location.search || '') + '#' + encodeState(state));
2022-08-21 10:11:25 +00:00
}
2023-11-15 19:17:02 +00:00
async function searchQueries() {
2023-11-26 23:28:11 +00:00
let {reply, error} = await doFetch(search_query);
2023-11-14 23:03:06 +00:00
if (error) {
throw new Error(error);
}
2023-11-26 23:28:11 +00:00
let data = reply.data;
if (reply.rows == 0) {
2023-11-14 23:03:06 +00:00
throw new Error("Search query returned empty result.");
2023-11-26 23:28:11 +00:00
} else if (reply.meta.length != 2 || reply.meta[0].name != "title" || reply.meta[1].name != "query") {
2023-11-14 23:03:06 +00:00
throw new Error("Search query should return exactly two columns: title and query.");
2023-11-26 23:28:11 +00:00
} else if (!Array.isArray(data.title) || !Array.isArray(data.query) || data.title.length != data.query.length) {
2023-11-14 23:03:06 +00:00
throw new Error("Wrong data format of the search query.");
}
2023-11-26 23:28:11 +00:00
for (let i = 0; i < data.title.length ; i + + ) {
queries.push({title: data.title[i], query: data.query[i]});
2023-11-14 23:03:06 +00:00
}
regenerate();
}
2023-11-16 11:38:32 +00:00
function refreshCustomized(value) {
if (value !== undefined) {
customized = value;
}
document.getElementById('search-span').style.opacity = customized ? 0.5 : 1.0;
}
2023-12-16 13:49:14 +00:00
function updateFromState() {
2022-08-21 10:11:25 +00:00
document.getElementById('url').value = host;
document.getElementById('user').value = user;
document.getElementById('password').value = password;
2023-11-14 23:03:06 +00:00
document.getElementById('search-query').value = search_query;
2023-11-16 11:38:32 +00:00
refreshCustomized();
2023-12-16 13:49:14 +00:00
}
2022-08-21 10:11:25 +00:00
2023-12-16 13:49:14 +00:00
function regenerate() {
2022-08-21 10:11:25 +00:00
findParamsInQueries();
buildParams();
plots.forEach(elem => elem & & elem.destroy());
plots = queries.map(e => null);
while (charts.firstChild) {
charts.removeChild(charts.lastChild);
}
for (let i = 0; i < queries.length ; + + i ) {
insertChart(i);
}
}
window.onpopstate = function(event) {
if (!event.state) { return; }
2023-11-16 11:38:32 +00:00
({host, user, queries, params, search_query, customized} = event.state);
2023-12-16 13:49:14 +00:00
updateFromState();
2022-08-21 10:11:25 +00:00
regenerate();
drawAll();
};
if (window.location.hash) {
try {
2023-11-16 13:09:30 +00:00
let search_query_, customized_;
2024-02-04 12:50:26 +00:00
({host, user, queries, params, search_query_, customized_} = decodeState(window.location.hash.substring(1)));
2024-02-04 03:03:18 +00:00
2023-11-16 13:09:30 +00:00
// For compatibility with old URLs' hashes
search_query = search_query_ !== undefined ? search_query_ : search_query;
customized = customized_ !== undefined ? customized_ : true;
2022-08-21 10:11:25 +00:00
} catch {}
}
2023-11-14 23:03:06 +00:00
async function start() {
try {
2023-12-16 13:49:14 +00:00
updateFromState();
2023-11-14 23:03:06 +00:00
if (queries.length == 0) {
2023-11-15 19:17:02 +00:00
await searchQueries();
2023-11-14 23:03:06 +00:00
} else {
regenerate();
}
2023-11-15 19:17:02 +00:00
saveState();
2023-11-14 23:03:06 +00:00
let new_theme = window.localStorage.getItem('theme');
if (new_theme & & new_theme != theme) {
setTheme(new_theme);
} else {
drawAll();
}
} catch (e) {
2023-12-16 14:02:20 +00:00
showError(e.message);
2023-11-14 23:03:06 +00:00
}
2022-08-21 10:11:25 +00:00
}
2023-11-14 23:03:06 +00:00
start();
2022-08-21 10:11:25 +00:00
< / script >
< / body >
< / html >