Files
datastore/programs/server/play.html
T
2025-12-31 15:36:41 -05:00

2694 lines
104 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">&nbsp;(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 += '&param_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>