mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
1895 lines
68 KiB
HTML
1895 lines
68 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDExIDEwIj48c3R5bGU+LmJne2ZpbGw6I2ZmMH0ub3tmaWxsOiMwMDB9PC9zdHlsZT48cmVjdCBjbGFzcz0iYmciIHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48cGF0aCBkPSJNMSwxIGgxIHY4IGgtMSB6IiBjbGFzcz0ibyIvPjxwYXRoIGQ9Ik0zLDEgaDEgdjggaC0xIHoiIGNsYXNzPSJvIi8+PHBhdGggZD0iTTUsMSBoMSB2OCBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNNywxIGgxIHY4IGgtMSB6IiBjbGFzcz0ibyIvPjxwYXRoIGQ9Ik05LDQuMjUgaDEgdjEuNSBoLTEgeiIgY2xhc3M9Im8iLz48L3N2Zz4K">
|
||
<title>ClickHouse Query</title>
|
||
|
||
<!-- Code Style:
|
||
|
||
Do not use any JavaScript or CSS frameworks or preprocessors.
|
||
This HTML page should not require any build systems (node.js, npm, gulp, etc.)
|
||
This HTML page should not be minified, instead it should be reasonably minimalistic by itself.
|
||
This HTML page should not load any external resources on load.
|
||
(CSS and JavaScript must be embedded directly to the page. No external fonts or images should be loaded).
|
||
This UI should look as lightweight, clean and fast as possible.
|
||
All UI elements must be aligned in pixel-perfect way.
|
||
There should not be any animations except the progress bar.
|
||
No unexpected changes in positions of elements while the page is loading.
|
||
Navigation by keyboard should work.
|
||
64-bit numbers must display correctly.
|
||
|
||
-->
|
||
|
||
<!-- Development Roadmap:
|
||
|
||
1. Support readonly servers.
|
||
Check if readonly = 1 (with SELECT FROM system.settings) to avoid sending settings. It can be done once on address/credentials change.
|
||
It can be done in background, e.g. wait 100 ms after address/credentials change and do the check.
|
||
Also, it can provide visual indication that credentials are correct.
|
||
|
||
Nice to have:
|
||
- resizable columns
|
||
- display totals
|
||
- display types in the header
|
||
- show/hide the editor
|
||
- check for readonly
|
||
- query caching
|
||
- downloads in any format
|
||
- history navigation
|
||
- short links by saving in a special table
|
||
- sharing by creating a short-living user
|
||
- notebook mode
|
||
|
||
-->
|
||
|
||
<style>
|
||
:root {
|
||
--background-color-rotate: 215;
|
||
--background-color: oklch(96% 0.0296 var(--background-color-rotate));
|
||
--element-background-color: #FFF;
|
||
--element-color-ok: #080;
|
||
--element-background-color-fail: #FEE;
|
||
--bar-color: #FF8; /* Light bar in background of table cells. */
|
||
--border-color: #CCC;
|
||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||
--button-color: #FF0;
|
||
--text-color: #000;
|
||
--text-color-active: #000;
|
||
--button-active-color: #FE0;
|
||
--button-active-text-color: #000;
|
||
--misc-text-color: #888;
|
||
--error-color: #FEE; /* Light-pink on light-cyan is so neat, I even want to trigger errors to see this cool combination of colors. */
|
||
--table-header-color: #EEE;
|
||
--table-hover-filter: brightness(0.9);
|
||
--link-color: #06D;
|
||
--logo-color: #FFF;
|
||
--border-color-selected: #F80;
|
||
--menu-toggle-color: #000;
|
||
|
||
--progress-color: #0C4;
|
||
--progress: 0%;
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--background-color-rotate: 250;
|
||
--background-color: oklch(15% 0.0296 var(--background-color-rotate));
|
||
--element-background-color: #000;
|
||
--element-color-ok: #8F8;
|
||
--element-background-color-fail: #300;
|
||
--bar-color: #320;
|
||
--border-color: #444;
|
||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||
--text-color: #CCC;
|
||
--text-color-active: #FFF;
|
||
--button-color: rgb(250 255 105);
|
||
--button-text-color: #000;
|
||
--button-active-color: #FE0;
|
||
--button-active-text-color: #000;
|
||
--misc-text-color: #888;
|
||
--error-color: #400;
|
||
--table-header-color: #102020;
|
||
--table-hover-filter: contrast(0.9) brightness(2);
|
||
--link-color: #4BDAF7;
|
||
--logo-color: #444;
|
||
--border-color-selected: #FF0;
|
||
--menu-toggle-color: #FF0;
|
||
|
||
--progress-color: #FF0;
|
||
}
|
||
|
||
*
|
||
{
|
||
box-sizing: border-box;
|
||
/* For iPad */
|
||
margin: 0;
|
||
border-radius: 0;
|
||
tab-size: 4;
|
||
}
|
||
|
||
html, body
|
||
{
|
||
height: 100%;
|
||
margin: 0;
|
||
/* This enables position: sticky on controls */
|
||
overflow: auto;
|
||
}
|
||
|
||
html
|
||
{
|
||
/* The fonts that have full support for hinting. */
|
||
font-family: Roboto Mono, Liberation Sans, DejaVu Sans, sans-serif, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji;
|
||
background: var(--background-color);
|
||
color: var(--text-color);
|
||
}
|
||
|
||
body
|
||
{
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
grid-template-areas: "menu main";
|
||
}
|
||
|
||
#menu
|
||
{
|
||
padding: 0.25rem;
|
||
grid-area: menu;
|
||
position: relative;
|
||
background: var(--table-header-color);
|
||
border-right: 1px solid var(--border-color);
|
||
}
|
||
|
||
#main
|
||
{
|
||
overflow: auto;
|
||
}
|
||
|
||
.menu-icon
|
||
{
|
||
width: 2.25em;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
fill: var(--menu-toggle-color);
|
||
}
|
||
|
||
.menu-icon:hover
|
||
{
|
||
transform: translate(1px, 1px);
|
||
}
|
||
|
||
#github-icon
|
||
{
|
||
position: absolute;
|
||
bottom: 0.25rem;
|
||
}
|
||
|
||
#github-icon-light, #github-icon-dark
|
||
{
|
||
display: none;
|
||
}
|
||
|
||
[data-theme="light"] #github-icon-light, [data-theme="dark"] #github-icon-dark
|
||
{
|
||
display: block;
|
||
}
|
||
|
||
.database button
|
||
{
|
||
cursor: pointer;
|
||
background: transparent;
|
||
font-size: 100%;
|
||
padding: 0;
|
||
user-select: none;
|
||
color: var(--text-color);
|
||
border-bottom: 1px dashed var(--text-color);
|
||
}
|
||
|
||
.database button:hover
|
||
{
|
||
color: var(--button-text-color);
|
||
background-color: var(--button-color);
|
||
}
|
||
|
||
.database button:focus
|
||
{
|
||
color: var(--button-text-color);
|
||
background-color: var(--button-color);
|
||
}
|
||
|
||
.tables
|
||
{
|
||
padding-left: 1rem;
|
||
}
|
||
|
||
.current
|
||
{
|
||
font-weight: bold;
|
||
}
|
||
|
||
.table
|
||
{
|
||
background-position: bottom;
|
||
background-size: 100% 2px;
|
||
background-repeat: no-repeat;
|
||
}
|
||
|
||
.table-addendum
|
||
{
|
||
position: absolute;
|
||
z-index: 1;
|
||
display: none;
|
||
white-space: pre;
|
||
transform: translate(0.25rem, calc(-50% + 0.5em));
|
||
color: var(--element-background-color);
|
||
background: var(--text-color);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 0.5rem;
|
||
padding: 0.25rem;
|
||
margin-left: 0.25rem;
|
||
}
|
||
|
||
.table-addendum::before {
|
||
content: " ";
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translate(-110%, -50%);
|
||
line-height: 0;
|
||
border-width: 0.5rem;
|
||
border-style: solid;
|
||
border-color: transparent var(--text-color) transparent transparent;
|
||
}
|
||
|
||
.table:hover .table-addendum
|
||
{
|
||
display: inline-block;
|
||
}
|
||
|
||
#main
|
||
{
|
||
grid-area: main;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
#controls
|
||
{
|
||
/* When a page will be scrolled horizontally due to large table size, keep controls in place. */
|
||
position: sticky;
|
||
left: 0;
|
||
}
|
||
|
||
/* Otherwise Webkit based browsers will display ugly border on focus. */
|
||
textarea, input, button
|
||
{
|
||
outline: none;
|
||
border: none;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.monospace
|
||
{
|
||
/* Prefer fonts that have full hinting info. This is important for non-retina displays.
|
||
Also I personally dislike "Ubuntu" font due to the similarity of 'r' and 'г' (it looks very ignorant). */
|
||
font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
|
||
}
|
||
|
||
.shadow
|
||
{
|
||
box-shadow: 0 0 1rem var(--shadow-color);
|
||
}
|
||
|
||
input, textarea
|
||
{
|
||
border: 1px solid var(--border-color);
|
||
/* The font must be not too small (to be inclusive) and not too large (it's less practical and make general feel of insecurity) */
|
||
font-size: 11pt;
|
||
padding: 0.25rem;
|
||
background-color: var(--element-background-color);
|
||
}
|
||
|
||
input:focus, textarea:focus
|
||
{
|
||
color: var(--text-color-active);
|
||
}
|
||
|
||
#query
|
||
{
|
||
display: block;
|
||
/* Make enough space for even big queries. */
|
||
height: 20vh;
|
||
/* Keeps query text-area's width full screen even when user adjusting the width of the query box. */
|
||
width: 100%;
|
||
min-width: 100%;
|
||
max-width: 100%;
|
||
margin-top: -1px; /* To compensate the border. */
|
||
contain: strict; /* Less input lag even if the table is large. */
|
||
}
|
||
|
||
#inputs
|
||
{
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#url
|
||
{
|
||
width: 70%;
|
||
}
|
||
|
||
#user, #password
|
||
{
|
||
width: calc(15% + 1px);
|
||
margin-left: -1px;
|
||
}
|
||
|
||
input.ok
|
||
{
|
||
color: var(--element-color-ok);
|
||
}
|
||
|
||
input.fail
|
||
{
|
||
background-color: var(--element-background-color-fail);
|
||
}
|
||
|
||
#run_div
|
||
{
|
||
margin-top: 1rem;
|
||
display: grid;
|
||
place-items: center;
|
||
gap: 1rem;
|
||
grid-template-columns: auto auto auto 1fr auto auto;
|
||
grid-template-areas: "run hint hourglass progress-bar progress-text theme";
|
||
}
|
||
|
||
#run
|
||
{
|
||
grid-area: run;
|
||
user-select: none;
|
||
color: var(--button-text-color);
|
||
background-color: var(--button-color);
|
||
padding: 0.25rem 1rem;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
font-size: 100%; /* Otherwise button element will have a lower font size. */
|
||
}
|
||
|
||
#run:hover, #run:focus
|
||
{
|
||
color: var(--button-active-text-color);
|
||
background-color: var(--button-active-color);
|
||
}
|
||
|
||
#hint
|
||
{
|
||
grid-area: hint;
|
||
color: var(--misc-text-color);
|
||
}
|
||
|
||
#hourglass
|
||
{
|
||
grid-area: hourglass;
|
||
display: none;
|
||
font-size: 110%;
|
||
color: #888;
|
||
animation: hourglass-animation 2s linear infinite;
|
||
}
|
||
|
||
#check-mark
|
||
{
|
||
grid-area: hourglass;
|
||
display: none;
|
||
font-size: 110%;
|
||
color: #080;
|
||
}
|
||
|
||
#stats
|
||
{
|
||
grid-area: progress-text;
|
||
color: var(--misc-text-color);
|
||
}
|
||
|
||
#stats b
|
||
{
|
||
color: var(--progress-color);
|
||
}
|
||
|
||
#progress
|
||
{
|
||
display: none;
|
||
width: 100%;
|
||
color: var(--misc-text-color);
|
||
|
||
grid-area: progress-bar;
|
||
background: linear-gradient(to right,
|
||
transparent 0%,
|
||
var(--progress-color) var(--progress),
|
||
transparent var(--progress));
|
||
}
|
||
|
||
#theme
|
||
{
|
||
grid-area: theme;
|
||
}
|
||
|
||
#toggle-light, #toggle-dark
|
||
{
|
||
cursor: pointer;
|
||
}
|
||
|
||
#data_div
|
||
{
|
||
margin-top: 1rem;
|
||
background-color: var(--element-background-color);
|
||
}
|
||
|
||
#data-table
|
||
{
|
||
width: 100%;
|
||
border-spacing: 0;
|
||
background-color: var(--element-background-color);
|
||
}
|
||
|
||
/* Will be displayed when user specified custom format. */
|
||
#data-unparsed
|
||
{
|
||
width: fit-content;
|
||
background-color: var(--element-background-color);
|
||
margin-top: 0rem;
|
||
padding: 0.25rem 0.5rem;
|
||
display: none;
|
||
}
|
||
|
||
td
|
||
{
|
||
background-color: var(--element-background-color);
|
||
/* For wide tables any individual column will be no more than 50% of page width. */
|
||
max-width: 50vw;
|
||
/* The content is cut unless you hover. */
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
padding: 0.25rem 0.5rem;
|
||
border: 1px solid var(--border-color);
|
||
white-space: pre;
|
||
vertical-align: top;
|
||
}
|
||
|
||
td u
|
||
{
|
||
text-decoration-color: var(--misc-text-color);
|
||
}
|
||
|
||
.right
|
||
{
|
||
text-align: right;
|
||
}
|
||
|
||
th
|
||
{
|
||
position: sticky;
|
||
top: -0.5rem;
|
||
padding: 0.25rem 0.5rem;
|
||
text-align: center;
|
||
background-color: var(--table-header-color);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.type-hint
|
||
{
|
||
display: none;
|
||
position: absolute;
|
||
user-select: none;
|
||
right: 0;
|
||
top: -1.25em;
|
||
font-size: 75%;
|
||
font-weight: normal;
|
||
color: var(--misc-text-color);
|
||
z-index: 1;
|
||
background: var(--element-background-color);
|
||
}
|
||
|
||
th:hover .type-hint
|
||
{
|
||
display: inline-block;
|
||
}
|
||
|
||
/* The row under mouse pointer is highlight for better legibility. */
|
||
tr:hover
|
||
{
|
||
filter: var(--table-hover-filter);
|
||
}
|
||
|
||
.tr-selected
|
||
{
|
||
filter: var(--table-hover-filter);
|
||
}
|
||
|
||
#error
|
||
{
|
||
background: var(--error-color);
|
||
white-space: pre-wrap;
|
||
padding: 0.5rem 1rem;
|
||
display: none;
|
||
}
|
||
|
||
.td-selected
|
||
{
|
||
white-space: pre-wrap;
|
||
max-width: none;
|
||
outline: 2px solid var(--border-color-selected);
|
||
}
|
||
|
||
td.transposed
|
||
{
|
||
max-width: none;
|
||
overflow: auto;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
td.empty-result
|
||
{
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.row-number
|
||
{
|
||
width: 1%;
|
||
text-align: right;
|
||
background-color: var(--table-header-color);
|
||
color: var(--misc-text-color);
|
||
}
|
||
|
||
div.empty-result
|
||
{
|
||
opacity: 10%;
|
||
font-size: 7vw;
|
||
font-family: Liberation Sans, DejaVu Sans, sans-serif;
|
||
}
|
||
|
||
@keyframes hourglass-animation {
|
||
0% { transform: none; }
|
||
25% { transform: rotate(180deg); }
|
||
50% { transform: rotate(180deg); }
|
||
75% { transform: rotate(360deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
a, a:visited
|
||
{
|
||
color: var(--link-color);
|
||
text-decoration: none;
|
||
}
|
||
|
||
#graph
|
||
{
|
||
display: none;
|
||
margin: auto;
|
||
}
|
||
|
||
/* This is for graph in svg */
|
||
text
|
||
{
|
||
font-size: 14px;
|
||
fill: var(--text-color);
|
||
}
|
||
|
||
.node rect
|
||
{
|
||
fill: var(--element-background-color);
|
||
filter: drop-shadow(.2rem .2rem .2rem var(--shadow-color));
|
||
stroke: var(--border-color);
|
||
}
|
||
|
||
.edgePath path
|
||
{
|
||
stroke: var(--text-color);
|
||
}
|
||
|
||
marker
|
||
{
|
||
fill: var(--text-color);
|
||
}
|
||
|
||
#logo
|
||
{
|
||
fill: var(--logo-color);
|
||
}
|
||
|
||
#cloud-logo
|
||
{
|
||
color: transparent;
|
||
text-shadow: 0 0 0 var(--logo-color);
|
||
font-size: 10vw;
|
||
display: block;
|
||
}
|
||
|
||
#logo-container
|
||
{
|
||
text-align: center;
|
||
margin-top: 5em;
|
||
line-height: 0.75;
|
||
}
|
||
|
||
#chart
|
||
{
|
||
border: 1px solid var(--border-color);
|
||
background-color: var(--element-background-color);
|
||
filter: drop-shadow(.2rem .2rem .2rem var(--shadow-color));
|
||
display: none;
|
||
height: 70vh;
|
||
}
|
||
|
||
#url_status
|
||
{
|
||
position: absolute;
|
||
display: inline-block;
|
||
font-size: 11pt;
|
||
padding: 0.25rem;
|
||
transform: translateX(-100%);
|
||
color: transparent;
|
||
user-select: none;
|
||
}
|
||
|
||
#url_status.ok
|
||
{
|
||
color: #0F0;
|
||
}
|
||
|
||
#url_status.fail
|
||
{
|
||
color: #F00;
|
||
}
|
||
|
||
#server_info
|
||
{
|
||
color: var(--misc-text-color);
|
||
}
|
||
|
||
/* This is for charts (uPlot), Copyright (c) 2022 Leon Sorokin, MIT License, https://github.com/leeoniya/uPlot/ */
|
||
.u-wrap {position: relative;user-select: none;}
|
||
.u-over, .u-under, .u-axis {position: absolute;}
|
||
.u-under {overflow: hidden;}
|
||
.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}
|
||
.u-legend {margin: auto;text-align: center; margin-top: 1em; font-family: Liberation Mono, DejaVu Sans Mono, MonoLisa, Consolas, monospace;}
|
||
.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 td { min-width: 13em; }
|
||
.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>
|
||
<div id="menu">
|
||
<div class="menu-icon">
|
||
<svg id="databases-toggle" viewBox="0 0 9 9"><path d="M0,0 h1 v8 h-1 z"/><path d="M2,0 h1 v8 h-1 z"/><path d="M4,0 h1 v8 h-1 z"/><path d="M6,0 h1 v8 h-1 z"/><path d="M8,3.25 h1 v1.5 h-1 z"/></svg>
|
||
</div>
|
||
<div id="databases"></div>
|
||
<!-- https://github.com/logos -->
|
||
<div id="github-icon" class="menu-icon">
|
||
<a href="https://github.com/ClickHouse/ClickHouse/">
|
||
<svg id="github-icon-light" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
||
<svg id="github-icon-dark" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div id="main">
|
||
<div id="controls">
|
||
<div id="inputs">
|
||
<input spellcheck="false" class="monospace shadow" id="url" type="text" value="http://localhost:8123/" placeholder="url" /><span id="url_status"><span id="server_info"></span>●</span><input spellcheck="false" class="monospace shadow" id="user" type="text" value="default" placeholder="user" /><input class="monospace shadow" id="password" type="password" placeholder="password" />
|
||
</div>
|
||
<div id="query_div"><textarea autofocus spellcheck="false" data-gramm="false" class="monospace shadow" id="query"></textarea></div>
|
||
<div id="run_div">
|
||
<button class="shadow" id="run">Run</button>
|
||
<div id="hint"> (Ctrl/Cmd+Enter)</div>
|
||
<div id="hourglass">⧗</div>
|
||
<div id="check-mark">✔</div>
|
||
<div id="progress"></div>
|
||
<div id="stats"></div>
|
||
<div id="theme"><span id="toggle-dark">🌑</span><span id="toggle-light">🌞</span></div>
|
||
</div>
|
||
</div>
|
||
<div id="data_div">
|
||
<table class="monospace shadow" id="data-table"></table>
|
||
<pre class="monospace shadow" id="data-unparsed"></pre>
|
||
</div>
|
||
<div id="chart"></div>
|
||
<svg id="graph" fill="none"></svg>
|
||
<p id="error" class="monospace shadow">
|
||
</p>
|
||
<p id="logo-container">
|
||
<a href="https://clickhouse.com/">
|
||
<svg id="logo" width="50%" viewBox="0 0 180 28" version="1.1" xmlns="http://www.w3.org/2000/svg"><title>Powered by ClickHouse</title><g transform="translate(21, 0)"><path d="M9.76,20.95 C8.43,20.97 7.11,20.73 5.87,20.26 C4.72,19.80 3.67,19.11 2.81,18.22 C1.90,17.32 1.20,16.25 0.74,15.07 C-0.24,12.29 -0.24,9.27 0.74,6.49 C1.67,4.07 3.61,2.16 6.08,1.26 C7.35,0.79 8.71,0.55 10.07,0.57 C11.16,0.54 12.24,0.67 13.30,0.93 C14.15,1.16 14.98,1.47 15.78,1.87 L15.03,4.17 C14.33,3.70 13.57,3.32 12.78,3.03 C11.83,2.71 10.83,2.56 9.82,2.58 C7.96,2.56 6.18,3.35 4.96,4.74 C4.30,5.50 3.79,6.38 3.47,7.33 C3.08,8.46 2.89,9.65 2.91,10.85 C2.90,12.02 3.07,13.18 3.43,14.29 C3.73,15.23 4.21,16.09 4.86,16.84 C5.46,17.52 6.21,18.06 7.05,18.41 C7.95,18.78 8.92,18.96 9.90,18.96 C10.80,18.96 11.68,18.82 12.53,18.55 C13.34,18.27 14.12,17.91 14.85,17.47 L15.57,19.20 C14.87,19.72 14.09,20.13 13.25,20.40 C12.13,20.78 10.95,20.97 9.76,20.95 L9.76,20.95 Z"></path><polygon points="21.20 0 21.20 20.67 18.73 20.67 18.73 0 21.20 0"></polygon><path d="M26.51,4.15 C26.09,4.18 25.68,4.03 25.38,3.74 C25.08,3.46 24.92,3.05 24.94,2.64 C24.92,2.23 25.07,1.83 25.35,1.52 C25.99,0.92 26.99,0.92 27.63,1.52 C27.92,1.82 28.07,2.23 28.06,2.64 C28.07,3.05 27.92,3.44 27.63,3.72 C27.33,4.01 26.93,4.16 26.51,4.15 L26.51,4.15 Z M27.75,6.31 L27.75,20.56 L25.31,20.56 L25.31,6.31 L27.77,6.31 L27.75,6.31 Z"></path><path d="M38.20,20.95 C37.23,20.96 36.27,20.78 35.36,20.44 C34.50,20.12 33.72,19.62 33.07,18.98 C32.40,18.30 31.88,17.51 31.54,16.63 C30.78,14.61 30.78,12.39 31.54,10.36 C31.89,9.48 32.43,8.68 33.11,8.02 C33.80,7.36 34.62,6.85 35.53,6.53 C36.46,6.19 37.45,6.02 38.44,6.02 C39.23,6.02 40.02,6.11 40.78,6.29 C41.40,6.43 41.99,6.67 42.52,7.00 L41.73,9.14 C41.23,8.76 40.68,8.44 40.10,8.18 C39.49,7.95 38.85,7.84 38.20,7.86 C37.57,7.85 36.95,7.97 36.38,8.20 C35.80,8.44 35.28,8.80 34.87,9.26 C34.42,9.77 34.07,10.36 33.85,10.99 C33.58,11.78 33.45,12.61 33.46,13.44 C33.36,14.92 33.85,16.38 34.83,17.51 C35.81,18.45 37.15,18.94 38.53,18.87 C39.72,18.85 40.89,18.48 41.88,17.81 L42.44,19.61 C41.88,20.02 41.26,20.33 40.60,20.52 C39.82,20.79 39.01,20.93 38.20,20.95 L38.20,20.95 Z"></path><path d="M47.94,0 L47.94,20.67 L45.44,20.67 L45.44,0 L47.94,0 Z M54.35,20.67 L48.27,13.01 L54.27,6.33 L56.83,6.33 L50.90,12.72 L57.24,20.67 L54.35,20.67 L54.35,20.67 Z"></path><polygon points="73.11 11.87 62.77 11.87 62.77 20.65 60.10 20.65 60.10 0.85 62.77 0.85 62.77 9.89 73.11 9.89 73.11 0.85 75.78 0.85 75.78 20.67 73.11 20.67"></polygon><path d="M86.30,20.95 C85.38,20.95 84.47,20.78 83.61,20.44 C82.78,20.12 82.01,19.63 81.38,19.00 C80.72,18.32 80.21,17.53 79.87,16.65 C79.12,14.60 79.12,12.35 79.87,10.30 C80.21,9.43 80.72,8.64 81.38,7.98 C82.02,7.35 82.78,6.86 83.61,6.53 C84.47,6.20 85.38,6.02 86.30,6.02 C87.23,6.02 88.15,6.20 89.01,6.53 C89.86,6.86 90.62,7.35 91.27,7.98 C91.94,8.64 92.47,9.43 92.82,10.30 C93.59,12.35 93.59,14.60 92.82,16.65 C92.50,17.52 92.00,18.32 91.37,19.00 C90.73,19.63 89.96,20.12 89.12,20.44 C88.22,20.79 87.27,20.96 86.30,20.95 Z M86.30,19.10 C86.88,19.10 87.46,18.99 88.00,18.79 C88.56,18.59 89.07,18.25 89.47,17.81 C89.93,17.31 90.28,16.71 90.50,16.06 C90.78,15.23 90.91,14.35 90.89,13.48 C90.92,12.60 90.78,11.72 90.50,10.89 C90.28,10.25 89.93,9.66 89.47,9.16 C89.07,8.72 88.56,8.39 88.00,8.18 C87.46,7.98 86.88,7.88 86.30,7.88 C85.74,7.88 85.18,7.98 84.65,8.18 C84.10,8.39 83.61,8.73 83.22,9.16 C82.77,9.66 82.42,10.25 82.21,10.89 C81.94,11.73 81.81,12.60 81.84,13.48 C81.81,14.35 81.94,15.23 82.21,16.06 C82.42,16.71 82.76,17.31 83.22,17.81 C83.61,18.25 84.10,18.58 84.65,18.79 C85.18,18.99 85.74,19.10 86.30,19.10 L86.30,19.10 Z"></path><path d="M104.21,20.12 C103.26,20.71 102.14,21.00 101.01,20.95 C99.79,21.00 98.61,20.56 97.74,19.73 C96.83,18.70 96.38,17.34 96.50,15.98 L96.50,6.31 L98.98,6.31 L98.98,15.98 C98.97,16.47 99.04,16.95 99.19,17.41 C99.30,17.76 99.50,18.07 99.77,18.32 C100.01,18.55 100.31,18.72 100.64,18.81 C100.99,18.91 101.35,18.96 101.71,18.96 C102.65,19.00 103.58,18.73 104.34,18.18 C105.05,17.63 105.63,16.94 106.03,16.14 L106.03,6.31 L108.49,6.31 L108.49,20.56 L106.32,20.56 L106.10,18.00 L106.10,18.00 C105.68,18.86 105.03,19.60 104.21,20.12 L104.21,20.12 Z"></path><path d="M116.42,20.95 C115.80,20.99 115.17,20.99 114.55,20.95 C114.09,20.89 113.62,20.80 113.17,20.69 C112.81,20.60 112.46,20.48 112.11,20.34 C111.83,20.22 111.55,20.07 111.29,19.91 L112.09,17.88 C112.27,18.02 112.47,18.15 112.67,18.26 C112.97,18.43 113.28,18.57 113.60,18.69 C114.01,18.85 114.43,18.97 114.86,19.06 C115.38,19.16 115.90,19.20 116.42,19.20 C117.26,19.26 118.09,19.04 118.79,18.59 C119.33,18.19 119.63,17.56 119.60,16.90 C119.62,16.34 119.38,15.80 118.94,15.45 C118.24,14.98 117.47,14.63 116.64,14.43 L114.86,13.84 C114.33,13.66 113.82,13.39 113.36,13.07 C112.91,12.76 112.55,12.35 112.28,11.89 C111.99,11.34 111.84,10.72 111.87,10.10 C111.79,8.92 112.33,7.79 113.29,7.10 C114.46,6.39 115.83,6.05 117.20,6.13 C118.07,6.11 118.94,6.19 119.79,6.37 C120.38,6.49 120.97,6.68 121.52,6.92 L120.95,8.96 C120.47,8.73 119.99,8.52 119.50,8.34 C118.79,8.11 118.05,8.00 117.31,8.02 C116.53,7.98 115.76,8.13 115.05,8.45 C114.47,8.75 114.14,9.37 114.20,10.01 C114.20,10.33 114.29,10.63 114.47,10.89 C114.66,11.15 114.90,11.36 115.18,11.52 C115.50,11.72 115.85,11.87 116.21,11.99 L117.45,12.36 L119.13,12.91 C119.66,13.10 120.16,13.36 120.61,13.70 C121.60,14.41 122.15,15.56 122.08,16.76 C122.12,17.46 121.95,18.17 121.61,18.79 C121.30,19.33 120.86,19.79 120.35,20.14 C119.80,20.50 119.19,20.76 118.55,20.91 C117.84,21.01 117.13,21.02 116.42,20.95 Z"></path><path d="M136.37,19.42 C135.76,19.91 135.06,20.28 134.31,20.50 C133.39,20.80 132.43,20.95 131.47,20.93 C129.48,21.04 127.53,20.30 126.14,18.89 C124.81,17.36 124.15,15.38 124.28,13.38 C124.26,12.31 124.43,11.26 124.79,10.26 C125.11,9.39 125.59,8.60 126.22,7.92 C126.79,7.29 127.50,6.79 128.29,6.45 C129.13,6.11 130.03,5.94 130.93,5.94 C131.84,5.93 132.74,6.10 133.58,6.45 C134.35,6.77 135.02,7.28 135.53,7.94 C136.06,8.66 136.42,9.50 136.56,10.38 C136.75,11.50 136.75,12.64 136.56,13.76 L126.72,13.76 C126.83,17.17 128.46,18.87 131.62,18.87 C132.38,18.89 133.13,18.77 133.85,18.53 C134.49,18.30 135.10,18.00 135.67,17.63 L136.37,19.42 Z M131.02,7.73 C130.01,7.72 129.05,8.12 128.35,8.83 C127.52,9.75 127.03,10.91 126.94,12.13 L134.33,12.13 C134.53,10.97 134.25,9.78 133.56,8.81 C132.93,8.09 131.99,7.69 131.02,7.73 Z"></path></g></svg>
|
||
</a>
|
||
<a id="cloud-logo" href="https://clickhouse.cloud/">☁</a>
|
||
</p>
|
||
</div>
|
||
</body>
|
||
|
||
<script type="text/javascript">
|
||
|
||
/// Incremental request number. When response is received,
|
||
/// if its request number does not equal to the current request number, response will be ignored.
|
||
/// This is to avoid race conditions.
|
||
let request_num = 0;
|
||
|
||
/// Save query in history only if it is different.
|
||
let previous_query = '';
|
||
|
||
/// Start of the last query
|
||
let last_query_start = 0;
|
||
|
||
const current_url = new URL(window.location);
|
||
const opened_locally = location.protocol == 'file:';
|
||
|
||
/// Run query instantly after page is loaded if the run parameter is present.
|
||
const run_immediately = current_url.searchParams.has("run");
|
||
|
||
let url_elem = document.getElementById('url');
|
||
let user_elem = document.getElementById('user');
|
||
let password_elem = document.getElementById('password');
|
||
|
||
const server_address = current_url.searchParams.get('url');
|
||
if (server_address) {
|
||
url_elem.value = server_address;
|
||
} else if (!opened_locally) {
|
||
/// Substitute the address of the server where the page is served.
|
||
url_elem.value = location.origin;
|
||
}
|
||
|
||
/// Substitute username if it's specified in the query string
|
||
const user_from_url = current_url.searchParams.get('user');
|
||
if (user_from_url) {
|
||
user_elem.value = user_from_url;
|
||
}
|
||
|
||
const pass_from_url = current_url.searchParams.get('password');
|
||
if (pass_from_url) {
|
||
password_elem.value = pass_from_url;
|
||
/// Browsers don't allow manipulating history for the 'file:' protocol.
|
||
if (!opened_locally) {
|
||
let replaced_pass = current_url.searchParams;
|
||
replaced_pass.delete('password');
|
||
window.history.replaceState(null, '',
|
||
window.location.origin + window.location.pathname + '?'
|
||
+ replaced_pass.toString() + window.location.hash);
|
||
}
|
||
}
|
||
|
||
async function ping(url) {
|
||
try {
|
||
let response = await fetch(new URL(url).origin + "?query", { method: 'OPTIONS', headers: { 'Authorization': 'never' } });
|
||
return response.ok;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
let ping_request_num = 0;
|
||
function checkURL() {
|
||
++ping_request_num;
|
||
const current_ping_request_num = ping_request_num;
|
||
let elem = document.getElementById('url_status');
|
||
elem.className = '';
|
||
document.getElementById('server_info').innerText = '';
|
||
ping(url_elem.value).then(status => {
|
||
if (current_ping_request_num == ping_request_num) {
|
||
elem.className = status ? 'ok' : 'fail';
|
||
if (status) {
|
||
checkCredentials();
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
url_elem.addEventListener('input', checkURL);
|
||
checkURL();
|
||
|
||
async function getServerStatus(server_address, user, password) {
|
||
let url = server_address +
|
||
(server_address.indexOf('?') >= 0 ? '&' : '?') +
|
||
'add_http_cors_header=1&default_format=JSONEachRow';
|
||
if (user) url += '&user=' + encodeURIComponent(user);
|
||
if (password) url += '&password=' + encodeURIComponent(password);
|
||
try {
|
||
let response = await fetch(url, { method: "POST", body: "SELECT version() AS v, uptime() AS t", headers: { 'Authorization': 'never' } });
|
||
if (!response.ok) return false;
|
||
json = await response.json();
|
||
return json;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
let load_databases_request_num = 0;
|
||
async function loadDatabases(server_address, user, password) {
|
||
++load_databases_request_num;
|
||
const current_load_databases_request_num = load_databases_request_num;
|
||
let url = server_address +
|
||
(server_address.indexOf('?') >= 0 ? '&' : '?') +
|
||
'add_http_cors_header=1&default_format=JSON';
|
||
if (user) url += '&user=' + encodeURIComponent(user);
|
||
if (password) url += '&password=' + encodeURIComponent(password);
|
||
|
||
let response = await fetch(url, {
|
||
method: "POST",
|
||
body: `SELECT database, database = currentDatabase() AS current
|
||
FROM system.databases WHERE database != 'INFORMATION_SCHEMA'
|
||
ORDER BY
|
||
database = 'information_schema',
|
||
database = 'system',
|
||
database != 'default',
|
||
database`,
|
||
headers: { 'Authorization': 'never' } });
|
||
if (!response.ok) return false;
|
||
json = await response.json();
|
||
|
||
if (current_load_databases_request_num != load_databases_request_num) return false;
|
||
|
||
let databases_elem = document.getElementById('databases');
|
||
while (databases_elem.firstChild) databases_elem.removeChild(databases_elem.firstChild);
|
||
|
||
for (let elem of json.data) {
|
||
let database = document.createElement('div');
|
||
database.className = 'database';
|
||
let database_link = document.createElement('button');
|
||
if (elem.current) database_link.className = 'current';
|
||
database_link.append(elem.database);
|
||
database.appendChild(database_link);
|
||
let tables = document.createElement('div');
|
||
tables.className = 'tables';
|
||
database.appendChild(tables);
|
||
databases_elem.appendChild(database);
|
||
|
||
database_link.addEventListener('click', e => {
|
||
if (!database.dataset['opened']) {
|
||
loadTables(server_address, user, password, elem.database, database).then(() => database.dataset['opened'] = 1);
|
||
} else {
|
||
delete database.dataset['opened'];
|
||
while (tables.firstChild) tables.removeChild(tables.firstChild);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
let load_tables_request_num = 0;
|
||
async function loadTables(server_address, user, password, database, container) {
|
||
++load_tables_request_num;
|
||
const current_load_tables_request_num = load_tables_request_num;
|
||
let url = server_address +
|
||
(server_address.indexOf('?') >= 0 ? '&' : '?') +
|
||
'add_http_cors_header=1&default_format=JSON';
|
||
if (user) url += '&user=' + encodeURIComponent(user);
|
||
if (password) url += '&password=' + encodeURIComponent(password);
|
||
url += '¶m_database=' + encodeURIComponent(database);
|
||
|
||
let response = await fetch(url, {
|
||
method: "POST",
|
||
body: `SELECT table, engine, total_rows, total_bytes FROM system.tables WHERE database = {database:String}
|
||
ORDER BY table LIKE '.inner%', table`,
|
||
headers: { 'Authorization': 'never' } });
|
||
if (!response.ok) return false;
|
||
json = await response.json();
|
||
|
||
if (current_load_tables_request_num != load_tables_request_num) return false;
|
||
|
||
container.parentElement.querySelector('.current').classList.remove('current');
|
||
container.firstChild.classList.add('current');
|
||
let tables_elem = container.querySelector('.tables');
|
||
while (tables_elem.firstChild) tables_elem.removeChild(tables_elem.firstChild);
|
||
|
||
let max_total_bytes = Math.max(...json.data.map(elem => +elem.total_bytes));
|
||
|
||
for (let elem of json.data) {
|
||
let table = document.createElement('div');
|
||
table.className = 'table';
|
||
let table_link = document.createElement('button');
|
||
table_link.append(elem.table);
|
||
|
||
if (max_total_bytes && +elem.total_bytes) {
|
||
const percent = Number(100 * +elem.total_bytes / max_total_bytes).toFixed(2);
|
||
table.style.backgroundImage = `linear-gradient(to right,
|
||
var(--progress-color) 0%,
|
||
var(--progress-color) ${percent}%,
|
||
transparent ${percent}%)`;
|
||
}
|
||
|
||
let table_addendum = document.createElement('span');
|
||
table_addendum.className = 'table-addendum';
|
||
|
||
let addendum = '';
|
||
if (+elem.total_bytes) addendum += formatReadableBytes(elem.total_bytes) + '\n';
|
||
if (+elem.total_rows) addendum += formatReadableRows(elem.total_rows) + ' rows\n';
|
||
addendum += elem.engine;
|
||
table_addendum.append(addendum);
|
||
|
||
table.appendChild(table_link);
|
||
table.appendChild(table_addendum);
|
||
tables_elem.appendChild(table);
|
||
|
||
table.addEventListener('click', e => {
|
||
const value = `SELECT * FROM "${database}"."${elem.table}" LIMIT 100`;
|
||
if (query_area.value == value) {
|
||
/// Double click will run the query.
|
||
post();
|
||
} else {
|
||
query_area.value = value;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (json.data.length == 0) {
|
||
tables_elem.append('no tables');
|
||
}
|
||
}
|
||
|
||
let databases_toggle = document.getElementById('databases-toggle');
|
||
databases_toggle.addEventListener('click', e => {
|
||
if (!databases_toggle.dataset['opened']) {
|
||
loadDatabases(url_elem.value, user_elem.value, password_elem.value).then(() => databases_toggle.dataset['opened'] = 1);
|
||
} else {
|
||
delete databases_toggle.dataset['opened'];
|
||
let databases_elem = document.getElementById('databases');
|
||
while (databases_elem.firstChild) databases_elem.removeChild(databases_elem.firstChild);
|
||
}
|
||
});
|
||
|
||
function formatUptime(t) {
|
||
if (t < 60) { return t + " sec" }
|
||
if (t < 3600) { return Number(t / 60).toFixed() + " min" }
|
||
if (t < 86400) { return Number(t / 3600).toFixed() + " hr" }
|
||
const days = Number(t / 86400).toFixed();
|
||
return days + " day" + (days > 1 ? 's' : '');
|
||
}
|
||
|
||
let check_credentials_request_num = 0;
|
||
function checkCredentials() {
|
||
++check_credentials_request_num;
|
||
const current_check_credentials_request_num = check_credentials_request_num;
|
||
getServerStatus(url_elem.value, user_elem.value, password_elem.value).then(json => {
|
||
if (current_check_credentials_request_num == check_credentials_request_num) {
|
||
if (json) {
|
||
[user_elem, password_elem].forEach(e => { e.classList.remove('fail'); e.classList.add('ok'); });
|
||
document.getElementById('server_info').innerText = `v${json.v}, uptime ${formatUptime(json.t)} `;
|
||
} else {
|
||
[user_elem, password_elem].forEach(e => { e.classList.remove('ok'); e.classList.add('fail'); });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
user_elem.addEventListener('input', checkCredentials);
|
||
password_elem.addEventListener('input', checkCredentials);
|
||
|
||
|
||
function queryToColor(str) {
|
||
let hash = 0;
|
||
for (let i = 0; i < str.length; i++) {
|
||
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
||
hash = hash & hash;
|
||
}
|
||
|
||
/// Limited range to avoid violet colors.
|
||
return 50 + Math.abs(hash % 200);
|
||
}
|
||
|
||
|
||
let row_idx = 0;
|
||
let elapsed_ns = 0;
|
||
let incomplete_result = false;
|
||
let is_explain_graph = false;
|
||
let explain_graph = '';
|
||
let column_is_number = {};
|
||
let extremes = {};
|
||
let header = {};
|
||
|
||
const default_format = 'JSONStringsEachRowWithProgress';
|
||
|
||
let controller = null;
|
||
async function postImpl(posted_request_num, query)
|
||
{
|
||
const user = user_elem.value;
|
||
const password = password_elem.value;
|
||
const server_address = url_elem.value;
|
||
let url = server_address +
|
||
(server_address.indexOf('?') >= 0 ? '&' : '?') +
|
||
/// Ask server to allow cross-domain requests.
|
||
'add_http_cors_header=1';
|
||
if (user) url += '&user=' + encodeURIComponent(user);
|
||
if (password) url += '&password=' + encodeURIComponent(password);
|
||
url += '&default_format=' + default_format + '&enable_http_compression=1';
|
||
|
||
/// Extremes only if the format is likely to be unchanged. This is imprecise.
|
||
if (!query.match(/\bFORMAT\s+\w+/i)) url += '&extremes=1';
|
||
|
||
last_query_start = performance.now();
|
||
|
||
document.getElementById('check-mark').style.display = 'none';
|
||
document.getElementById('hourglass').style.display = 'inline-block';
|
||
|
||
clear();
|
||
|
||
let is_table = false;
|
||
let is_raw = false;
|
||
let is_chart = false;
|
||
let is_error = false;
|
||
|
||
let response, reply, format;
|
||
reply = '';
|
||
try {
|
||
controller = new AbortController();
|
||
response = await fetch(url, { method: "POST", body: query, signal: controller.signal,
|
||
headers: { 'Authorization': 'never' } });
|
||
|
||
format = response.headers.get('X-ClickHouse-Format') ?? default_format;
|
||
|
||
if (!response.ok) {
|
||
is_error = true;
|
||
reply = await response.text();
|
||
if (posted_request_num != request_num) { return; }
|
||
|
||
let has_exception = false;
|
||
for (line of reply.split('\n')) {
|
||
if (line.startsWith(`{"exception":`)) {
|
||
update(JSON.parse(line));
|
||
has_exception = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!has_exception) {
|
||
renderError(reply);
|
||
}
|
||
|
||
document.getElementById('hourglass').style.display = 'none';
|
||
document.documentElement.style.setProperty('--progress', '0');
|
||
return;
|
||
} else if (format == default_format) {
|
||
is_table = true;
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
let buffer = '';
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (posted_request_num != request_num) { return; }
|
||
if (done) break;
|
||
|
||
let new_content = decoder.decode(value, { stream: true });
|
||
buffer += new_content;
|
||
reply += new_content;
|
||
|
||
let lines = buffer.split('\n');
|
||
let fragment = document.createDocumentFragment();
|
||
|
||
let cont = true;
|
||
for (line of lines.slice(0, -1)) {
|
||
const data = JSON.parse(line);
|
||
cont = update(data, fragment);
|
||
if (!cont) { break; }
|
||
}
|
||
|
||
if (fragment.hasChildNodes()) {
|
||
document.getElementById('tbody').appendChild(fragment);
|
||
}
|
||
|
||
if (!cont)
|
||
{
|
||
controller.abort();
|
||
break;
|
||
}
|
||
buffer = lines[lines.length - 1];
|
||
}
|
||
} else if (format == 'JSONCompactColumns') {
|
||
is_chart = true;
|
||
reply = await response.text();
|
||
if (posted_request_num != request_num) { return; }
|
||
renderChart(JSON.parse(reply));
|
||
} else {
|
||
is_raw = true;
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
let buffer = '';
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
let new_content = decoder.decode(value, { stream: true });
|
||
reply += new_content;
|
||
|
||
if (posted_request_num != request_num) { return; }
|
||
updateRaw(new_content);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (posted_request_num != request_num) { return; }
|
||
|
||
console.log(e);
|
||
if (e instanceof TypeError) {
|
||
reply = "Network error";
|
||
} else if (e.name === 'AbortError') {
|
||
reply = "Query was cancelled";
|
||
} else {
|
||
reply = e.toString();
|
||
}
|
||
renderError(reply);
|
||
document.getElementById('hourglass').style.display = 'none';
|
||
document.documentElement.style.setProperty('--progress', '0');
|
||
return;
|
||
}
|
||
|
||
if (is_explain_graph) {
|
||
await renderGraph();
|
||
} else {
|
||
if (extremes['min'] && extremes['max']) renderExtremes();
|
||
if (is_table) transposeTableIfNeeded();
|
||
}
|
||
|
||
document.getElementById('check-mark').style.display = 'inline-block';
|
||
document.getElementById('hourglass').style.display = 'none';
|
||
|
||
if (is_raw) {
|
||
elapsed_ns = 1e6 * (performance.now() - last_query_start);
|
||
}
|
||
|
||
updateProgressText();
|
||
document.documentElement.style.setProperty('--progress', '0');
|
||
document.documentElement.style.setProperty('--background-color-rotate', queryToColor(query));
|
||
|
||
if (posted_request_num != request_num) { return; }
|
||
|
||
/// The query is saved in browser history (in a state JSON object)
|
||
/// as well as in URL fragment identifier.
|
||
if (query != previous_query) {
|
||
const state = {
|
||
query: query,
|
||
format: format,
|
||
ok: response.ok,
|
||
data: reply.length > 100000 ? null : reply, /// Lower than the browser's limit.
|
||
elapsed_ns: elapsed_ns,
|
||
};
|
||
const title = "ClickHouse Query: " + query;
|
||
|
||
let history_url = window.location.pathname + '?user=' + encodeURIComponent(user);
|
||
if (run_immediately) {
|
||
history_url += "&run=1";
|
||
}
|
||
if (server_address != location.origin) {
|
||
/// Save server's address in URL if it's not identical to the address of the play UI.
|
||
history_url += '&url=' + encodeURIComponent(server_address);
|
||
}
|
||
history_url += '#' + window.btoa(query);
|
||
|
||
if (previous_query == '') {
|
||
history.replaceState(state, title, history_url);
|
||
} else {
|
||
history.pushState(state, title, history_url);
|
||
}
|
||
document.title = title;
|
||
previous_query = query;
|
||
}
|
||
}
|
||
|
||
async function restoreFromHistory(state) {
|
||
clear();
|
||
query_area.value = state.query;
|
||
|
||
if (!state.data) return;
|
||
|
||
let is_table = false;
|
||
let is_raw = false;
|
||
let is_chart = false;
|
||
let is_error = false;
|
||
|
||
let format = state.format;
|
||
|
||
if (!state.ok) {
|
||
is_error = true;
|
||
|
||
let has_exception = false;
|
||
for (line of state.data.split('\n')) {
|
||
if (line.startsWith(`{"exception":`)) {
|
||
update(JSON.parse(line));
|
||
has_exception = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!has_exception) {
|
||
renderError(state.data);
|
||
}
|
||
|
||
document.getElementById('hourglass').style.display = 'none';
|
||
document.documentElement.style.setProperty('--progress', '0');
|
||
return;
|
||
} else if (format == default_format) {
|
||
is_table = true;
|
||
let lines = state.data.split('\n');
|
||
let fragment = document.createDocumentFragment();
|
||
|
||
for (line of lines) {
|
||
if (line.length == 0) continue;
|
||
if (!update(JSON.parse(line), fragment)) { break; }
|
||
}
|
||
|
||
if (fragment.hasChildNodes()) {
|
||
document.getElementById('tbody').appendChild(fragment);
|
||
}
|
||
} else if (format == 'JSONCompactColumns') {
|
||
is_chart = true;
|
||
if (posted_request_num != request_num) { return; }
|
||
renderChart(JSON.parse(state.data));
|
||
} else {
|
||
is_raw = true;
|
||
updateRaw(state.data);
|
||
}
|
||
|
||
if (is_explain_graph) {
|
||
await renderGraph();
|
||
} else {
|
||
if (extremes['min'] && extremes['max']) renderExtremes();
|
||
if (is_table) transposeTableIfNeeded();
|
||
}
|
||
|
||
document.getElementById('check-mark').style.display = 'inline-block';
|
||
document.getElementById('hourglass').style.display = 'none';
|
||
|
||
if (is_raw) elapsed_ns = state.elapsed_ns;
|
||
|
||
updateProgressText();
|
||
document.documentElement.style.setProperty('--progress', '0');
|
||
document.documentElement.style.setProperty('--background-color-rotate', queryToColor(query));
|
||
}
|
||
|
||
let query_area = document.getElementById('query');
|
||
|
||
window.onpopstate = function(event) {
|
||
if (!event.state) return;
|
||
restoreFromHistory(event.state);
|
||
};
|
||
|
||
if (window.location.hash) {
|
||
query_area.value = window.atob(window.location.hash.substr(1));
|
||
}
|
||
|
||
let in_flight = false;
|
||
async function post()
|
||
{
|
||
++request_num;
|
||
let query = query_area.value;
|
||
in_flight = true;
|
||
let run = document.getElementById('run');
|
||
run.innerText = 'Stop';
|
||
await postImpl(request_num, query);
|
||
run.blur();
|
||
run.innerText = 'Run';
|
||
in_flight = false;
|
||
}
|
||
|
||
async function cancel()
|
||
{
|
||
controller.abort();
|
||
document.getElementById('run').innerText = 'Run';
|
||
}
|
||
|
||
document.getElementById('run').onclick = function(e)
|
||
{
|
||
if (in_flight) {
|
||
cancel();
|
||
} else {
|
||
post();
|
||
}
|
||
}
|
||
|
||
document.addEventListener('keydown', event => {
|
||
/// Firefox has code 13 for Enter and Chromium has code 10.
|
||
if ((event.metaKey || event.ctrlKey) && (event.keyCode == 13 || event.keyCode == 10)) {
|
||
post();
|
||
}
|
||
});
|
||
|
||
/// Pressing Tab in textarea will increase indentation.
|
||
/// But for accessibility reasons, we will fall back to tab navigation if the user already used Tab for that.
|
||
|
||
let user_prefers_tab_navigation = false;
|
||
|
||
[...document.querySelectorAll('input')].map(elem => {
|
||
elem.onkeydown = (e) => {
|
||
if (e.key == 'Tab') { user_prefers_tab_navigation = true; }
|
||
};
|
||
});
|
||
|
||
query_area.onkeydown = (e) => {
|
||
if (e.key == 'Tab' && !event.shiftKey && !user_prefers_tab_navigation) {
|
||
let elem = e.target;
|
||
let selection_start = elem.selectionStart;
|
||
let selection_end = elem.selectionEnd;
|
||
|
||
elem.value = elem.value.substring(0, elem.selectionStart) + ' ' + elem.value.substring(elem.selectionEnd);
|
||
elem.selectionStart = selection_start + 4;
|
||
elem.selectionEnd = selection_start + 4;
|
||
|
||
e.preventDefault();
|
||
return false;
|
||
} else if (e.key === 'Enter' && !(event.metaKey || event.ctrlKey)) {
|
||
// If the user presses Enter, and the previous line starts with spaces,
|
||
// then we will insert the same number of spaces.
|
||
const elem = e.target;
|
||
if (elem.selectionStart !== elem.selectionEnd) {
|
||
// If there is a selection, then we will not insert spaces.
|
||
return;
|
||
}
|
||
const cursor_pos = elem.selectionStart;
|
||
|
||
const elem_value = elem.value;
|
||
const text_before_cursor = elem_value.substring(0, cursor_pos);
|
||
const text_after_cursor = elem_value.substring(cursor_pos);
|
||
const prev_lines = text_before_cursor.split('\n');
|
||
const prev_line = prev_lines.pop();
|
||
const lead_spaces = prev_line.match(/^\s*/)[0];
|
||
if (!lead_spaces) {
|
||
return;
|
||
}
|
||
|
||
// Add leading spaces to the current line.
|
||
elem.value = text_before_cursor + '\n' + lead_spaces + text_after_cursor;
|
||
elem.selectionStart = cursor_pos + lead_spaces.length + 1;
|
||
elem.selectionEnd = elem.selectionStart;
|
||
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
};
|
||
|
||
function clearElement(id)
|
||
{
|
||
let elem = document.getElementById(id);
|
||
while (elem.firstChild) {
|
||
elem.removeChild(elem.lastChild);
|
||
}
|
||
elem.style.display = 'none';
|
||
}
|
||
|
||
function clear()
|
||
{
|
||
clearElement('data-table');
|
||
clearElement('graph');
|
||
clearElement('chart');
|
||
clearElement('data-unparsed');
|
||
clearElement('error');
|
||
clearElement('progress');
|
||
|
||
document.getElementById('check-mark').display = 'none';
|
||
document.getElementById('hourglass').display = 'none';
|
||
document.getElementById('stats').innerText = '';
|
||
document.getElementById('logo-container').style.display = 'block';
|
||
|
||
extremes = {};
|
||
header = {};
|
||
row_idx = 0;
|
||
elapsed_ns = 0;
|
||
incomplete_result = false;
|
||
is_explain_graph = false;
|
||
explain_graph = '';
|
||
}
|
||
|
||
function formatReadable(number = 0, decimals = 2, units = []) {
|
||
const k = 1000;
|
||
const i = number ? Math.floor(Math.log(number) / Math.log(k)) : 0;
|
||
const unit = units[i];
|
||
const dm = unit ? decimals : 0;
|
||
return Number(number / Math.pow(k, i)).toFixed(dm) + unit;
|
||
}
|
||
|
||
function formatReadableBytes(bytes) {
|
||
const units = [' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB'];
|
||
|
||
return formatReadable(bytes, 2, units);
|
||
}
|
||
|
||
function formatReadableRows(rows) {
|
||
const units = ['', ' thousand', ' million', ' billion', ' trillion', ' quadrillion'];
|
||
|
||
return formatReadable(rows, 2, units);
|
||
}
|
||
|
||
function renderCell(name, value)
|
||
{
|
||
let td = document.createElement('td');
|
||
|
||
let is_null = (value === null);
|
||
let is_link = false;
|
||
|
||
/// Test: SELECT number, toString(number) AS str, number % 2 ? number : NULL AS nullable, range(number) AS arr, CAST((['hello', 'world'], [number, number % 2]) AS Map(String, UInt64)) AS map FROM numbers(10)
|
||
let text;
|
||
if (is_null) {
|
||
text = 'ᴺᵁᴸᴸ';
|
||
} else if (typeof(value) === 'object') {
|
||
text = JSON.stringify(value);
|
||
} else {
|
||
text = value;
|
||
|
||
/// If it looks like URL, create a link. This is for convenience.
|
||
if (typeof(value) == 'string' && value.match(/^https?:\/\/\S+$/)) {
|
||
is_link = true;
|
||
}
|
||
}
|
||
|
||
let node = document.createTextNode(text);
|
||
if (is_link) {
|
||
let link = document.createElement('a');
|
||
link.appendChild(node);
|
||
link.href = text;
|
||
link.setAttribute('target', '_blank');
|
||
node = link;
|
||
}
|
||
|
||
td.className = column_is_number[name] ? 'right' : 'left';
|
||
|
||
if (column_is_number[name]) {
|
||
if (text.length >= 5 && text.match(/^[\d]{5,}$/)) {
|
||
let i = 0;
|
||
td.innerHTML = text.split('').reverse().map(c => {
|
||
if (i > 0 && i % 3 == 0) { c = `<u>${c}<\/u>`; }; ++i; return c;
|
||
}).reverse().join('');
|
||
return td;
|
||
}
|
||
}
|
||
|
||
td.appendChild(node);
|
||
return td;
|
||
}
|
||
|
||
document.getElementById('query').addEventListener('focus', e => {
|
||
current_selected_cell?.classList.remove('td-selected');
|
||
current_selected_cell?.parentElement.classList.remove('tr-selected');
|
||
current_selected_cell = null;
|
||
});
|
||
|
||
document.addEventListener('keydown', e => {
|
||
let cell = current_selected_cell;
|
||
if (!cell) { return; }
|
||
|
||
const row = cell.parentElement;
|
||
const table = row.parentElement;
|
||
let cell_index = cell.cellIndex;
|
||
let row_index = row.rowIndex;
|
||
|
||
switch(e.key) {
|
||
case 'ArrowUp': --row_index; break;
|
||
case 'ArrowDown': ++row_index; break;
|
||
case 'ArrowLeft': --cell_index; break;
|
||
case 'ArrowRight': ++cell_index; break;
|
||
default:
|
||
return;
|
||
}
|
||
|
||
/// The first cell in each row is the row number.
|
||
if (row_index < 0 || cell_index <= 0) { return; }
|
||
|
||
let new_cell = table.rows[row_index]?.cells[cell_index];
|
||
if (!new_cell) { return; }
|
||
|
||
e.preventDefault();
|
||
|
||
cell.classList.remove('td-selected');
|
||
cell.parentElement.classList.remove('tr-selected');
|
||
new_cell.classList.add('td-selected');
|
||
new_cell.parentElement.classList.add('tr-selected');
|
||
new_cell.scrollIntoView({ block: 'nearest' });
|
||
current_selected_cell = new_cell;
|
||
});
|
||
|
||
function transposeTableIfNeeded()
|
||
{
|
||
let table = document.getElementById('data-table');
|
||
if (table.rows.length > 1 || table.rows[0]?.cells.length <= 5) return;
|
||
|
||
let heads = table.tHead.childNodes;
|
||
let values = table.rows[0]?.cells;
|
||
let num_cols = heads.length - 1;
|
||
|
||
let new_tbody = document.createElement('tbody');
|
||
for (let i = 0; i < num_cols; ++i) {
|
||
let tr = document.createElement('tr');
|
||
let th = heads[1];
|
||
th.className = 'right';
|
||
th.style.width = '0';
|
||
tr.appendChild(th);
|
||
if (values) {
|
||
let td = values[1];
|
||
td.classList.remove('right');
|
||
tr.appendChild(td);
|
||
} else if (i == 0) {
|
||
/// If the result is empty, show this fact with a style.
|
||
let td = document.createElement('td');
|
||
td.rowSpan = num_cols;
|
||
td.className = 'empty-result';
|
||
let div = document.createElement('div');
|
||
div.appendChild(document.createTextNode("empty result"));
|
||
div.className = 'empty-result';
|
||
td.appendChild(div);
|
||
tr.appendChild(td);
|
||
}
|
||
new_tbody.appendChild(tr);
|
||
}
|
||
|
||
table.removeChild(table.tHead);
|
||
table.removeChild(table.tBodies[0]);
|
||
table.appendChild(new_tbody);
|
||
}
|
||
|
||
function update(json, fragment) {
|
||
if (json.progress) {
|
||
updateProgress(json.progress);
|
||
return true;
|
||
} else if (json.meta) {
|
||
appendHeader(json.meta);
|
||
return true;
|
||
} else if (json.row) {
|
||
if (is_explain_graph
|
||
|| (row_idx == 0 && Object.keys(json.row).length == 1 && json.row.explain == 'digraph'
|
||
&& query_area.value.match(/^\s*EXPLAIN/i))) {
|
||
is_explain_graph = true;
|
||
explain_graph += json.row.explain + '\n';
|
||
return true;
|
||
} else {
|
||
return appendRow(json.row, fragment);
|
||
}
|
||
} else if (json.min) {
|
||
extremes['min'] = json.min;
|
||
return true;
|
||
} else if (json.max) {
|
||
extremes['max'] = json.max;
|
||
return true;
|
||
} else if (json.exception) {
|
||
renderError(json.exception);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
function updateRaw(data) {
|
||
const fragment = document.createTextNode(data);
|
||
let container = document.getElementById('data-unparsed');
|
||
container.appendChild(fragment);
|
||
container.style.display = 'block';
|
||
}
|
||
|
||
function updateProgressText() {
|
||
let progress_elem = document.getElementById('progress');
|
||
let progress_text = '';
|
||
if (row_idx) {
|
||
progress_text += `${row_idx}${incomplete_result ? '+' : ''} rows in result`;
|
||
if (elapsed_ns) progress_text += ', ';
|
||
}
|
||
if (elapsed_ns) { progress_text += `${(elapsed_ns / 1e9).toFixed(2)} sec.`; }
|
||
progress_elem.innerText = progress_text;
|
||
progress_elem.style.display = 'block';
|
||
}
|
||
|
||
function updateProgress(progress) {
|
||
let stats = document.getElementById('stats');
|
||
let progress_elem = document.getElementById('progress');
|
||
|
||
const rows = progress.read_rows;
|
||
const bytes = progress.read_bytes;
|
||
const total_rows = progress.total_rows_to_read;
|
||
elapsed_ns = progress.elapsed_ns;
|
||
|
||
let formatted_rows = formatReadableRows(rows);
|
||
let formatted_bytes = formatReadableBytes(bytes);
|
||
|
||
const rps = rows * 1e9 / elapsed_ns;
|
||
const bps = bytes * 1e9 / elapsed_ns;
|
||
|
||
let formatted_rps = formatReadableRows(rps) + '/sec';
|
||
let formatted_bps = formatReadableBytes(bps) + '/sec';
|
||
|
||
if (rows >= 1e11) { formatted_rows = `<b>${formatted_rows}<\/b>`; }
|
||
if (bytes >= 1e12) { formatted_bytes = `<b>${formatted_bytes}<\/b>`; }
|
||
if (rps >= 1e10) { formatted_rps = `<b>${formatted_rps}<\/b>`; }
|
||
if (bps >= 1e10) { formatted_bps = `<b>${formatted_bps}<\/b>`; }
|
||
|
||
let text = '';
|
||
|
||
if (total_rows) { text += (100 * Math.min(1.0, rows / total_rows)).toFixed(1) + '%, '; }
|
||
|
||
text += `Read ${formatted_rows} rows, ${formatted_bytes}`;
|
||
|
||
if (rps > 1e6) { text += ` (${formatted_rps}, ${formatted_bps})`; }
|
||
|
||
stats.innerHTML = text;
|
||
|
||
updateProgressText();
|
||
|
||
document.documentElement.style.setProperty('--progress',
|
||
rows && total_rows ? (100 * rows / total_rows) + '%' : '0');
|
||
}
|
||
|
||
function appendHeader(meta) {
|
||
if (meta.length == 0) { return; }
|
||
header = meta;
|
||
|
||
let thead = document.createElement('thead');
|
||
|
||
let th = document.createElement('th');
|
||
th.className = 'row-number';
|
||
th.appendChild(document.createTextNode('№'));
|
||
thead.appendChild(th);
|
||
|
||
meta.forEach(elem => {
|
||
let th = document.createElement('th');
|
||
const name = document.createTextNode(elem.name);
|
||
th.appendChild(name);
|
||
let type_hint = document.createElement('span');
|
||
type_hint.className = 'type-hint';
|
||
type_hint.appendChild(document.createTextNode(elem.type));
|
||
th.appendChild(type_hint);
|
||
thead.appendChild(th);
|
||
|
||
column_is_number[elem.name] = !!elem.type.match(/^(Nullable\()?(U?Int|Decimal|Float)/);
|
||
});
|
||
|
||
let table = document.getElementById('data-table');
|
||
while (table.firstChild) {
|
||
table.removeChild(table.lastChild);
|
||
}
|
||
let tbody = document.createElement('tbody');
|
||
tbody.id = 'tbody';
|
||
table.appendChild(thead);
|
||
table.appendChild(tbody);
|
||
table.style.display = 'table';
|
||
|
||
row_idx = 0;
|
||
}
|
||
|
||
const max_rows = 10_000;
|
||
const max_cells = 100_000;
|
||
|
||
let current_selected_cell = null;
|
||
function appendRow(row, fragment) {
|
||
if (row_idx >= max_rows || (1 + row_idx) * Object.keys(header).length > max_cells) {
|
||
incomplete_result = true;
|
||
return false;
|
||
}
|
||
|
||
++row_idx;
|
||
let tr = document.createElement('tr');
|
||
|
||
let td = document.createElement('td');
|
||
td.className = 'row-number';
|
||
td.appendChild(document.createTextNode(row_idx));
|
||
tr.appendChild(td);
|
||
|
||
let col_idx = 0;
|
||
for (const column of header) {
|
||
const td = renderCell(column.name, row[column.name]);
|
||
++col_idx;
|
||
|
||
td.onclick = () => {
|
||
current_selected_cell?.classList.remove('td-selected');
|
||
current_selected_cell?.parentElement.classList.remove('tr-selected');
|
||
current_selected_cell = td;
|
||
current_selected_cell.classList.add('td-selected')
|
||
current_selected_cell.parentElement.classList.add('tr-selected')
|
||
current_selected_cell.scrollIntoView({ block: 'nearest' });
|
||
};
|
||
|
||
tr.appendChild(td);
|
||
}
|
||
|
||
fragment.appendChild(tr);
|
||
incomplete_result = false;
|
||
return true;
|
||
}
|
||
|
||
function renderExtremes() {
|
||
let col_idx = 0;
|
||
for (const column of header) {
|
||
const name = column.name;
|
||
++col_idx;
|
||
if (column_is_number[name]
|
||
&& extremes.min[name] !== undefined && extremes.max[name] !== undefined
|
||
&& Number(extremes.max[name]) > Number(extremes.min[name])) {
|
||
let table = document.getElementById('data-table');
|
||
for (let row of table.rows) {
|
||
let cell = row.cells[col_idx];
|
||
let value = +cell.innerText;
|
||
if (value > 0) {
|
||
const ratio = 100 * value / extremes.max[name];
|
||
|
||
cell.style.cssText = `
|
||
background-size: 100% 50%;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background: linear-gradient(to right,
|
||
var(--bar-color) 0%, var(--bar-color) ${ratio}%,
|
||
var(--element-background-color) ${ratio}%, var(--element-background-color) 100%)`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderError(message)
|
||
{
|
||
document.getElementById('error').innerText = message ? message : "No response.";
|
||
document.getElementById('error').style.display = 'block';
|
||
document.getElementById('logo-container').style.display = 'none';
|
||
}
|
||
|
||
/// Huge JS libraries should be loaded only if needed.
|
||
function loadJS(src, integrity) {
|
||
return new Promise((resolve, reject) => {
|
||
const script = document.createElement('script');
|
||
script.src = src;
|
||
if (integrity) {
|
||
script.crossOrigin = 'anonymous';
|
||
script.integrity = integrity;
|
||
} else {
|
||
console.warn('no integrity for', src)
|
||
}
|
||
script.addEventListener('load', function() { resolve(true); });
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
let load_dagre_promise;
|
||
function loadDagre() {
|
||
if (load_dagre_promise) { return load_dagre_promise; }
|
||
|
||
load_dagre_promise = Promise.all([
|
||
loadJS('https://dagrejs.github.io/project/dagre/v0.8.5/dagre.min.js',
|
||
'sha384-2IH3T69EIKYC4c+RXZifZRvaH5SRUdacJW7j6HtE5rQbvLhKKdawxq6vpIzJ7j9M'),
|
||
loadJS('https://dagrejs.github.io/project/graphlib-dot/v0.6.4/graphlib-dot.min.js',
|
||
'sha384-Q7oatU+b+y0oTkSoiRH9wTLH6sROySROCILZso/AbMMm9uKeq++r8ujD4l4f+CWj'),
|
||
loadJS('https://dagrejs.github.io/project/dagre-d3/v0.6.4/dagre-d3.min.js',
|
||
'sha384-9N1ty7Yz7VKL3aJbOk+8ParYNW8G5W+MvxEfFL9G7CRYPmkHI9gJqyAfSI/8190W'),
|
||
loadJS('https://cdn.jsdelivr.net/npm/[email protected]',
|
||
'sha384-S+Kf0r6YzKIhKA8d1k2/xtYv+j0xYUU3E7+5YLrcPVab6hBh/r1J6cq90OXhw80u'),
|
||
]);
|
||
|
||
return load_dagre_promise;
|
||
}
|
||
|
||
async function renderGraph()
|
||
{
|
||
clearElement('data-table');
|
||
await loadDagre();
|
||
|
||
/// https://github.com/dagrejs/dagre-d3/issues/131
|
||
const dot = explain_graph.replace(/shape\s*=\s*box/g, 'shape=rect');
|
||
|
||
let graph = graphlibDot.read(dot);
|
||
let render = new dagreD3.render();
|
||
|
||
graph.setGraph({
|
||
nodesep: 20,
|
||
rankdir: 'LR',
|
||
});
|
||
|
||
let svg = document.getElementById('graph');
|
||
svg.style.display = 'block';
|
||
|
||
render(d3.select("#graph"), graph);
|
||
|
||
svg.style.width = graph.graph().width;
|
||
svg.style.height = graph.graph().height;
|
||
}
|
||
|
||
let load_uplot_promise;
|
||
function loadUplot() {
|
||
if (load_uplot_promise) { return load_uplot_promise; }
|
||
load_uplot_promise = loadJS('https://cdn.jsdelivr.net/npm/[email protected]/dist/uPlot.iife.min.js',
|
||
'sha384-TwdJPnTsKP6pnvFZZKda0WJCXpjcHCa7MYHmjrYDu6rsEsb/UnFdoL0phS5ODqTA');
|
||
return load_uplot_promise;
|
||
}
|
||
|
||
let uplot;
|
||
async function renderChart(json)
|
||
{
|
||
await loadUplot();
|
||
clear();
|
||
|
||
let chart = document.getElementById('chart');
|
||
chart.style.display = 'block';
|
||
|
||
let paths = uPlot.paths.stepped({align: 1});
|
||
|
||
const [line_color, fill_color, grid_color, axes_color] = theme == 'light'
|
||
? ["#F8C", "#F8C", "#CCC", "#444"]
|
||
: ["#FF6", "#FF6", "#444", "#CCC"];
|
||
|
||
const opts = {
|
||
width: chart.clientWidth,
|
||
height: chart.clientHeight,
|
||
scales: { x: { time: json[0][0] > 1000000000 && json[0][0] < 2000000000 } },
|
||
axes: [ { stroke: axes_color,
|
||
grid: { width: 1 / devicePixelRatio, stroke: grid_color },
|
||
ticks: { width: 1 / devicePixelRatio, stroke: grid_color } },
|
||
{ stroke: axes_color,
|
||
grid: { width: 1 / devicePixelRatio, stroke: grid_color },
|
||
ticks: { width: 1 / devicePixelRatio, stroke: grid_color } } ],
|
||
series: [ { label: "x" },
|
||
{ label: "y", stroke: line_color, fill: fill_color,
|
||
drawStyle: 0, lineInterpolation: 1, paths } ],
|
||
padding: [ null, null, null, (Math.ceil(Math.log10(Math.max(...json[1]))) + Math.floor(Math.log10(Math.max(...json[1])) / 3)) * 6 ],
|
||
};
|
||
|
||
uplot = new uPlot(opts, json, chart);
|
||
}
|
||
|
||
function resizeChart() {
|
||
if (uplot) {
|
||
let chart = document.getElementById('chart');
|
||
uplot.setSize({ width: chart.clientWidth, height: chart.clientHeight });
|
||
}
|
||
}
|
||
|
||
function redrawChart() {
|
||
if (uplot && document.getElementById('chart').style.display == 'block') {
|
||
renderChart(uplot.data);
|
||
}
|
||
}
|
||
|
||
new ResizeObserver(resizeChart).observe(document.getElementById('chart'));
|
||
|
||
/// First we check if theme is set via the 'theme' GET parameter, if not, we check localStorage, otherwise we check OS preference.
|
||
let theme = current_url.searchParams.get('theme');
|
||
if (['dark', 'light'].indexOf(theme) === -1) {
|
||
theme = window.localStorage.getItem('theme');
|
||
}
|
||
if (!theme) {
|
||
theme = 'light';
|
||
}
|
||
|
||
function setColorTheme(new_theme, update_preference) {
|
||
theme = new_theme;
|
||
if (update_preference) {
|
||
window.localStorage.setItem('theme', theme);
|
||
}
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
redrawChart();
|
||
}
|
||
|
||
if (theme) {
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
} else {
|
||
/// Obtain system-level user preference
|
||
const media_query_list = window.matchMedia('(prefers-color-scheme: dark)');
|
||
if (media_query_list.matches) {
|
||
setColorTheme('dark');
|
||
}
|
||
|
||
/// There is a rumor that on some computers, the theme is changing automatically on day/night.
|
||
media_query_list.addEventListener('change', function(e) {
|
||
setColorTheme(e.matches ? 'dark' : 'light');
|
||
});
|
||
}
|
||
|
||
if (run_immediately) {
|
||
post();
|
||
}
|
||
|
||
document.getElementById('toggle-light').onclick = function() {
|
||
setColorTheme('light', true);
|
||
}
|
||
|
||
document.getElementById('toggle-dark').onclick = function() {
|
||
setColorTheme('dark', true);
|
||
}
|
||
</script>
|
||
</html>
|