ClickHouse/programs/server/play.html

473 lines
15 KiB
HTML
Raw Normal View History

2020-10-19 19:25:58 +00:00
<html> <!-- TODO If I write DOCTYPE HTML something changes but I don't know what. -->
2020-10-19 18:29:51 +00:00
<head>
<meta charset="UTF-8">
<title>ClickHouse Query</title>
2020-11-04 22:17:19 +00:00
<!-- Code Style:
2020-10-19 19:25:58 +00:00
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
(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.
2020-10-19 23:37:10 +00:00
No unexpected changes in positions of elements while the page is loading.
Navigation by keyboard should work.
64-bit numbers must display correctly.
2020-10-19 19:25:58 +00:00
-->
2020-11-04 22:17:19 +00:00
<!-- Development Roadmap:
1. Add indication that the query was sent and when the query has been finished.
Do not use any animated spinners. Just a text or check mark.
Eliminate race conditions (results from the previous query should be ignored on arrival, the previous request should be cancelled).
2. 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.
3. Add history in localstorage. Integrate with history API.
There can be a counter in localstorage, that will be appended to location #fragment.
The 'back', 'forward' buttons in browser should work.
Also there should be UI element to list all the queries from history and select from the list.
4. Trivial sharing capabilities.
Sharing is only possible when system.query_log is accessible. Read the X-ClickHouse-QueryId from the response.
Share button will: - emit SYSTEM FLUSH LOGS if not readonly; - find the query in the query_log;
- generate an URL with the query id and: server address if not equal to the URL's host; user name if not default;
indication that password should be entered in case of non-empty password.
-->
2020-10-19 18:29:51 +00:00
<style type="text/css">
2020-10-19 23:37:10 +00:00
:root {
--background-color: #DDF8FF; /* Or #FFFBEF; actually many pastel colors look great for light theme. */
--element-background-color: #FFF;
--border-color: #EEE;
--shadow-color: rgba(0, 0, 0, 0.1);
2020-10-20 01:05:29 +00:00
--button-color: #FFAA00; /* Orange on light-cyan is especially good. */
2020-10-19 23:37:10 +00:00
--text-color: #000;
--button-active-color: #F00;
--button-active-text-color: #FFF;
--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: #F8F8F8;
--table-hover-color: #FFF8EF;
--null-color: #A88;
}
[data-theme="dark"] {
--background-color: #000;
--element-background-color: #102030;
--border-color: #111;
--shadow-color: rgba(255, 255, 255, 0.1);
--text-color: #CCC;
--button-color: #FFAA00;
--button-text-color: #000;
--button-active-color: #F00;
--button-active-text-color: #FFF;
--misc-text-color: #888;
2020-10-20 22:13:41 +00:00
--error-color: #400;
2020-10-19 23:37:10 +00:00
--table-header-color: #102020;
--table-hover-color: #003333;
--null-color: #A88;
}
2020-10-19 18:29:51 +00:00
html, body
{
2020-10-19 19:25:58 +00:00
/* Personal choice. */
2020-10-19 18:29:51 +00:00
font-family: Sans-Serif;
2020-10-19 23:37:10 +00:00
background: var(--background-color);
color: var(--text-color);
2020-10-19 19:25:58 +00:00
}
/* Otherwise Webkit based browsers will display ugly border on focus. */
2020-10-19 23:37:10 +00:00
textarea, input, button
2020-10-19 19:25:58 +00:00
{
outline: none;
2020-10-19 23:37:10 +00:00
border: none;
color: var(--text-color);
2020-10-19 18:29:51 +00:00
}
2020-10-19 19:25:58 +00:00
/* Otherwise scrollbar may appear dynamically and it will alter viewport height,
then relative heights of elements will change suddenly, and it will break overall impression. */
/* html
2020-10-19 18:29:51 +00:00
{
overflow-x: scroll;
}*/
div
{
width: 100%;
}
.monospace
{
2020-10-19 19:25:58 +00:00
/* 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).
*/
2020-10-19 18:29:51 +00:00
font-family: Liberation Mono, DejaVu Sans Mono, MonoLisa, Consolas, Monospace;
}
.shadow
{
2020-10-19 23:37:10 +00:00
box-shadow: 0 0 1rem var(--shadow-color);
2020-10-19 18:29:51 +00:00
}
input, textarea
{
2020-10-19 23:37:10 +00:00
border: 1px solid var(--border-color);
2020-10-19 19:25:58 +00:00
/* The font must be not too small (to be inclusive) and not too large (it's less practical and make general feel of insecurity) */
2020-10-19 18:29:51 +00:00
font-size: 11pt;
padding: 0.25rem;
2020-10-19 23:37:10 +00:00
background-color: var(--element-background-color);
2020-10-19 18:29:51 +00:00
}
#query
{
2020-10-19 19:25:58 +00:00
/* Make enough space for even huge queries. */
2020-10-19 18:29:51 +00:00
height: 20%;
width: 100%;
}
#inputs
{
white-space: nowrap;
width: 100%;
}
#url
{
width: 70%;
}
#user
{
width: 15%;
}
#password
{
width: 15%;
}
#run_div
{
margin-top: 1rem;
}
#run
{
2020-10-19 23:37:10 +00:00
color: var(--button-text-color);
background-color: var(--button-color);
2020-10-19 18:29:51 +00:00
padding: 0.25rem 1rem;
cursor: pointer;
font-weight: bold;
2020-10-19 23:37:10 +00:00
font-size: 100%; /* Otherwise button element will have lower font size. */
2020-10-19 18:29:51 +00:00
}
2020-10-19 23:37:10 +00:00
#run:hover, #run:focus
2020-10-19 18:29:51 +00:00
{
2020-10-19 23:37:10 +00:00
color: var(--button-active-text-color);
background-color: var(--button-active-color);
2020-10-19 18:29:51 +00:00
}
#stats
{
float: right;
2020-10-19 23:37:10 +00:00
color: var(--misc-text-color);
}
#toggle-light, #toggle-dark
{
float: right;
padding-right: 0.5rem;
cursor: pointer;
2020-10-19 18:29:51 +00:00
}
.hint
{
2020-10-19 23:37:10 +00:00
color: var(--misc-text-color);
2020-10-19 18:29:51 +00:00
}
#data_div
{
margin-top: 1rem;
}
#data-table
{
border-collapse: collapse;
border-spacing: 0;
2020-10-19 19:25:58 +00:00
/* I need pixel-perfect alignment but not sure the following is correct, please help */
2020-10-19 18:29:51 +00:00
min-width: calc(100vw - 2rem);
}
2020-10-19 23:37:10 +00:00
/* Will be displayed when user specified custom format. */
#data-unparsed
{
background-color: var(--element-background-color);
margin-top: 0rem;
padding: 0.25rem 0.5rem;
display: none;
}
2020-10-19 18:29:51 +00:00
td
{
2020-10-19 23:37:10 +00:00
background-color: var(--element-background-color);
2020-10-19 18:29:51 +00:00
white-space: nowrap;
2020-10-19 19:25:58 +00:00
/* For wide tables any individual column will be no more than 50% of page width. */
2020-10-19 18:29:51 +00:00
max-width: 50vw;
2020-10-19 19:25:58 +00:00
/* The content is cut unless you hover. */
2020-10-19 18:29:51 +00:00
overflow: hidden;
padding: 0.25rem 0.5rem;
2020-10-19 23:37:10 +00:00
border: 1px solid var(--border-color);
2020-10-19 18:29:51 +00:00
white-space: pre;
}
td.right
{
text-align: right;
}
th
{
padding: 0.25rem 0.5rem;
text-align: middle;
2020-10-19 23:37:10 +00:00
background-color: var(--table-header-color);
border: 1px solid var(--border-color);
2020-10-19 18:29:51 +00:00
}
2020-10-19 19:25:58 +00:00
/* The row under mouse pointer is highlight for better legibility. */
2020-10-19 18:29:51 +00:00
tr:hover, tr:hover td
{
2020-10-19 23:37:10 +00:00
background-color: var(--table-hover-color);
2020-10-19 18:29:51 +00:00
}
tr:hover
{
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
}
#error
{
2020-10-19 23:37:10 +00:00
background: var(--error-color);
2020-10-19 18:29:51 +00:00
white-space: pre-wrap;
padding: 0.5rem 1rem;
display: none;
}
2020-10-19 19:25:58 +00:00
/* When mouse pointer is over table cell, will display full text (with wrap) instead of cut.
TODO Find a way to make it work on touch devices. */
2020-10-19 18:29:51 +00:00
td.left:hover
{
white-space: pre-wrap;
}
2020-10-19 19:25:58 +00:00
/* The style for SQL NULL */
2020-10-19 18:29:51 +00:00
.null
{
2020-10-19 23:37:10 +00:00
color: var(--null-color);
2020-10-19 18:29:51 +00:00
}
</style>
</head>
<body>
<div id="inputs">
<input class="monospace shadow" id="url" type="text" value="http://localhost:8123/" /><input class="monospace shadow" id="user" type="text" value="default" /><input class="monospace shadow" id="password" type="password" />
</div>
<div>
2020-10-19 23:37:10 +00:00
<textarea autofocus spellcheck="false" class="monospace shadow" id="query"></textarea>
2020-10-19 18:29:51 +00:00
</div>
<div id="run_div">
2020-10-19 23:37:10 +00:00
<button class="shadow" id="run">Run</button>
2020-10-19 18:29:51 +00:00
<span class="hint">&nbsp;(Ctrl+Enter)</span>
<span id="stats"></span>
2020-10-19 23:37:10 +00:00
<span id="toggle-dark">🌑</span><span id="toggle-light">🌞</span>
2020-10-19 18:29:51 +00:00
</div>
<div id="data_div">
2020-10-19 23:37:10 +00:00
<table class="monospace shadow" id="data-table"></table>
<pre class="monospace shadow" id="data-unparsed"></pre>
2020-10-19 18:29:51 +00:00
</div>
<p id="error" class="monospace shadow">
</p>
</body>
<script type="text/javascript">
2020-10-19 23:37:10 +00:00
/// Substitute the address of the server where the page is served.
if (location.protocol != 'file:') {
document.getElementById('url').value = location.origin;
}
2020-10-19 18:29:51 +00:00
function post()
{
2020-10-20 22:13:41 +00:00
/// TODO: Avoid race condition on subsequent requests when responses may come out of order.
/// TODO: Check if URL already contains query string (append parameters).
2020-10-19 18:29:51 +00:00
var url = document.getElementById('url').value +
2020-10-19 23:37:10 +00:00
/// Ask server to allow cross-domain requests.
2020-10-19 18:29:51 +00:00
'?add_http_cors_header=1' +
'&user=' + encodeURIComponent(document.getElementById('user').value) +
'&password=' + encodeURIComponent(document.getElementById('password').value) +
'&default_format=JSONCompact' +
2020-10-19 23:37:10 +00:00
/// Safety settings to prevent results that browser cannot display.
2020-10-19 18:29:51 +00:00
'&max_result_rows=1000&max_result_bytes=10000000&result_overflow_mode=break';
var query = document.getElementById('query').value;
var xhr = new XMLHttpRequest;
xhr.open('POST', url, true);
xhr.send(query);
xhr.onreadystatechange = function()
{
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status === 200) {
2020-10-19 23:37:10 +00:00
var json;
try { json = JSON.parse(this.response); } catch (e) {}
if (json !== undefined && json.statistics !== undefined) {
renderResult(json);
} else {
renderUnparsedResult(this.response);
}
2020-10-19 18:29:51 +00:00
} else {
2020-10-20 22:13:41 +00:00
/// TODO: Proper rendering of network errors.
2020-10-19 18:29:51 +00:00
renderError(this.response);
}
} else {
2020-10-19 23:37:10 +00:00
//console.log(this);
2020-10-19 18:29:51 +00:00
}
}
}
document.getElementById('run').onclick = function()
{
post();
}
document.getElementById('query').onkeypress = function(event)
{
2020-10-20 01:08:30 +00:00
/// Firefox has code 13 for Enter and Chromium has code 10.
2020-10-19 19:25:58 +00:00
if (event.ctrlKey && (event.charCode == 13 || event.charCode == 10)) {
2020-10-19 18:29:51 +00:00
post();
}
}
function clear()
{
var table = document.getElementById('data-table');
while (table.firstChild) {
table.removeChild(table.lastChild);
}
2020-10-19 23:37:10 +00:00
document.getElementById('data-unparsed').innerText = '';
document.getElementById('data-unparsed').style.display = 'none';
2020-10-19 18:29:51 +00:00
document.getElementById('error').innerText = '';
document.getElementById('error').style.display = 'none';
2020-10-19 23:37:10 +00:00
document.getElementById('stats').innerText = '';
2020-10-19 18:29:51 +00:00
}
function renderResult(response)
{
//console.log(response);
clear();
var stats = document.getElementById('stats');
stats.innerText = 'Elapsed: ' + response.statistics.elapsed.toFixed(3) + " sec, read " + response.statistics.rows_read + " rows.";
var thead = document.createElement('thead');
for (var idx in response.meta) {
var th = document.createElement('th');
var name = document.createTextNode(response.meta[idx].name);
th.appendChild(name);
thead.appendChild(th);
}
2020-10-19 23:37:10 +00:00
/// To prevent hanging the browser, limit the number of cells in a table.
/// It's important to have the limit on number of cells, not just rows, because tables may be wide or narrow.
2020-10-19 18:29:51 +00:00
var max_rows = 10000 / response.meta.length;
var row_num = 0;
var tbody = document.createElement('tbody');
for (var row_idx in response.data) {
var tr = document.createElement('tr');
for (var col_idx in response.data[row_idx]) {
var td = document.createElement('td');
var cell = response.data[row_idx][col_idx];
var is_null = (cell === null);
var content = document.createTextNode(is_null ? 'ᴺᵁᴸᴸ' : cell);
td.appendChild(content);
2020-10-20 22:13:41 +00:00
/// TODO: Execute regexp only once for each column.
2020-10-19 18:29:51 +00:00
td.className = response.meta[col_idx].type.match(/^(U?Int|Decimal|Float)/) ? 'right' : 'left';
if (is_null) {
td.className += ' null';
}
tr.appendChild(td);
}
tbody.appendChild(tr);
++row_num;
if (row_num >= max_rows) {
break;
}
}
var table = document.getElementById('data-table');
table.appendChild(thead);
table.appendChild(tbody);
}
2020-10-20 01:08:30 +00:00
/// A function to render raw data when non-default format is specified.
2020-10-19 23:37:10 +00:00
function renderUnparsedResult(response)
{
clear();
var data = document.getElementById('data-unparsed')
2020-10-20 22:13:41 +00:00
if (response === '') {
/// TODO: Fade or remove previous result when new request will be performed.
response = 'Ok.';
}
2020-10-19 23:37:10 +00:00
data.innerText = response;
/// inline-block make width adjust to the size of content.
data.style.display = 'inline-block';
}
2020-10-19 18:29:51 +00:00
function renderError(response)
{
clear();
document.getElementById('error').innerText = response;
document.getElementById('error').style.display = 'block';
}
2020-10-19 23:37:10 +00:00
2020-10-20 00:32:50 +00:00
function setColorTheme(theme)
2020-10-19 23:37:10 +00:00
{
2020-10-20 00:32:50 +00:00
window.localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
2020-10-19 23:37:10 +00:00
}
2020-10-20 01:08:30 +00:00
/// The choice of color theme is saved in browser.
2020-10-20 00:32:50 +00:00
var theme = window.localStorage.getItem('theme');
if (theme) {
setColorTheme(theme);
2020-10-19 23:37:10 +00:00
}
document.getElementById('toggle-light').onclick = function()
{
2020-10-20 00:32:50 +00:00
setColorTheme('light');
2020-10-19 23:37:10 +00:00
}
document.getElementById('toggle-dark').onclick = function()
{
2020-10-20 00:32:50 +00:00
setColorTheme('dark');
2020-10-19 23:37:10 +00:00
}
2020-10-19 18:29:51 +00:00
</script>
</html>