mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
2694 lines
104 KiB
HTML
2694 lines
104 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:
|
||
- display totals
|
||
- 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. */
|
||
--bar-color-negative: #FEE;
|
||
--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%;
|
||
|
||
--selection-color: #FFF;
|
||
--selection-background-color: #456;
|
||
}
|
||
|
||
[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;
|
||
--bar-color-negative: #311;
|
||
--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;
|
||
|
||
--selection-color: #000;
|
||
--selection-background-color: #FF8;
|
||
}
|
||
|
||
::selection {
|
||
color: var(--selection-color);
|
||
background-color: var(--selection-background-color);
|
||
}
|
||
|
||
*
|
||
{
|
||
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;
|
||
scrollbar-color: var(--border-color) var(--element-background-color);
|
||
}
|
||
|
||
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);
|
||
display: grid;
|
||
grid-template-rows: auto 1fr auto;
|
||
grid-template-areas: "databases-icon" "contents" "github-icon";
|
||
}
|
||
|
||
#main
|
||
{
|
||
overflow: auto;
|
||
grid-area: main;
|
||
padding: 0.5rem;
|
||
position: relative; /* for stacking context */
|
||
}
|
||
|
||
.menu-icon
|
||
{
|
||
width: 2.25em;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
fill: var(--menu-toggle-color);
|
||
}
|
||
|
||
.menu-icon:hover
|
||
{
|
||
transform: translate(1px, 1px);
|
||
}
|
||
|
||
#databases-icon
|
||
{
|
||
grid-area: databases-icon;
|
||
}
|
||
|
||
#databases
|
||
{
|
||
grid-area: contents;
|
||
}
|
||
|
||
#github-icon
|
||
{
|
||
grid-area: github-icon;
|
||
}
|
||
|
||
#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);
|
||
&:hover {
|
||
text-decoration: 1px underline dotted 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);
|
||
}
|
||
|
||
.no-tables
|
||
{
|
||
color: var(--misc-text-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: 999;
|
||
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;
|
||
}
|
||
|
||
.table-addendum-elem
|
||
{
|
||
display: block;
|
||
cursor: pointer;
|
||
&:hover {
|
||
text-decoration: 1px underline dotted var(--element-background-color);
|
||
}
|
||
}
|
||
|
||
#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. */
|
||
resize: none; /* Disable default browser resizer */
|
||
}
|
||
|
||
#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: 0.5rem;
|
||
display: grid;
|
||
place-items: center;
|
||
gap: 1rem;
|
||
grid-template-columns: auto auto auto 1fr auto auto auto;
|
||
grid-template-areas: "run hint hourglass progress-bar download 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, #download-div:hover, #download-div: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);
|
||
}
|
||
|
||
#download-div
|
||
{
|
||
grid-area: download;
|
||
display: none;
|
||
user-select: none;
|
||
color: var(--button-text-color);
|
||
background-color: var(--button-color);
|
||
padding: 0.25rem 0.65rem;
|
||
cursor: pointer;
|
||
position: relative;
|
||
}
|
||
|
||
#download
|
||
{
|
||
width: 1em;
|
||
height: 1em;
|
||
}
|
||
|
||
#download-dropdown
|
||
{
|
||
display: none;
|
||
position: absolute;
|
||
background-color: var(--button-color);
|
||
border: 1px solid var(--button-text-color);
|
||
padding: 0.5rem;
|
||
z-index: 999;
|
||
box-shadow: 0 4px 6px var(--shadow-color);
|
||
}
|
||
|
||
#download-dropdown.show
|
||
{
|
||
display: block;
|
||
}
|
||
|
||
.download-option
|
||
{
|
||
margin-bottom: 0.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.download-option label
|
||
{
|
||
color: var(--button-text-color);
|
||
white-space: nowrap;
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.download-option select,
|
||
#download-format-other
|
||
{
|
||
flex: 1;
|
||
padding: 0.25rem;
|
||
background-color: var(--element-background-color);
|
||
color: var(--text-color);
|
||
border: 1px solid var(--shadow-color);
|
||
|
||
&:focus {
|
||
outline: none;
|
||
}
|
||
}
|
||
|
||
#download-format-other
|
||
{
|
||
display: none;
|
||
margin-top: 0.25rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
#download-format-other.show
|
||
{
|
||
display: block;
|
||
}
|
||
|
||
#download-button
|
||
{
|
||
width: 100%;
|
||
margin-top: 0rem;
|
||
padding: 0.5rem 0.5rem;
|
||
background-color: var(--element-background-color);
|
||
color: var(--text-color);
|
||
border: none;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
|
||
&:hover {
|
||
background-color: var(--button-active-color);
|
||
color: var(--button-active-text-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;
|
||
user-select: none;
|
||
}
|
||
|
||
[data-theme="light"] #toggle-light, [data-theme="dark"] #toggle-dark
|
||
{
|
||
display: none;
|
||
}
|
||
|
||
#data_div
|
||
{
|
||
margin-top: 1rem;
|
||
background-color: var(--element-background-color);
|
||
}
|
||
|
||
#data_div.fixed
|
||
{
|
||
width: fit-content;
|
||
}
|
||
|
||
#data-table
|
||
{
|
||
width: 100%;
|
||
border-spacing: 0;
|
||
background-color: var(--element-background-color);
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
#data-table.fixed
|
||
{
|
||
table-layout: fixed;
|
||
width: var(--table-width);
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
|
||
th .name {
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.type-hint
|
||
{
|
||
display: none;
|
||
position: absolute;
|
||
user-select: none;
|
||
left: 0;
|
||
top: -1px;
|
||
transform: translateY(-100%);
|
||
font-size: 75%;
|
||
font-weight: normal;
|
||
color: var(--misc-text-color);
|
||
background: var(--element-background-color);
|
||
max-height: 5em;
|
||
white-space: pre;
|
||
}
|
||
|
||
th:hover .type-hint
|
||
{
|
||
display: inline-block;
|
||
}
|
||
|
||
.resizer {
|
||
position: absolute;
|
||
top: 0;
|
||
right: -0.3rem;
|
||
width: 0.5rem;
|
||
cursor: col-resize;
|
||
user-select: none;
|
||
height: 100%;
|
||
}
|
||
|
||
.resizer:hover, .resizer-dragging
|
||
{
|
||
border-radius: 0.25rem;
|
||
background-color: var(--border-color-selected);
|
||
}
|
||
|
||
.textarea-resizer {
|
||
position: absolute;
|
||
bottom: -0.3rem;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 0.5rem;
|
||
cursor: row-resize;
|
||
user-select: none;
|
||
}
|
||
|
||
.textarea-resizer:hover, .textarea-resizer-dragging
|
||
{
|
||
border-radius: 0.25rem;
|
||
background-color: var(--border-color);
|
||
}
|
||
|
||
#query_div {
|
||
position: relative;
|
||
}
|
||
|
||
/* 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);
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
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;
|
||
position: sticky;
|
||
left: 0;
|
||
}
|
||
|
||
#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);
|
||
}
|
||
|
||
/* Prevents a different color when Chromium auto-fills inputs from the list. */
|
||
input:-webkit-autofill,
|
||
input:-webkit-autofill:hover,
|
||
input:-webkit-autofill:focus,
|
||
input:-webkit-autofill:active
|
||
{
|
||
transition: background-color 600000s 0s, color 600000s 0s;
|
||
}
|
||
|
||
/* 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 {font-family: Liberation Mono, DejaVu Sans Mono, MonoLisa, Consolas, monospace; border-spacing: 0; border-collapse: collapse;}
|
||
.u-inline {display: block;}
|
||
.u-inline * {display: inline-block;}
|
||
.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 id="databases-icon" 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">
|
||
<form id="controls">
|
||
<div id="inputs">
|
||
<input name="url" spellcheck="false" class="monospace shadow" id="url" type="url" value="" placeholder="url" /><span id="url_status"><span id="server_info"></span>●</span><input name="user" spellcheck="false" class="monospace shadow" id="user" type="text" value="default" placeholder="user" /><input name="password" class="monospace shadow" id="password" type="password" placeholder="password" />
|
||
</div>
|
||
<div id="query_div"><textarea autofocus spellcheck="false" autocorrect="false" autocomplete="false" data-gramm="false" class="monospace shadow" id="query"></textarea></div>
|
||
<div id="run_div">
|
||
<button type="submit" 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="download-div" class="shadow">
|
||
<svg id="download" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||
<polyline points="7 10 12 15 17 10"/>
|
||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||
</svg>
|
||
</div>
|
||
<div id="stats"></div>
|
||
<div id="theme"><span id="toggle-dark">🌘</span><span id="toggle-light">☀️</span></div>
|
||
</div>
|
||
</form>
|
||
<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 id="download-dropdown">
|
||
<div>
|
||
<div class="download-option">
|
||
<label for="download-format">Format:</label>
|
||
<select id="download-format">
|
||
<option value="CSV">CSV</option>
|
||
<option value="TSV">TSV</option>
|
||
<option value="JSON">JSON</option>
|
||
<option value="NDJSON" selected>JSONLines</option>
|
||
<option value="Parquet">Parquet</option>
|
||
<option value="Markdown">Markdown</option>
|
||
<option value="other">Other...</option>
|
||
</select>
|
||
</div>
|
||
<input type="text" id="download-format-other" placeholder="Enter format name (e.g., Arrow, Avro)" />
|
||
</div>
|
||
<button id="download-button">Download</button>
|
||
</div>
|
||
</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');
|
||
|
||
if (url_elem.value == '') {
|
||
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;
|
||
} else {
|
||
url_elem.value = 'http://localhost:8123/';
|
||
}
|
||
}
|
||
|
||
/// 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);
|
||
|
||
// Use natural order for database names
|
||
json.data.sort((a, b) => {
|
||
// Maintain original order for default, system, information_schema
|
||
const getPriority = (name) => {
|
||
switch (name) {
|
||
case 'default': return 0;
|
||
case 'system': return 2;
|
||
case 'information_schema': return 3
|
||
default: return 1;
|
||
}
|
||
}
|
||
|
||
return getPriority(a.database) - getPriority(b.database)
|
||
|| a.database.localeCompare(b.database, undefined, { numeric: true });
|
||
});
|
||
|
||
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));
|
||
|
||
// Use natural order for table names
|
||
json.data.sort((a, b) => a.table.localeCompare(b.table, undefined, { numeric: true }));
|
||
|
||
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';
|
||
|
||
const set_query_or_run = (e, value) => {
|
||
if (query_area.value == value) {
|
||
/// Double click will run the query.
|
||
post();
|
||
} else {
|
||
query_area.value = value;
|
||
}
|
||
e.stopPropagation();
|
||
}
|
||
|
||
const show_table_properties = e => set_query_or_run(e, `SELECT * FROM system.tables WHERE database = '${database}' AND table = '${elem.table}'`);
|
||
|
||
if (+elem.total_bytes) {
|
||
let bytes_elem = document.createElement('span');
|
||
bytes_elem.className = 'table-addendum-elem';
|
||
bytes_elem.textContent = formatReadableBytes(elem.total_bytes) + '\n';
|
||
bytes_elem.addEventListener('click', show_table_properties);
|
||
table_addendum.append(bytes_elem);
|
||
}
|
||
if (+elem.total_rows) {
|
||
let rows_elem = document.createElement('span');
|
||
rows_elem.className = 'table-addendum-elem';
|
||
rows_elem.textContent = formatReadableRows(elem.total_rows) + ' rows\n';
|
||
rows_elem.addEventListener('click', show_table_properties);
|
||
table_addendum.append(rows_elem);
|
||
}
|
||
let engine_elem = document.createElement('span');
|
||
engine_elem.textContent = elem.engine;
|
||
engine_elem.className = 'table-addendum-elem';
|
||
engine_elem.addEventListener('click', e => set_query_or_run(e, `SHOW TABLE "${database}"."${elem.table}"`));
|
||
table_addendum.append(engine_elem);
|
||
|
||
table.appendChild(table_link);
|
||
table.appendChild(table_addendum);
|
||
table.addEventListener('click', e => set_query_or_run(e, `SELECT * FROM "${database}"."${elem.table}" LIMIT 100`));
|
||
tables_elem.appendChild(table);
|
||
}
|
||
|
||
if (json.data.length == 0) {
|
||
let table = document.createElement('div');
|
||
table.className = 'no-tables';
|
||
table.append('no tables');
|
||
tables_elem.append(table);
|
||
}
|
||
}
|
||
|
||
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'); });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function resetCredentialsStatus() {
|
||
[user_elem, password_elem].forEach(e => { e.classList.remove('ok'); e.classList.remove('fail'); });
|
||
}
|
||
|
||
user_elem.addEventListener('input', resetCredentialsStatus);
|
||
password_elem.addEventListener('input', resetCredentialsStatus);
|
||
|
||
/// The password is usually copy-pasted.
|
||
/// To not worry about extracting the clipboard data, but use the value instead,
|
||
/// and not worry too much about input/paste race, add the timeout.
|
||
user_elem.addEventListener('paste', () => setTimeout(checkCredentials, 100));
|
||
password_elem.addEventListener('paste', () => setTimeout(checkCredentials, 100));
|
||
|
||
|
||
function queryToColor(str) {
|
||
let hash = 2166136261;
|
||
for (let i = 0; i < str.length; i++) {
|
||
hash ^= str.charCodeAt(i);
|
||
hash = Math.imul(hash, 16777619);
|
||
}
|
||
|
||
/// Limited range to avoid violet colors.
|
||
return 50 + ((hash >>> 0) % 200);
|
||
}
|
||
|
||
function toBase64(str) {
|
||
const encoder = new TextEncoder();
|
||
const bytes = encoder.encode(str);
|
||
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
||
return btoa(binary);
|
||
}
|
||
|
||
function fromBase64(base64) {
|
||
const binary = atob(base64);
|
||
const bytes = new Uint8Array(binary.length);
|
||
for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); }
|
||
const decoder = new TextDecoder();
|
||
return decoder.decode(bytes);
|
||
}
|
||
|
||
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';
|
||
/// We will put the result in the query cache, so it can be used when download is initiated later.
|
||
/// But we don't use cached results, so the results that we get here, are always fresh.
|
||
url += '&use_query_cache=1&enable_reads_from_query_cache=0&enable_writes_to_query_cache=1&query_cache_ttl=600&query_cache_nondeterministic_function_handling=save';
|
||
|
||
/// 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('download-div').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.getElementById('download-div').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.getElementById('download-div').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';
|
||
document.getElementById('download-div').style.display = 'block';
|
||
last_query_for_download = query;
|
||
|
||
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 += '#' + toBase64(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');
|
||
|
||
appendTextareaResizer();
|
||
|
||
window.onpopstate = function(event) {
|
||
if (!event.state) return;
|
||
restoreFromHistory(event.state);
|
||
};
|
||
|
||
if (window.location.hash) {
|
||
query_area.value = fromBase64(window.location.hash.substr(1));
|
||
}
|
||
|
||
let in_flight = false;
|
||
async function post()
|
||
{
|
||
++request_num;
|
||
document.body.scrollTo(0, 0);
|
||
let query = await getQueryUnderCursor();
|
||
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('controls').addEventListener('submit', e =>
|
||
{
|
||
e.preventDefault();
|
||
if (in_flight) {
|
||
cancel();
|
||
} else {
|
||
post();
|
||
|
||
if (password_elem.value) {
|
||
const cred = new PasswordCredential({
|
||
id: user_elem.value,
|
||
password: password_elem.value,
|
||
name: url_elem.value,
|
||
});
|
||
navigator.credentials.store(cred);
|
||
}
|
||
}
|
||
});
|
||
|
||
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)) {
|
||
document.getElementById('controls').requestSubmit();
|
||
}
|
||
});
|
||
|
||
/// 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) {
|
||
document.execCommand('insertText', false, ' ');
|
||
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;
|
||
} else if ((e.ctrlKey || e.metaKey) && e.key === '/' && !e.shiftKey) {
|
||
// Comment/uncomment selected lines on Ctrl+/ or Cmd+/
|
||
const elem = e.target;
|
||
const start = elem.selectionStart;
|
||
const end = elem.selectionEnd;
|
||
const value = elem.value;
|
||
|
||
let lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
||
let lineEnd = value.indexOf('\n', end);
|
||
if (lineEnd === -1) lineEnd = value.length;
|
||
|
||
let selectedText = value.substring(lineStart, lineEnd);
|
||
let lines = selectedText.split('\n');
|
||
|
||
// Comment out each line, or uncomment if all lines are already commented
|
||
const allCommented = lines.every(line => /^\s*--/.test(line));
|
||
|
||
// If all lines are commented out, uncomment them
|
||
if (allCommented) {
|
||
lines = lines.map(line => line.replace(/^(\s*)-- ?/, '$1'));
|
||
} else {
|
||
lines = lines.map(line => {
|
||
if (/^\s*$/.test(line)) {
|
||
return line;
|
||
}
|
||
return line.replace(/^(\s*)/, '$1-- ');
|
||
});
|
||
}
|
||
|
||
const replacedText = lines.join('\n');
|
||
elem.value = value.slice(0, lineStart) + replacedText + value.slice(lineEnd);
|
||
|
||
// Adjust selection to keep the user in flow and enable quick reversal
|
||
elem.selectionStart = lineStart;
|
||
elem.selectionEnd = lineStart + replacedText.length;
|
||
|
||
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('data-table').classList.remove('fixed');
|
||
document.getElementById('data_div').classList.remove('fixed');
|
||
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 (Object.keys(header).length == 0 || 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';
|
||
th.removeChild(th.querySelector('.resizer'))
|
||
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 changeTableLayout() {
|
||
const table = document.getElementById('data-table');
|
||
const headers = table.querySelectorAll('th');
|
||
|
||
document.getElementById('data-table').style.setProperty('--table-width', `${table.offsetWidth}px`);
|
||
|
||
columnWidths = [];
|
||
headers.forEach(header => {
|
||
columnWidths.push(header.offsetWidth);
|
||
});
|
||
|
||
table.classList.add('fixed');
|
||
document.getElementById('data_div').classList.add('fixed');
|
||
|
||
headers.forEach((header, index) => {
|
||
header.style.width = `${columnWidths[index]}px`;
|
||
});
|
||
}
|
||
|
||
|
||
function appendHeaderResizer(header) {
|
||
let drag_state = {
|
||
elem: null,
|
||
is_dragging: true,
|
||
offset_x: null,
|
||
offset_width: null
|
||
};
|
||
|
||
const start = (e) => {
|
||
if (e.button !== 0) { return; }
|
||
|
||
changeTableLayout();
|
||
drag_state.offset_x = e.clientX;
|
||
drag_state.offset_width = header.offsetWidth;
|
||
drag_state.is_dragging = true;
|
||
drag_state.elem = e.target;
|
||
drag_state.elem.classList.add('resizer-dragging');
|
||
|
||
document.addEventListener('pointermove', move);
|
||
document.addEventListener('pointerup', stop);
|
||
document.addEventListener('pointercancel', stop);
|
||
}
|
||
|
||
const move = (e) => {
|
||
if (!drag_state.is_dragging) { return; }
|
||
|
||
const dx = e.clientX - drag_state.offset_x;
|
||
header.style.width = `${drag_state.offset_width + dx}px`;
|
||
if(dx < 0) {
|
||
const table = document.getElementById('data-table');
|
||
table.style.setProperty('--table-width', `${table.offsetWidth + dx}px`)
|
||
};
|
||
}
|
||
|
||
const stop = (e) => {
|
||
drag_state.is_dragging = false;
|
||
drag_state.elem.classList.remove('resizer-dragging');
|
||
|
||
document.removeEventListener('pointermove', move);
|
||
document.removeEventListener('pointerup', stop);
|
||
document.removeEventListener('pointercancel', stop);
|
||
}
|
||
|
||
const resizer = document.createElement('div');
|
||
resizer.className = 'resizer';
|
||
resizer.addEventListener('pointerdown', start);
|
||
resizer.addEventListener('touchstart', (e) => e.preventDefault());
|
||
|
||
header.appendChild(resizer);
|
||
}
|
||
|
||
|
||
function appendTextareaResizer() {
|
||
const queryDiv = document.getElementById('query_div');
|
||
const textarea = document.getElementById('query');
|
||
|
||
let drag_state = {
|
||
elem: null,
|
||
is_dragging: false,
|
||
offset_y: null,
|
||
offset_height: null
|
||
};
|
||
|
||
const start = (e) => {
|
||
if (e.button !== 0) { return; }
|
||
|
||
drag_state.offset_y = e.clientY;
|
||
drag_state.offset_height = textarea.offsetHeight;
|
||
drag_state.is_dragging = true;
|
||
drag_state.elem = e.target;
|
||
drag_state.elem.classList.add('textarea-resizer-dragging');
|
||
|
||
document.addEventListener('pointermove', move);
|
||
document.addEventListener('pointerup', stop);
|
||
document.addEventListener('pointercancel', stop);
|
||
}
|
||
|
||
const move = (e) => {
|
||
if (!drag_state.is_dragging) { return; }
|
||
|
||
const dy = e.clientY - drag_state.offset_y;
|
||
const newHeight = drag_state.offset_height + dy;
|
||
|
||
textarea.style.height = `${newHeight}px`;
|
||
}
|
||
|
||
const stop = (e) => {
|
||
drag_state.is_dragging = false;
|
||
drag_state.elem.classList.remove('textarea-resizer-dragging');
|
||
|
||
document.removeEventListener('pointermove', move);
|
||
document.removeEventListener('pointerup', stop);
|
||
document.removeEventListener('pointercancel', stop);
|
||
}
|
||
|
||
const resizer = document.createElement('div');
|
||
resizer.className = 'textarea-resizer';
|
||
resizer.addEventListener('pointerdown', start);
|
||
resizer.addEventListener('touchstart', (e) => e.preventDefault());
|
||
|
||
queryDiv.appendChild(resizer);
|
||
}
|
||
|
||
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);
|
||
|
||
/// Assign z-index in the reverse order.
|
||
/// This is needed to make sure the contents that overflow to the right (column resizer) is visible.
|
||
let zIndex = meta.length;
|
||
|
||
meta.forEach(elem => {
|
||
let th = document.createElement('th');
|
||
const name = document.createTextNode(elem.name);
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.className = 'name';
|
||
nameSpan.appendChild(name);
|
||
th.appendChild(nameSpan);
|
||
let type_hint = document.createElement('span');
|
||
type_hint.className = 'type-hint';
|
||
type_hint.appendChild(document.createTextNode(elem.type));
|
||
th.appendChild(type_hint);
|
||
appendHeaderResizer(th);
|
||
th.style.zIndex = zIndex;
|
||
--zIndex;
|
||
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 = 1_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');
|
||
|
||
const double_sided = extremes.min[name] < 0;
|
||
const ratio_positive = extremes.max[name] / (extremes.max[name] - extremes.min[name]);
|
||
const ratio_negative = 1 - ratio_positive;
|
||
|
||
for (let row of table.rows) {
|
||
let cell = row.cells[col_idx];
|
||
let value = +cell.innerText;
|
||
|
||
if (!double_sided) {
|
||
if (value > 0) {
|
||
const ratio = 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) ${100 * ratio}%,
|
||
var(--element-background-color) ${100 * ratio}%, var(--element-background-color) 100%)`;
|
||
}
|
||
} else {
|
||
if (value > 0) {
|
||
const ratio = value / extremes.max[name];
|
||
|
||
cell.style.cssText = `
|
||
background-size: 100% 50%;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background: linear-gradient(to right,
|
||
var(--element-background-color) 0%, var(--element-background-color) ${100 * ratio_negative}%,
|
||
var(--bar-color) ${100 * ratio_negative}%, var(--bar-color) ${100 * (ratio_negative + ratio * ratio_positive)}%,
|
||
var(--element-background-color) ${100 * (ratio_negative + ratio * ratio_positive)}%, var(--element-background-color) 100%)`;
|
||
} else if (value < 0) {
|
||
const ratio = value / extremes.min[name];
|
||
|
||
cell.style.cssText = `
|
||
background-size: 100% 50%;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background: linear-gradient(to right,
|
||
var(--element-background-color) 0%, var(--element-background-color) ${100 * (1 - ratio) * ratio_negative}%,
|
||
var(--bar-color-negative) ${100 * (1 - ratio) * ratio_negative}%, var(--bar-color-negative) ${100 * ratio_negative}%,
|
||
var(--element-background-color) ${100 * ratio_negative}%, 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;
|
||
}
|
||
|
||
function legendAsTooltipPlugin({ className, style = {} } = {}) {
|
||
let legendEl;
|
||
let multiline;
|
||
|
||
function init(u, opts) {
|
||
legendEl = u.root.querySelector(".u-legend");
|
||
legendEl.classList.remove("u-inline");
|
||
className && legendEl.classList.add(className);
|
||
|
||
uPlot.assign(legendEl.style, {
|
||
textAlign: "right",
|
||
pointerEvents: "none",
|
||
display: "none",
|
||
position: "absolute",
|
||
left: 0,
|
||
top: 0,
|
||
zIndex: 100,
|
||
boxShadow: "2px 2px 10px rgba(0, 0, 0, 0.1)",
|
||
...style
|
||
});
|
||
|
||
const nodes = legendEl.querySelectorAll("th");
|
||
for (let i = 0; i < nodes.length; i++)
|
||
nodes[i]._order = i;
|
||
|
||
if (opts.series.length == 2) {
|
||
multiline = false;
|
||
for (let i = 0; i < nodes.length; i++)
|
||
nodes[i].style.display = "none";
|
||
} else {
|
||
multiline = true;
|
||
legendEl.querySelector("th").remove();
|
||
legendEl.querySelector("td").setAttribute('colspan', '2');
|
||
legendEl.querySelector("td").style.textAlign = 'center';
|
||
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 = ". . .";
|
||
}
|
||
|
||
const overEl = u.over;
|
||
overEl.style.overflow = "visible";
|
||
|
||
overEl.appendChild(legendEl);
|
||
|
||
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
|
||
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
|
||
}
|
||
|
||
function nodeListToArray(nodeList) {
|
||
return Array.prototype.slice.call(nodeList);
|
||
}
|
||
|
||
function update(u) {
|
||
let { left, top } = u.cursor;
|
||
/// 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);
|
||
if (top >= legendEl.clientHeight) {
|
||
top -= legendEl.clientHeight;
|
||
}
|
||
legendEl.style.transform = "translate(" + left + "px, " + top + "px)";
|
||
|
||
if (multiline) {
|
||
let nodes = nodeListToArray(legendEl.querySelectorAll("tr"));
|
||
let header = nodes.shift();
|
||
let footer = nodes.pop();
|
||
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);
|
||
nodes.forEach(function (node) { node.parentNode.appendChild(node); });
|
||
for (let i = 0; i < nodes.length; i++) {
|
||
nodes[i].style.display = i < showLimit ? null : "none";
|
||
}
|
||
footer.parentNode.appendChild(footer);
|
||
footer.style.display = nodes.length > showLimit ? null : "none";
|
||
}
|
||
}
|
||
|
||
return {
|
||
hooks: {
|
||
init: init,
|
||
setCursor: update,
|
||
}
|
||
};
|
||
}
|
||
|
||
let uplot;
|
||
async function renderChart(json)
|
||
{
|
||
await loadUplot();
|
||
clear();
|
||
|
||
let chart = document.getElementById('chart');
|
||
chart.style.display = 'block';
|
||
|
||
let paths = json[0].length < chart.clientWidth / 5 ? uPlot.paths.bars() : uPlot.paths.linear();
|
||
|
||
const [line_color, fill_color, grid_color, axes_color] = theme == 'light'
|
||
? ["rgba(255, 0, 200, 100%)", "rgba(255, 128, 200, 50%)", "#CCC", "#444"]
|
||
: ["rgba(255, 255, 0, 100%)", "rgba(255, 255, 100, 50%)", "#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, paths, points: { show: false } } ],
|
||
padding: [ null, null, null, (Math.ceil(Math.log10(Math.max(...json[1]))) + Math.floor(Math.log10(Math.max(...json[1])) / 3)) * 6 ],
|
||
plugins: [ legendAsTooltipPlugin() ],
|
||
};
|
||
|
||
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'));
|
||
|
||
let lexer_module;
|
||
async function loadLexer() {
|
||
// base64 -w0 build/src/Parsers/Lexer.wasm
|
||
const lexer_base64 = "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A08NAkEBIQgDQCACLQAAIgVBMGshAQJAAkAgBwRAIAFB/wFxQQpJIAVBwQBrQQZJcg0CIAVB4QBrQQZPDQEMAgsgAUH/AXFBCkkNAQsgCCAFQd8AR3JBAXENBAJAIAJBAWoiCSADTw0AIAktAAAiBUEwayEBIAcEQCABQf8BcUEKSSAFQcEAa0EGSXINASAFQeEAa0EGTw0HDAELIAFB/wFxQQlLDQYLIAMgCUYNBAsgBiACQQFqIgI2AgBBACEIIAIgA0kNAAsMAgsgBiACQQFqIgI2AgBBACEIIAIgA0cNAAsLIAJBAWoiASADTw0AIAItAAAhBQJAIAcEQCAFQSByQfAARw0CDAELIAVBIHJB5QBHDQELIAYgATYCAAJAIAJBAmogA08NAAJAIAEtAABBK2sOAwABAAELIAYgAUEBaiIBNgIAIAEgA08NAgtBASECA0AgAS0AACIFQTBrQf8BcUEKTwRAIAIgBUHfAEdyQQFxDQMgAUEBaiICIANPDQMgAi0AAEEwa0H/AXFBCUsNAwsgBiABQQFqIgE2AgBBACECIAEgA0cNAAsMAQsgAiEBCwJ/AkACQCABIANPDQAgAS0AACICQd8ARiACQTBrQf8BcUEKSXJFIAJB3wFxQcEAa0H/AXFBGUtxDQAgAUEBaiEFA0AgBiAFIgE2AgAgASADTw0CIAFBAWohBSABLQAAIgdBMGshAiAHQd8ARiACQf8BcUEKSXIgB0HfAXFBwQBrQf8BcUEaSXINAAsgAUEBawwCCyAAIAE2AggMCgsgAUEBawshAwJAIAEgBE0NACAEQQFrIQUDQCAFQQFqIgUtAAAiAkEkRiACQd8ARnIgAkEwa0H/AXFBCklyRSACQd8BcUHBAGtB/wFxQRpPcUUEQCADIAVLDQEMAgsLIAAgATYCCCAAIAQ2AgQgAEEvOgAADwsMCQsgBiAEQQJqIgE2AgACQCABIANGDQADQCABLQAAQQpGDQEgAUEBaiIBIANHDQALIAMhAQsLIAAgATYCCCAAIAQ2AgQgAEEBOgAACyAGIAE2AgAPCyAAIAE2AgggACAENgIEIABBADoAAA8LIAAgBEEBaiIANgIIDAILIAAgBEECaiIANgIIDAELIAAgATYCCCAAIAQ2AgQgAEEoOgAADwsgBiAANgIADwsgACAENgIEIABBAzoAAA8LIAAgATYCCCAAIAQ2AgQgAEECOgAACyUAIABBADoAECAAIAM2AgwgACACNgIIIAAgATYCBCAAIAE2AgALNgEBfyMAQRBrIgMkACADQQRqIAAQASABIAMoAgg2AgAgAiADKAIMNgIAIAMtAAQgA0EQaiQACwcAIABBAUsLBwAgAEEnSwsHACAAQSdGCwsIAQBBgAgLARQAeQlwcm9kdWNlcnMBDHByb2Nlc3NlZC1ieQEFY2xhbmdZMjAuMC4wZ2l0IChnaXRAZ2l0aHViLmNvbTpsbHZtL2xsdm0tcHJvamVjdC5naXQgNmNiYzY0ZWQ5MjJjYzY5YmMyOTJkMzk0YmE1YzY4MWZhMzA5ZjQwNCkAaw90YXJnZXRfZmVhdHVyZXMGKw9tdXRhYmxlLWdsb2JhbHMrE25vbnRyYXBwaW5nLWZwdG9pbnQrC2J1bGstbWVtb3J5KwhzaWduLWV4dCsPcmVmZXJlbmNlLXR5cGVzKwptdWx0aXZhbHVl";
|
||
|
||
if (!lexer_module) {
|
||
const binary = atob(lexer_base64);
|
||
const bytes = new Uint8Array(binary.length);
|
||
|
||
for (let i = 0; i < binary.length; i++) {
|
||
bytes[i] = binary.charCodeAt(i);
|
||
}
|
||
|
||
lexer_module = await WebAssembly.instantiate(bytes);
|
||
}
|
||
}
|
||
|
||
async function tokenize(query) {
|
||
await loadLexer();
|
||
|
||
let exports = lexer_module.instance.exports;
|
||
let buffer = exports.memory.buffer;
|
||
let memory_offset = 0;
|
||
|
||
/// Allocate memory for the lexer object
|
||
const lexer = new Uint8Array(buffer, memory_offset, exports.clickhouse_lexer_size);
|
||
memory_offset += exports.clickhouse_lexer_size;
|
||
|
||
/// Allocate the query
|
||
const bytes = new TextEncoder().encode(query);
|
||
const query_array = new Uint8Array(buffer, memory_offset, bytes.length);
|
||
query_array.set(bytes);
|
||
const query_begin = memory_offset;
|
||
memory_offset += bytes.length;
|
||
const query_end = memory_offset;
|
||
|
||
/// Initialize the lexer
|
||
exports.clickhouse_lexer_create(lexer, query_begin, query_end, 65536);
|
||
|
||
/// Allocate the out ptrs
|
||
const token_begin = memory_offset;
|
||
memory_offset += 4;
|
||
const token_end = memory_offset;
|
||
memory_offset += 4;
|
||
|
||
let result = [];
|
||
|
||
while (true) {
|
||
const token_type = exports.clickhouse_lexer_next_token(lexer, token_begin, token_end);
|
||
if (exports.clickhouse_lexer_token_is_error(token_type) || exports.clickhouse_lexer_token_is_end(token_type)) {
|
||
break;
|
||
}
|
||
|
||
const view = new DataView(buffer);
|
||
const begin = view.getUint32(token_begin, true);
|
||
const end = view.getUint32(token_end, true);
|
||
|
||
const token_bytes = new Uint8Array(buffer, begin, end - begin);
|
||
let token = new TextDecoder().decode(token_bytes);
|
||
|
||
result.push({type: token_type, significant: exports.clickhouse_lexer_token_is_significant(token_type), token: token});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
async function getQueryUnderCursor() {
|
||
const all_queries = query_area.value;
|
||
|
||
if (typeof WebAssembly !== 'object' || typeof WebAssembly.instantiate !== 'function') {
|
||
return all_queries;
|
||
}
|
||
|
||
const tokens = await tokenize(all_queries);
|
||
|
||
const cursor_position = query_area.selectionStart;
|
||
let current_query_start = 0;
|
||
let current_offset = 0;
|
||
|
||
for (const elem of tokens) {
|
||
if (current_query_start == current_offset && !elem.significant) {
|
||
current_query_start += elem.token.length;
|
||
}
|
||
current_offset += elem.token.length;
|
||
if (elem.token == ';') {
|
||
if (current_offset >= cursor_position) {
|
||
query_area.setSelectionRange(current_query_start, current_offset);
|
||
query_area.focus();
|
||
return all_queries.substring(current_query_start, current_offset);
|
||
} else {
|
||
current_query_start = current_offset;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
return all_queries;
|
||
}
|
||
|
||
function setColorTheme(new_theme, update_preference) {
|
||
theme = new_theme;
|
||
if (update_preference) {
|
||
window.localStorage.setItem('theme', theme);
|
||
}
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
redrawChart();
|
||
}
|
||
/// 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) {
|
||
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');
|
||
} else {
|
||
setColorTheme('light');
|
||
}
|
||
|
||
/// 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);
|
||
}
|
||
|
||
// Download functionality
|
||
let last_query_for_download = '';
|
||
|
||
// Toggle download dropdown
|
||
document.getElementById('download-div').addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
const dropdown = document.getElementById('download-dropdown');
|
||
const button = document.getElementById('download-div');
|
||
const context = document.getElementById('main');
|
||
|
||
if (dropdown.classList.contains('show')) {
|
||
dropdown.classList.remove('show');
|
||
} else {
|
||
// Position the dropdown relative to the button
|
||
dropdown.classList.add('show');
|
||
const button_rect = button.getBoundingClientRect();
|
||
const context_rect = context.getBoundingClientRect();
|
||
const dropdown_rect = dropdown.getBoundingClientRect();
|
||
|
||
dropdown.style.top = (button_rect.top - context_rect.top) + 'px';
|
||
dropdown.style.left = (button_rect.left - context_rect.left + button_rect.width - dropdown_rect.width) + 'px';
|
||
}
|
||
});
|
||
|
||
// Prevent dropdown from closing when clicking inside it
|
||
document.getElementById('download-dropdown').addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
});
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', function() {
|
||
const dropdown = document.getElementById('download-dropdown');
|
||
dropdown.classList.remove('show');
|
||
});
|
||
|
||
// Handle "Other" format selection
|
||
document.getElementById('download-format').addEventListener('change', function() {
|
||
const otherInput = document.getElementById('download-format-other');
|
||
if (this.value === 'other') {
|
||
otherInput.classList.add('show');
|
||
otherInput.focus();
|
||
} else {
|
||
otherInput.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// Handle download button click
|
||
document.getElementById('download-button').addEventListener('click', async function() {
|
||
const formatSelect = document.getElementById('download-format');
|
||
const formatOther = document.getElementById('download-format-other');
|
||
|
||
let format = formatSelect.value;
|
||
if (format === 'other') {
|
||
format = formatOther.value.trim();
|
||
if (!format) {
|
||
alert('Please enter a format name');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Get current values
|
||
const user = user_elem.value;
|
||
const password = password_elem.value;
|
||
const server_address = url_elem.value;
|
||
const query = last_query_for_download;
|
||
|
||
if (!query) {
|
||
alert('No query to download results for');
|
||
return;
|
||
}
|
||
|
||
// Build download URL
|
||
let url = server_address +
|
||
(server_address.indexOf('?') >= 0 ? '&' : '?') +
|
||
'add_http_cors_header=1' +
|
||
'&enable_http_compression=1' +
|
||
'&use_query_cache=1&enable_reads_from_query_cache=1&enable_writes_to_query_cache=0&query_cache_ttl=600&query_cache_nondeterministic_function_handling=save' +
|
||
'&default_format=' + encodeURIComponent(format) +
|
||
'&http_response_headers=' + encodeURIComponent(`{'Content-Disposition':'attachment; filename="result.${encodeURIComponent(format.toLowerCase())}"'}`);
|
||
if (user) url += '&user=' + encodeURIComponent(user);
|
||
if (password) url += '&password=' + encodeURIComponent(password);
|
||
|
||
let downloadQuery = query.replaceAll(/\bFORMAT\s+\w+/ig, '');
|
||
|
||
// Create a form and submit it to trigger download
|
||
const form = document.createElement('form');
|
||
form.method = 'POST';
|
||
form.action = url;
|
||
form.enctype = 'multipart/form-data';
|
||
form.style.display = 'none';
|
||
|
||
const queryInput = document.createElement('input');
|
||
queryInput.type = 'hidden';
|
||
queryInput.name = 'query';
|
||
queryInput.value = downloadQuery;
|
||
form.appendChild(queryInput);
|
||
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
document.body.removeChild(form);
|
||
|
||
// Close dropdown
|
||
document.getElementById('download-dropdown').classList.remove('show');
|
||
});
|
||
</script>
|
||
</html>
|