Files
datastore/programs/server/play.html
T
Alexey MilovidovandGitHub 6f4e5650bf Merge pull request #97531 from ClickHouse/play-html-database-loading-hourglass
Show rotating hourglass while loading tables in play.html sidebar
2026-02-22 20:15:44 +01:00

3026 lines
116 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
- 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;
--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";
cursor: e-resize;
}
#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);
}
.loading-hourglass
{
display: inline-block;
margin-left: 0.5em;
color: var(--misc-text-color);
animation: hourglass-animation 2s linear infinite;
line-height: 0;
vertical-align: middle;
}
.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: flex;
align-items: center;
gap: 1rem;
}
query-progress
{
flex: 1;
}
#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, #copy-div:hover, #copy-div: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);
}
#copy-div
{
display: none;
user-select: none;
color: var(--button-text-color);
background-color: var(--button-color);
padding: 0.25rem 0.65rem;
cursor: pointer;
position: relative;
}
.copy-balloon
{
position: absolute;
bottom: calc(100% + 0.25rem);
left: 50%;
transform: translateX(-50%);
background-color: var(--button-color);
color: var(--button-text-color);
padding: 0.25rem 0.5rem;
white-space: nowrap;
font-size: 0.85em;
opacity: 0;
animation: balloon-fade 1s ease-out;
pointer-events: none;
}
@keyframes balloon-fade
{
0% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; }
}
#copy-icon
{
width: 1em;
height: 1em;
vertical-align: middle;
}
#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;
vertical-align: middle;
}
#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);
}
}
#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;
}
.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;
}
a, a:visited
{
color: var(--link-color);
text-decoration: none;
}
#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;
}
#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;
}
</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>
<query-progress id="query-progress"></query-progress>
<div id="copy-div" class="shadow" title="Copy raw result to clipboard">
<svg id="copy-icon" 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">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</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="theme"><span id="toggle-dark">🌘</span><span id="toggle-light">☀️</span></div>
</div>
</form>
<query-result id="query-result"></query-result>
<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/">&#9729;</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">
class QueryProgressElement extends HTMLElement
{
constructor()
{
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host
{
display: grid;
grid-template-columns: auto 1fr auto;
gap: 1rem;
align-items: center;
}
#hourglass, #check-mark
{
grid-column: 1;
grid-row: 1;
}
#hourglass
{
display: none;
font-size: 110%;
color: #888;
animation: hourglass-animation 2s linear infinite;
}
#check-mark
{
display: none;
font-size: 110%;
color: #080;
}
#progress
{
display: none;
width: 100%;
color: var(--misc-text-color);
background: linear-gradient(to right,
transparent 0%,
var(--progress-color) var(--progress, 0%),
transparent var(--progress, 0%));
}
#stats
{
color: var(--misc-text-color);
white-space: nowrap;
}
#stats b
{
color: var(--progress-color);
}
@keyframes hourglass-animation
{
0% { transform: none; }
25% { transform: rotate(180deg); }
50% { transform: rotate(180deg); }
75% { transform: rotate(360deg); }
100% { transform: rotate(360deg); }
}
</style>
<span id="hourglass">&#10711;</span>
<span id="check-mark">&#10004;</span>
<span id="progress"></span>
<span id="stats"></span>
`;
this._hourglass = this.shadowRoot.getElementById('hourglass');
this._checkMark = this.shadowRoot.getElementById('check-mark');
this._progress = this.shadowRoot.getElementById('progress');
this._stats = this.shadowRoot.getElementById('stats');
}
clear()
{
this._hourglass.style.display = 'none';
this._checkMark.style.display = 'none';
this._stats.innerText = '';
this._progress.style.display = 'none';
this._progress.innerText = '';
this.style.setProperty('--progress', '0%');
}
start()
{
this._checkMark.style.display = 'none';
this._hourglass.style.display = 'inline-block';
}
finish()
{
this._hourglass.style.display = 'none';
this._checkMark.style.display = 'inline-block';
}
updateProgress(progress)
{
const rows = +progress.read_rows;
const bytes = +progress.read_bytes;
const total_rows = +progress.total_rows_to_read;
const 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})`; }
this._stats.innerHTML = text;
this.style.setProperty('--progress',
rows && total_rows ? (100 * rows / total_rows) + '%' : '0');
}
updateText(rowCount, elapsedNs, incomplete)
{
let progress_text = '';
if (rowCount)
{
progress_text += `${rowCount}${incomplete ? '+' : ''} rows in result`;
if (elapsedNs) progress_text += ', ';
}
if (elapsedNs) { progress_text += `${(elapsedNs / 1e9).toFixed(2)} sec.`; }
this._progress.innerText = progress_text;
this._progress.style.display = 'block';
}
}
customElements.define('query-progress', QueryProgressElement);
class QueryResultElement extends HTMLElement
{
static _loadDagrePromise = null;
static _loadUplotPromise = null;
constructor()
{
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host
{
display: block;
}
.monospace
{
font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
}
.shadow
{
box-shadow: 0 0 1rem var(--shadow-color);
}
::selection
{
color: var(--selection-color);
background-color: var(--selection-background-color);
}
a, a:visited
{
color: var(--link-color);
text-decoration: 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);
}
#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);
max-width: 50vw;
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);
}
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;
}
#graph
{
display: none;
margin: auto;
}
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);
}
#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;
}
.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;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>
<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>
`;
this._dataDiv = this.shadowRoot.getElementById('data_div');
this._dataTable = this.shadowRoot.getElementById('data-table');
this._dataUnparsed = this.shadowRoot.getElementById('data-unparsed');
this._chart = this.shadowRoot.getElementById('chart');
this._graph = this.shadowRoot.getElementById('graph');
this._error = this.shadowRoot.getElementById('error');
this._row_idx = 0;
this._elapsed_ns = 0;
this._incomplete_result = false;
this._is_explain_graph = false;
this._explain_graph = '';
this._column_is_number = {};
this._extremes = {};
this._header = {};
this._current_selected_cell = null;
this._uplot = null;
this._queryText = '';
this._max_rows = 1_000;
this._max_cells = 100_000;
this.setAttribute('tabindex', '0');
this.style.outline = 'none';
this.addEventListener('keydown', (e) => this._handleKeyDown(e));
new ResizeObserver(() => this._resizeChart()).observe(this._chart);
}
get rowCount() { return this._row_idx; }
get elapsedNs() { return this._elapsed_ns; }
set elapsedNs(v) { this._elapsed_ns = v; }
get incompleteResult() { return this._incomplete_result; }
get isExplainGraph() { return this._is_explain_graph; }
get explainGraph() { return this._explain_graph; }
set queryText(v) { this._queryText = v; }
get rawText() { return this._dataUnparsed.textContent; }
_clearElement(elem)
{
while (elem.firstChild)
{
elem.removeChild(elem.lastChild);
}
elem.style.display = 'none';
}
clear()
{
this._clearElement(this._dataTable);
this._clearElement(this._graph);
this._clearElement(this._chart);
this._clearElement(this._dataUnparsed);
this._clearElement(this._error);
this._dataTable.classList.remove('fixed');
this._dataDiv.classList.remove('fixed');
this._extremes = {};
this._header = {};
this._row_idx = 0;
this._elapsed_ns = 0;
this._incomplete_result = false;
this._is_explain_graph = false;
this._explain_graph = '';
this._current_selected_cell = null;
}
clearSelection()
{
this._current_selected_cell?.classList.remove('td-selected');
this._current_selected_cell?.parentElement.classList.remove('tr-selected');
this._current_selected_cell = null;
}
clearError()
{
this._clearElement(this._error);
}
_renderCell(name, value)
{
let td = document.createElement('td');
let is_null = (value === null);
let is_link = false;
let text;
if (is_null)
{
text = '\u1D3A\u1D41\u1D38\u1D38';
}
else if (typeof(value) === 'object')
{
text = JSON.stringify(value);
}
else
{
text = value;
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 = this._column_is_number[name] ? 'right' : 'left';
if (this._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;
}
appendHeader(meta)
{
if (meta.length == 0) { return; }
this._header = meta;
let thead = document.createElement('thead');
let th = document.createElement('th');
th.className = 'row-number';
th.appendChild(document.createTextNode('\u2116'));
thead.appendChild(th);
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);
this._appendHeaderResizer(th);
th.style.zIndex = zIndex;
--zIndex;
thead.appendChild(th);
this._column_is_number[elem.name] = !!elem.type.match(/^(Nullable\()?(U?Int|Decimal|Float)/);
});
let table = this._dataTable;
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';
this._row_idx = 0;
}
appendRow(row, fragment)
{
if (this._row_idx >= this._max_rows || (1 + this._row_idx) * Object.keys(this._header).length > this._max_cells)
{
this._incomplete_result = true;
return false;
}
++this._row_idx;
let tr = document.createElement('tr');
let td = document.createElement('td');
td.className = 'row-number';
td.appendChild(document.createTextNode(this._row_idx));
tr.appendChild(td);
for (const column of this._header)
{
const td = this._renderCell(column.name, row[column.name]);
td.onclick = () => {
this._current_selected_cell?.classList.remove('td-selected');
this._current_selected_cell?.parentElement.classList.remove('tr-selected');
this._current_selected_cell = td;
this._current_selected_cell.classList.add('td-selected');
this._current_selected_cell.parentElement.classList.add('tr-selected');
this._current_selected_cell.scrollIntoView({ block: 'nearest' });
this.focus();
};
tr.appendChild(td);
}
fragment.appendChild(tr);
this._incomplete_result = false;
return true;
}
flushFragment(fragment)
{
let tbody = this.shadowRoot.getElementById('tbody');
if (tbody && fragment.hasChildNodes())
{
tbody.appendChild(fragment);
}
}
renderExtremes()
{
let col_idx = 0;
for (const column of this._header)
{
const name = column.name;
++col_idx;
if (this._column_is_number[name]
&& this._extremes.min[name] !== undefined && this._extremes.max[name] !== undefined
&& Number(this._extremes.max[name]) > Number(this._extremes.min[name]))
{
let table = this._dataTable;
const double_sided = this._extremes.min[name] < 0;
const ratio_positive = this._extremes.max[name] / (this._extremes.max[name] - this._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 / this._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 / this._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 / this._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%)`;
}
}
}
}
}
}
transposeIfNeeded()
{
let table = this._dataTable;
if (Object.keys(this._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)
{
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);
}
_changeTableLayout()
{
const table = this._dataTable;
const headers = table.querySelectorAll('th');
table.style.setProperty('--table-width', `${table.offsetWidth}px`);
let columnWidths = [];
headers.forEach(header => {
columnWidths.push(header.offsetWidth);
});
table.classList.add('fixed');
this._dataDiv.classList.add('fixed');
headers.forEach((header, index) => {
header.style.width = `${columnWidths[index]}px`;
});
}
_appendHeaderResizer(header)
{
let drag_state = {
elem: null,
is_dragging: true,
offset_x: null,
offset_width: null
};
const start = (e) => {
if (e.button !== 0) { return; }
this._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 = this._dataTable;
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);
}
renderError(message)
{
this._error.innerText = message ? message : "No response.";
this._error.style.display = 'block';
}
updateRaw(data)
{
const fragment = document.createTextNode(data);
this._dataUnparsed.appendChild(fragment);
this._dataUnparsed.style.display = 'block';
}
update(json, fragment)
{
if (json.progress)
{
this._elapsed_ns = +json.progress.elapsed_ns;
this.dispatchEvent(new CustomEvent('query-progress', { detail: json.progress }));
return true;
}
else if (json.meta)
{
this.appendHeader(json.meta);
return true;
}
else if (json.row)
{
if (this._is_explain_graph
|| (this._row_idx == 0 && Object.keys(json.row).length == 1 && json.row.explain == 'digraph'
&& this._queryText.match(/^\s*EXPLAIN/i)))
{
this._is_explain_graph = true;
this._explain_graph += json.row.explain + '\n';
return true;
}
else
{
return this.appendRow(json.row, fragment);
}
}
else if (json.min)
{
this._extremes['min'] = json.min;
return true;
}
else if (json.max)
{
this._extremes['max'] = json.max;
return true;
}
else if (json.exception)
{
this.renderError(json.exception);
return true;
}
}
_handleKeyDown(e)
{
let cell = this._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;
}
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' });
this._current_selected_cell = new_cell;
}
_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);
});
}
_loadDagre()
{
if (QueryResultElement._loadDagrePromise) { return QueryResultElement._loadDagrePromise; }
QueryResultElement._loadDagrePromise = Promise.all([
this._loadJS('https://dagrejs.github.io/project/dagre/v0.8.5/dagre.min.js',
'sha384-2IH3T69EIKYC4c+RXZifZRvaH5SRUdacJW7j6HtE5rQbvLhKKdawxq6vpIzJ7j9M'),
this._loadJS('https://dagrejs.github.io/project/graphlib-dot/v0.6.4/graphlib-dot.min.js',
'sha384-Q7oatU+b+y0oTkSoiRH9wTLH6sROySROCILZso/AbMMm9uKeq++r8ujD4l4f+CWj'),
this._loadJS('https://dagrejs.github.io/project/dagre-d3/v0.6.4/dagre-d3.min.js',
'sha384-9N1ty7Yz7VKL3aJbOk+8ParYNW8G5W+MvxEfFL9G7CRYPmkHI9gJqyAfSI/8190W'),
this._loadJS('https://cdn.jsdelivr.net/npm/[email protected]',
'sha384-S+Kf0r6YzKIhKA8d1k2/xtYv+j0xYUU3E7+5YLrcPVab6hBh/r1J6cq90OXhw80u'),
]);
return QueryResultElement._loadDagrePromise;
}
_loadUplot()
{
if (QueryResultElement._loadUplotPromise) { return QueryResultElement._loadUplotPromise; }
QueryResultElement._loadUplotPromise = this._loadJS('https://cdn.jsdelivr.net/npm/[email protected]/dist/uPlot.iife.min.js',
'sha384-TwdJPnTsKP6pnvFZZKda0WJCXpjcHCa7MYHmjrYDu6rsEsb/UnFdoL0phS5ODqTA');
return QueryResultElement._loadUplotPromise;
}
async renderGraph()
{
this._clearElement(this._dataTable);
await this._loadDagre();
const dot = this._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 = this._graph;
svg.style.display = 'block';
render(d3.select(svg), graph);
svg.style.width = graph.graph().width;
svg.style.height = graph.graph().height;
}
_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;
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,
}
};
}
async renderChart(json)
{
await this._loadUplot();
let chart = this._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: [ this._legendAsTooltipPlugin() ],
};
this._uplot = new uPlot(opts, json, chart);
}
_resizeChart()
{
if (this._uplot)
{
let chart = this._chart;
this._uplot.setSize({ width: chart.clientWidth, height: chart.clientHeight });
}
}
redrawChart()
{
if (this._uplot && this._chart.style.display == 'block')
{
this.renderChart(this._uplot.data);
}
}
}
customElements.define('query-result', QueryResultElement);
/// 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');
let progressEl = document.getElementById('query-progress');
let resultEl = document.getElementById('query-result');
let logoEl = document.getElementById('logo-container');
resultEl.addEventListener('query-progress', (e) => {
elapsed_ns = +e.detail.elapsed_ns;
progressEl.updateProgress(e.detail);
progressEl.updateText(resultEl.rowCount, elapsed_ns, resultEl.incompleteResult);
});
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 auxiliaryQuery(server_address, user, password, query, url_params = '') {
let url = server_address +
(server_address.indexOf('?') >= 0 ? '&' : '?') +
'add_http_cors_header=1&default_format=JSON' + url_params;
if (user) url += '&user=' + encodeURIComponent(user);
if (password) url += '&password=' + encodeURIComponent(password);
let response;
try {
response = await fetch(url, { method: "POST", body: query, headers: { 'Authorization': 'never' } });
} catch (e) {
clear();
resultEl.renderError("Network error");
logoEl.style.display = 'none';
return false;
}
if (!response.ok) {
clear();
resultEl.renderError(await response.text());
logoEl.style.display = 'none';
return false;
}
resultEl.clearError();
logoEl.style.display = 'block';
return await response.json();
}
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 json = await auxiliaryQuery(server_address, user, password,
`SELECT database, database = currentDatabase() AS current
FROM system.databases WHERE database != 'INFORMATION_SCHEMA'
ORDER BY
database = 'information_schema',
database = 'system',
database != 'default',
database`);
if (!json) return false;
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']) {
let hourglass = document.createElement('span');
hourglass.className = 'loading-hourglass';
hourglass.textContent = '\u29D7';
database_link.after(hourglass);
loadTables(server_address, user, password, elem.database, database).then(ok => {
hourglass.remove();
if (ok !== false) 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 json = await auxiliaryQuery(server_address, user, password,
`SELECT table, engine, total_rows, total_bytes FROM system.tables WHERE database = {database:String}
ORDER BY table LIKE '.inner%', table`,
'&param_database=' + encodeURIComponent(database));
if (!json) return false;
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');
let menu_elem = document.getElementById('menu');
function toggleDatabases() {
if (!databases_toggle.dataset['opened']) {
loadDatabases(url_elem.value, user_elem.value, password_elem.value).then(ok => {
if (ok !== false) {
databases_toggle.dataset['opened'] = 1;
menu_elem.style.cursor = 'w-resize';
}
});
} else {
delete databases_toggle.dataset['opened'];
menu_elem.style.cursor = 'e-resize';
let databases_elem = document.getElementById('databases');
while (databases_elem.firstChild) databases_elem.removeChild(databases_elem.firstChild);
}
}
databases_toggle.addEventListener('click', e => {
e.stopPropagation();
toggleDatabases();
});
document.getElementById('menu').addEventListener('click', e => {
if (!e.target.closest('#github-icon') && !e.target.closest('#databases-toggle') && !e.target.closest('button')) {
toggleDatabases();
}
});
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 elapsed_ns = 0;
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&query_cache_system_table_handling=ignore';
/// 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('download-div').style.display = 'none';
document.getElementById('copy-div').style.display = 'none';
clear();
progressEl.start();
resultEl.queryText = query;
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":`)) {
resultEl.update(JSON.parse(line));
has_exception = true;
break;
}
}
if (!has_exception) {
resultEl.renderError(reply);
logoEl.style.display = 'none';
}
progressEl.clear();
document.getElementById('download-div').style.display = 'none';
document.getElementById('copy-div').style.display = 'none';
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 = resultEl.update(data, fragment);
if (!cont) { break; }
}
resultEl.flushFragment(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; }
resultEl.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; }
resultEl.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();
}
resultEl.renderError(reply);
logoEl.style.display = 'none';
progressEl.clear();
document.getElementById('download-div').style.display = 'none';
document.getElementById('copy-div').style.display = 'none';
return;
}
if (resultEl.isExplainGraph) {
await resultEl.renderGraph();
} else {
if (resultEl._extremes['min'] && resultEl._extremes['max']) resultEl.renderExtremes();
if (is_table) resultEl.transposeIfNeeded();
}
progressEl.finish();
document.getElementById('download-div').style.display = 'block';
document.getElementById('copy-div').style.display = is_raw ? 'block' : 'none';
last_query_for_download = query;
if (is_raw) {
resultEl.elapsedNs = 1e6 * (performance.now() - last_query_start);
elapsed_ns = resultEl.elapsedNs;
} else {
elapsed_ns = resultEl.elapsedNs;
}
progressEl.updateText(resultEl.rowCount, resultEl.elapsedNs, resultEl.incompleteResult);
progressEl.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;
resultEl.queryText = 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":`)) {
resultEl.update(JSON.parse(line));
has_exception = true;
break;
}
}
if (!has_exception) {
resultEl.renderError(state.data);
logoEl.style.display = 'none';
}
progressEl.clear();
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 (!resultEl.update(JSON.parse(line), fragment)) { break; }
}
resultEl.flushFragment(fragment);
} else if (format == 'JSONCompactColumns') {
is_chart = true;
if (posted_request_num != request_num) { return; }
resultEl.renderChart(JSON.parse(state.data));
} else {
is_raw = true;
resultEl.updateRaw(state.data);
}
if (resultEl.isExplainGraph) {
await resultEl.renderGraph();
} else {
if (resultEl._extremes['min'] && resultEl._extremes['max']) resultEl.renderExtremes();
if (is_table) resultEl.transposeIfNeeded();
}
progressEl.finish();
document.getElementById('copy-div').style.display = is_raw ? 'block' : 'none';
if (is_raw) {
resultEl.elapsedNs = state.elapsed_ns;
}
elapsed_ns = resultEl.elapsedNs;
progressEl.updateText(resultEl.rowCount, resultEl.elapsedNs, resultEl.incompleteResult);
progressEl.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;
// Favicon changes to gray while a query is running, to indicate progress on the browser tab.
const favicon_default = document.querySelector('link[rel="icon"]').href;
const favicon_running = 'data:image/svg+xml;base64,' + btoa(atob(favicon_default.split(',')[1]).replace('#ff0', '#bbb'));
function setFavicon(href) { document.querySelector('link[rel="icon"]').href = href; }
async function post()
{
++request_num;
document.body.scrollTo(0, 0);
let query = await getQueryUnderCursor();
in_flight = true;
setFavicon(favicon_running);
let run = document.getElementById('run');
run.innerText = 'Stop';
await postImpl(request_num, query);
run.blur();
run.innerText = 'Run';
setFavicon(favicon_default);
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 clear()
{
resultEl.clear();
progressEl.clear();
logoEl.style.display = 'block';
elapsed_ns = 0;
}
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);
}
document.getElementById('query').addEventListener('focus', e => {
resultEl.clearSelection();
});
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);
}
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);
resultEl.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);
}
// Copy raw result to clipboard
document.getElementById('copy-div').addEventListener('click', function() {
let text = resultEl.rawText;
if (!text) return;
let button = this;
let showBalloon = function(message) {
let balloon = document.createElement('span');
balloon.className = 'copy-balloon';
balloon.textContent = message;
button.appendChild(balloon);
balloon.addEventListener('animationend', () => balloon.remove());
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
() => showBalloon('Result copied'),
() => showBalloon('Copy failed'));
} else {
showBalloon('Copy not supported');
}
});
// 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&query_cache_system_table_handling=ignore' +
'&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>