mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-09-19 16:20:50 +00:00
refined dashboard
This commit is contained in:
parent
bc50b68f05
commit
03e91450a3
@ -26,76 +26,45 @@
|
||||
<style>
|
||||
:root {
|
||||
--background-color: #DDF8FF;
|
||||
/* Or #FFFBEF; actually many pastel colors look great for light theme. */
|
||||
--element-background-color: #FFF;
|
||||
--input-background-color: #cccccc;
|
||||
--accent-color: #faff68;
|
||||
--border-color: #EEE;
|
||||
--input-background-color: #BDD;
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--button-color: #FFAA00;
|
||||
/* Orange on light-cyan is especially good. */
|
||||
--text-color: #080808;
|
||||
--header-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. */
|
||||
--null-color: #A88;
|
||||
--link-color: #06D;
|
||||
--logo-color: #CEE;
|
||||
--logo-color-active: #BDD;
|
||||
--tab-switch-background-color: #FFF;
|
||||
--tab-switch-hover-color: #F1F1F1;
|
||||
--tab-switch-selected-border-color: #000;
|
||||
--storage-path-hover-color: #FFF;
|
||||
--storage-path-selected-border-color: #000;
|
||||
--storage-ui-button-hover-color: #F1F1F1;
|
||||
--storage-ui-button-border-color: #000;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background-color: #414141;
|
||||
--element-background-color: #252521;
|
||||
--input-background-color: #111;
|
||||
--accent-color: #faff68;
|
||||
--border-color: #111;
|
||||
--shadow-color: rgba(255, 255, 255, 0.1);
|
||||
--text-color: #B4B4B4;
|
||||
--header-text-color: #F9F9F9;
|
||||
--button-color: #FFAA00;
|
||||
--button-text-color: #000;
|
||||
--button-active-color: #F00;
|
||||
--button-active-text-color: #FFF;
|
||||
--misc-text-color: #888;
|
||||
--error-color: #400;
|
||||
--null-color: #A88;
|
||||
--link-color: #4BDAF7;
|
||||
--logo-color: #222;
|
||||
--logo-color-active: #333;
|
||||
--tab-switch-background-color: #252521;
|
||||
--tab-switch-hover-color: #151515;
|
||||
--tab-switch-selected-border-color: #faff68;
|
||||
--storage-path-hover-color: #151515;
|
||||
--storage-path-selected-border-color: #faff68;
|
||||
--storage-ui-button-hover-color: #151515;
|
||||
--storage-ui-button-border-color: #faff68;
|
||||
}
|
||||
|
||||
/* [data-theme="dark"] {
|
||||
--background-color: #000;
|
||||
--element-background-color: #102030;
|
||||
--accent-color: #faff68;
|
||||
--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;
|
||||
--error-color: #400;
|
||||
--null-color: #A88;
|
||||
--link-color: #4BDAF7;
|
||||
--logo-color: #222;
|
||||
--logo-color-active: #333;
|
||||
--tab-switch-background-color: #102030;
|
||||
--tab-switch-hover-color: #182838;
|
||||
--tab-switch-selected-border-color: #CCC;
|
||||
} */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
/* For iPad */
|
||||
@ -125,20 +94,49 @@
|
||||
}
|
||||
|
||||
#controls {
|
||||
/* When a page will be scrolled horizontally due to large table size, keep controls in place. */
|
||||
position: sticky;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Otherwise Webkit based browsers will display ugly border on focus. */
|
||||
textarea,
|
||||
input,
|
||||
button {
|
||||
input {
|
||||
outline: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.storage-ui-button {
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.0rem;
|
||||
color: var(--header-text-color);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--storage-ui-button-border-color);
|
||||
}
|
||||
|
||||
.storage-ui-button:hover {
|
||||
background-color: var(--storage-ui-button-hover-color);
|
||||
}
|
||||
|
||||
.storage-ui-button:disabled {
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.0rem;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.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). */
|
||||
@ -154,6 +152,7 @@
|
||||
box-shadow: 0 0 1rem var(--shadow-color);
|
||||
}
|
||||
|
||||
#displayStorageData,
|
||||
#command-scrolling-container,
|
||||
#command,
|
||||
#command-output {
|
||||
@ -180,9 +179,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--link-color);
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -314,6 +311,11 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: var(--header-text-color);
|
||||
font-size: 1.0rem;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
@ -349,37 +351,6 @@
|
||||
text-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
.tab:checked:nth-of-type(3)~.tab__content:nth-of-type(3) {
|
||||
opacity: 1;
|
||||
/* transition: 0.5s opacity ease-in, 0.8s transform ease; */
|
||||
/* flex-grow: 2; */
|
||||
position: relative;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
transform: translateY(0px);
|
||||
text-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
.tab:checked:nth-of-type(4)~.tab__content:nth-of-type(4) {
|
||||
opacity: 1;
|
||||
/* transition: 0.5s opacity ease-in, 0.8s transform ease; */
|
||||
position: relative;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
transform: translateY(0px);
|
||||
text-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
.tab:checked:nth-of-type(5)~.tab__content:nth-of-type(5) {
|
||||
opacity: 1;
|
||||
/* transition: 0.5s opacity ease-in, 0.8s transform ease; */
|
||||
position: relative;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
transform: translateY(0px);
|
||||
text-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
#sidebar-version-display {
|
||||
position: absolute;
|
||||
bottom: -1rem;
|
||||
@ -403,6 +374,56 @@
|
||||
margin-left: 16rem;
|
||||
}
|
||||
|
||||
.storage_viewer_container * {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.storage_viewer_container ul {
|
||||
padding-left: 1em;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.storage_viewer_container>ul:first-of-type {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.storage_viewer_container li span.storage_node_description {
|
||||
cursor: pointer;
|
||||
padding-left: 5px;
|
||||
padding-bottom: 4px;
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.storage_viewer_container li span.storage_node_description:hover {
|
||||
background-color: var(--storage-path-hover-color);
|
||||
}
|
||||
|
||||
.storage_viewer_container li span.storage_node_description.storage_leaf {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
.storage_viewer_container span.storage_node_description.selected {
|
||||
color: var(--header-color);
|
||||
border-left: 4px solid var(--storage-path-selected-border-color);
|
||||
}
|
||||
|
||||
.storage_viewer_container li span.storage_ui_icon,
|
||||
.storage_viewer_container li span.storage_ui_icon * {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.storage_viewer_container input.storage_node_name_input {
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
padding-left: 5px;
|
||||
padding-bottom: 4px;
|
||||
background-color: var(--input-background-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -440,18 +461,88 @@
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="controls">
|
||||
<div id="theme_div">
|
||||
<span id="toggle-dark">🌑</span><span id="toggle-light">🌞</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-wrap">
|
||||
<!-- active tab on page load gets checked attribute -->
|
||||
<input type="radio" id="tab_storage" name="tabGroup1" class="tab">
|
||||
<input type="radio" id="tab_storage" name="tabGroup1" class="tab" checked>
|
||||
<label for="tab_storage">Storage</label>
|
||||
|
||||
<input type="radio" id="tab_4lw" name="tabGroup1" class="tab" checked>
|
||||
<input type="radio" id="tab_4lw" name="tabGroup1" class="tab">
|
||||
<label for="tab_4lw">4lw</label>
|
||||
|
||||
<div class="tab__content">
|
||||
<h3>Storage</h3>
|
||||
<div style="display: flex; width: 100%;">
|
||||
|
||||
<div id="storage-container"></div>
|
||||
<!-- Tree view side -->
|
||||
<div style="flex-basis: 40%; box-sizing: border-box; margin-right: 0.5rem;">
|
||||
<div id="storage-viewer-container"></div>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; box-sizing: border-box; margin-left: 0.5rem">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div style="flex: 1; overflow-wrap: break-word;">
|
||||
<p id="displayStoragePath" class="monospace">--</p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button class="storage-ui-button monospace" id="addChildButton"
|
||||
onclick="storage_viewer.addNodeToSelectedNode()">
|
||||
Add Child
|
||||
</button>
|
||||
<button class="storage-ui-button monospace" id="deleteButton"
|
||||
onclick="storage_viewer.deleteSelectedNode()">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;">
|
||||
<div style="flex-basis: 50%; box-sizing: border-box; margin-right: 0.1rem;">
|
||||
<h4>numChildren:</h4>
|
||||
<p id="displayStorageNumChildren" class="monospace">--</p>
|
||||
<h4>dataLength:</h4>
|
||||
<p id="displayStorageDataLength" class="monospace">--</p>
|
||||
<h4>version:</h4>
|
||||
<p id="displayStorageVersion" class="monospace">--</p>
|
||||
<h4>aversion:</h4>
|
||||
<p id="displayStorageAVersion" class="monospace">--</p>
|
||||
<h4>cversion:</h4>
|
||||
<p id="displayStorageCVersion" class="monospace">--</p>
|
||||
<h4>ctime:</h4>
|
||||
<p id="displayStorageCtime" class="monospace">--</p>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; box-sizing: border-box; margin-left: 0.1rem">
|
||||
<h4>ephemeralOwner:</h4>
|
||||
<p id="displayStorageEphemeralOwner" class="monospace">--</p>
|
||||
<h4>cZxid:</h4>
|
||||
<p id="displayStorageCZxid" class="monospace">--</p>
|
||||
<h4>pZxid:</h4>
|
||||
<p id="displayStoragePZxid" class="monospace">--</p>
|
||||
<h4>mZxid:</h4>
|
||||
<p id="displayStorageMZxid" class="monospace">--</p>
|
||||
<h4>mtime:</h4>
|
||||
<p id="displayStorageMtime" class="monospace">--</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div style="display: flex; flex-direction: column; justify-content: flex-end;">
|
||||
<h1>Node Data:</h1>
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-start;">
|
||||
<button class="storage-ui-button monospace"
|
||||
onclick="storage_viewer.saveSelectedNodeData()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="displayStorageData" spellcheck="false" data-gramm="false" class="monospace"
|
||||
style="height: 20em"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab__content">
|
||||
@ -491,13 +582,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="controls">
|
||||
<div id="theme_div">
|
||||
<span id="toggle-dark">🌑</span><span id="toggle-light">🌞</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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"
|
||||
@ -543,6 +627,467 @@
|
||||
</body>
|
||||
|
||||
<script type="text/javascript">
|
||||
class StorageViewer {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.root = undefined;
|
||||
this.selected_node = undefined;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.root = await this.loadRoot();
|
||||
if (this.root.hasData()) {
|
||||
await this.root.loadData();
|
||||
}
|
||||
this.root.select(); // select root
|
||||
this.reload();
|
||||
}
|
||||
|
||||
async loadRoot() {
|
||||
const server_url = new URL(window.location);
|
||||
server_url.pathname = `/api/v1/storage/list/`;
|
||||
|
||||
const res = await fetch(server_url.toString());
|
||||
const data = res.ok ? await res.json() : undefined;
|
||||
|
||||
return data ? new StorageNode('/', undefined, data.stat, data.child_node_names) : undefined;
|
||||
}
|
||||
|
||||
reload() {
|
||||
if (this.container == null) {
|
||||
console.warn("No container specified");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.root == null) {
|
||||
console.warn("Root is not loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.classList.add("storage_viewer_container");
|
||||
|
||||
var cnt = document.createElement("ul");
|
||||
|
||||
cnt.appendChild(this.root.render());
|
||||
|
||||
this.container.innerHTML = "";
|
||||
this.container.appendChild(cnt);
|
||||
|
||||
this.renderSelectedNodeData();
|
||||
}
|
||||
|
||||
updateSelectedNode(node) {
|
||||
if (this.selected_node !== undefined) {
|
||||
this.selected_node.selected = false;
|
||||
}
|
||||
this.selected_node = node;
|
||||
this.selected_node.selected = true;
|
||||
}
|
||||
|
||||
renderSelectedNodeData() {
|
||||
// path
|
||||
document.getElementById('displayStoragePath').innerText = this.selected_node?.path ?? '--';
|
||||
// stat
|
||||
document.getElementById('displayStorageAVersion').innerText = this.selected_node?.stat?.aversion ?? '--';
|
||||
document.getElementById('displayStorageCZxid').innerText = this.selected_node?.stat?.cZxid ?? '--';
|
||||
document.getElementById('displayStorageCVersion').innerText = this.selected_node?.stat?.cversion ?? '--';
|
||||
document.getElementById('displayStorageDataLength').innerText = this.selected_node?.stat?.dataLength ?? '--';
|
||||
document.getElementById('displayStorageEphemeralOwner').innerText = this.selected_node?.stat?.ephemeralOwner ?? '--';
|
||||
document.getElementById('displayStorageMZxid').innerText = this.selected_node?.stat?.mZxid ?? '--';
|
||||
document.getElementById('displayStorageNumChildren').innerText = this.selected_node?.stat?.numChildren ?? '--';
|
||||
document.getElementById('displayStoragePZxid').innerText = this.selected_node?.stat?.pZxid ?? '--';
|
||||
document.getElementById('displayStorageVersion').innerText = this.selected_node?.stat?.version ?? '--';
|
||||
if (this.selected_node?.stat?.mtime != undefined) {
|
||||
document.getElementById('displayStorageMtime').innerText = new Date(this.selected_node?.stat?.mtime).toLocaleString('en-US', { hour12: false });
|
||||
} else {
|
||||
document.getElementById('displayStorageMtime').innerText = '--';
|
||||
}
|
||||
if (this.selected_node?.stat?.mtime != undefined) {
|
||||
document.getElementById('displayStorageCtime').innerText = new Date(this.selected_node?.stat?.mtime).toLocaleString('en-US', { hour12: false });
|
||||
} else {
|
||||
document.getElementById('displayStorageCtime').innerText = '--';
|
||||
}
|
||||
// data
|
||||
if (this.selected_node && this.selected_node.data) {
|
||||
document.getElementById('displayStorageData').value = new TextDecoder('utf-8').decode(this.selected_node.data);
|
||||
} else {
|
||||
document.getElementById('displayStorageData').value = '';
|
||||
}
|
||||
// buttons
|
||||
var delete_button = document.getElementById('deleteButton');
|
||||
if (!this.selected_node || !this.selected_node.isLeaf() || this.selected_node.isRoot()) {
|
||||
delete_button.disabled = true;
|
||||
delete_button.setAttribute('title', 'Only nodes with no children can be deleted.');
|
||||
} else {
|
||||
delete_button.disabled = false;
|
||||
delete_button.removeAttribute('title');
|
||||
}
|
||||
}
|
||||
|
||||
async saveSelectedNodeData() {
|
||||
if (!this.selected_node) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data_to_save = document.getElementById('displayStorageData').value;
|
||||
let encoder = new TextEncoder();
|
||||
|
||||
var server_url = new URL(window.location);
|
||||
server_url.pathname = `/api/v1/storage/set${this.selected_node.path}`;
|
||||
|
||||
var request_url = server_url.toString();
|
||||
request_url += '?' + new URLSearchParams({ version: this.selected_node.version })
|
||||
|
||||
const res = await fetch(request_url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: encoder.encode(data_to_save)
|
||||
});
|
||||
if (res.ok) {
|
||||
await this.selected_node.reloadContent();
|
||||
} else {
|
||||
// TODO render conflict error.
|
||||
}
|
||||
|
||||
this.reload();
|
||||
}
|
||||
|
||||
async deleteSelectedNode() {
|
||||
if (!this.selected_node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selected_node.isLeaf()) {
|
||||
return; // the button should be disabled
|
||||
}
|
||||
|
||||
if (this.selected_node.isRoot()) {
|
||||
return; // the button should be disabled
|
||||
}
|
||||
this.selected_node.parent.removeChild(this.selected_node.name());
|
||||
|
||||
var server_url = new URL(window.location);
|
||||
server_url.pathname = `/api/v1/storage/remove${this.selected_node.path}`;
|
||||
|
||||
var request_url = server_url.toString();
|
||||
request_url += '?' + new URLSearchParams({ version: this.selected_node.version })
|
||||
|
||||
const res = await fetch(request_url,
|
||||
{
|
||||
method: 'POST',
|
||||
});
|
||||
if (res.ok) {
|
||||
await this.selected_node.parent.reloadContent();
|
||||
await this.selected_node.parent.select();
|
||||
} else {
|
||||
// TODO render removal error.
|
||||
}
|
||||
|
||||
this.reload();
|
||||
}
|
||||
|
||||
async addNodeToSelectedNode() {
|
||||
if (!this.selected_node) {
|
||||
return;
|
||||
}
|
||||
this.selected_node.makeChildStub();
|
||||
// expand the node if not expanded
|
||||
if (!this.selected_node.expanded) {
|
||||
await this.selected_node.toggleExpanded();
|
||||
}
|
||||
this.reload();
|
||||
|
||||
// focus on the input after everything is rendered
|
||||
var stub_name_input = document.getElementsByClassName('storage_node_name_input')[0];
|
||||
stub_name_input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
class StorageNodeStub {
|
||||
constructor(parent) {
|
||||
this.parent = parent
|
||||
}
|
||||
|
||||
render() {
|
||||
var result = document.createElement("li");
|
||||
var input_field = document.createElement("input");
|
||||
input_field.className = "storage_node_name_input"; // setting a class to your input field
|
||||
input_field.storage_node = this;
|
||||
|
||||
input_field.onblur = function () {
|
||||
// need to destroy the stub as user discarded the input
|
||||
this.storage_node.destroy();
|
||||
storage_viewer.reload();
|
||||
}
|
||||
|
||||
// trigger event if user press Enter
|
||||
input_field.addEventListener("keydown", async function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
if (input_field.value == '') {
|
||||
// need to destroy the stub as user discarded the input
|
||||
this.storage_node.destroy();
|
||||
storage_viewer.reload();
|
||||
return;
|
||||
}
|
||||
await this.storage_node.parent.createChild(input_field.value);
|
||||
this.storage_node.destroy(); // we replaced the stub with a real node
|
||||
storage_viewer.reload();
|
||||
}
|
||||
});
|
||||
|
||||
result.appendChild(input_field);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.parent.stub_child = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StorageNode {
|
||||
constructor(path, parent, stat, children_names = []) {
|
||||
this.path = path;
|
||||
this.stat = stat;
|
||||
this.data = undefined;
|
||||
this.version = stat?.version ?? undefined;
|
||||
this.children_names = children_names;
|
||||
this.children = []; // nodes themselves are lazy loaded
|
||||
this.expanded = false;
|
||||
this.selected = false;
|
||||
this.parent = parent
|
||||
this.stub_child = undefined; // StorageNodeStub reference if a child node is in creation
|
||||
}
|
||||
|
||||
isLeaf() {
|
||||
return (this.children_names.length == 0 && this.stub_child == undefined);
|
||||
}
|
||||
|
||||
isRoot() {
|
||||
return this.path == '/';
|
||||
}
|
||||
|
||||
async reloadContent() {
|
||||
var server_url = new URL(window.location);
|
||||
|
||||
var promises = [];
|
||||
server_url.pathname = `/api/v1/storage/list${this.path}`;
|
||||
promises.push(fetch(server_url.toString())
|
||||
.then(res => res.ok ? res.json() : undefined)
|
||||
.then(data => {
|
||||
if (data) {
|
||||
this.stat = data.stat;
|
||||
this.version = data.stat?.version ?? undefined;
|
||||
this.children_names = data.child_node_names;
|
||||
}
|
||||
}));
|
||||
promises.push(this.loadData());
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async expand() {
|
||||
if (this.isLeaf()) {
|
||||
console.warn('This node has no children.');
|
||||
return;
|
||||
}
|
||||
|
||||
var server_url = new URL(window.location);
|
||||
server_url.pathname = `/api/v1/storage/list`;
|
||||
var list_url = server_url.toString();
|
||||
|
||||
const childNodeRequests = this.children_names.map(child_name =>
|
||||
fetch(list_url + this.getPathOfChild(child_name))
|
||||
.then(res => res.ok ? res.json() : undefined)
|
||||
.then(data => data ? new StorageNode(this.getPathOfChild(child_name), this, data.stat, data.child_node_names) : undefined)
|
||||
);
|
||||
|
||||
this.children = await Promise.all(childNodeRequests);
|
||||
this.expanded = true;
|
||||
}
|
||||
|
||||
async collapse() {
|
||||
if (storage_viewer.selected_node.isChildOf(this)) {
|
||||
this.select();
|
||||
}
|
||||
this.children.forEach(function (child) {
|
||||
child.collapse();
|
||||
});
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
var result = document.createElement("li");
|
||||
var span_desc = document.createElement("span");
|
||||
span_desc.className = "storage_node_description";
|
||||
span_desc.storage_node = this;
|
||||
|
||||
span_desc.addEventListener("click", async function (e) {
|
||||
var cur_el = e.target;
|
||||
|
||||
while (typeof cur_el.storage_node === "undefined" || cur_el.classList.contains("storage_viewer_container")) {
|
||||
cur_el = cur_el.parentElement;
|
||||
}
|
||||
|
||||
var node_cur = cur_el.storage_node;
|
||||
|
||||
if (typeof node_cur === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node_cur.hasData()) {
|
||||
await node_cur.loadData();
|
||||
}
|
||||
|
||||
node_cur.select();
|
||||
storage_viewer.reload();
|
||||
});
|
||||
|
||||
if (this.selected) {
|
||||
span_desc.classList.add("selected");
|
||||
}
|
||||
|
||||
if (this.isLeaf()) {
|
||||
var ret = '';
|
||||
span_desc.innerHTML = ret + this.name() + "</span>";
|
||||
span_desc.classList.add("storage_leaf");
|
||||
|
||||
result.appendChild(span_desc);
|
||||
} else {
|
||||
var ret = '';
|
||||
var span_icon = document.createElement("span");
|
||||
span_icon.className = "storage_ui_icon";
|
||||
span_icon.style.fontFamily = "monospace";
|
||||
span_icon.innerHTML = this.expanded ? "<span>▾</span>" : "<span>▸</span>";
|
||||
|
||||
span_icon.addEventListener("click", async function (e) {
|
||||
e.stopPropagation();
|
||||
var cur_el = e.target;
|
||||
|
||||
while (typeof cur_el.storage_node === "undefined" || cur_el.classList.contains("storage_viewer_container")) {
|
||||
cur_el = cur_el.parentElement;
|
||||
}
|
||||
|
||||
var node_cur = cur_el.storage_node;
|
||||
|
||||
if (typeof node_cur === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node_cur.isLeaf()) {
|
||||
await node_cur.toggleExpanded();
|
||||
}
|
||||
storage_viewer.reload();
|
||||
});
|
||||
|
||||
span_desc.appendChild(span_icon);
|
||||
span_desc.appendChild(document.createTextNode(this.name()));
|
||||
result.appendChild(span_desc);
|
||||
|
||||
if (this.expanded) {
|
||||
var ul_container = document.createElement("ul");
|
||||
|
||||
if (this.stub_child) {
|
||||
ul_container.appendChild(this.stub_child.render());
|
||||
}
|
||||
|
||||
this.children.forEach(function (child) {
|
||||
ul_container.appendChild(child.render());
|
||||
});
|
||||
|
||||
result.appendChild(ul_container)
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async toggleExpanded() {
|
||||
if (this.isLeaf()) {
|
||||
return;
|
||||
}
|
||||
if (this.expanded) {
|
||||
this.collapse();
|
||||
} else {
|
||||
await this.expand();
|
||||
}
|
||||
};
|
||||
|
||||
select() {
|
||||
storage_viewer.updateSelectedNode(this);
|
||||
};
|
||||
|
||||
name() {
|
||||
if (this.path === '/') {
|
||||
return '/';
|
||||
}
|
||||
const split_path = this.path.split('/');
|
||||
if (!split_path[split_path.length - 1]) {
|
||||
split_path.pop();
|
||||
}
|
||||
return split_path.length > 0 ? split_path[split_path.length - 1] : 'unknown';
|
||||
}
|
||||
|
||||
getPathOfChild(child_name) {
|
||||
return (this.path == '/' ? '' : this.path) + `/${child_name}`;
|
||||
}
|
||||
|
||||
hasData() {
|
||||
return (this.stat?.dataLength ?? 0) !== 0;
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
var server_url = new URL(window.location);
|
||||
server_url.pathname = `/api/v1/storage/get`;
|
||||
|
||||
const res = await fetch(server_url.toString() + this.path);
|
||||
if (res.ok) {
|
||||
const buffer = await res.arrayBuffer();
|
||||
this.data = new Uint8Array(buffer);
|
||||
} else {
|
||||
this.data = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
removeChild(child_name) {
|
||||
this.children_names = this.children_names.filter(name => name !== child_name);
|
||||
this.children = this.children.filter(child => child.name() !== child_name);
|
||||
}
|
||||
|
||||
makeChildStub() {
|
||||
this.stub_child = new StorageNodeStub(this);
|
||||
}
|
||||
|
||||
async createChild(child_name) {
|
||||
var server_url = new URL(window.location);
|
||||
server_url.pathname = `/api/v1/storage/create`;
|
||||
var create_url = server_url.toString();
|
||||
|
||||
const res = await fetch(create_url + this.getPathOfChild(child_name),
|
||||
{
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.children_names.push(child_name);
|
||||
var new_child_node = new StorageNode(this.getPathOfChild(child_name), this, undefined, []);
|
||||
// load necessary data for a new node
|
||||
await new_child_node.reloadContent();
|
||||
await this.reloadContent()
|
||||
this.children.push(new_child_node);
|
||||
new_child_node.select();
|
||||
} else {
|
||||
// TODO render creation error.
|
||||
}
|
||||
}
|
||||
|
||||
isChildOf(node) {
|
||||
return this.path.startsWith(node.path);
|
||||
}
|
||||
}
|
||||
|
||||
const current_url = new URL(window.location);
|
||||
const opened_locally = location.protocol == 'file:';
|
||||
|
||||
@ -606,7 +1151,6 @@
|
||||
window.localStorage.setItem('theme', theme);
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
redrawChart();
|
||||
}
|
||||
|
||||
if (theme) {
|
||||
@ -700,6 +1244,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
var storage_viewer = new StorageViewer(document.getElementById("storage-viewer-container"));
|
||||
storage_viewer.init();
|
||||
|
||||
start();
|
||||
|
||||
</script>
|
||||
|
@ -26,7 +26,8 @@ INCBIN(resource_keeper_dashboard_html, SOURCE_DIR "/programs/keeper/dashboard.ht
|
||||
namespace DB
|
||||
{
|
||||
|
||||
void KeeperDashboardWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &)
|
||||
void KeeperDashboardWebUIRequestHandler::handleRequest(
|
||||
HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &)
|
||||
{
|
||||
/// Raw config reference is used here to avoid dependency on Context and ServerSettings.
|
||||
/// This is painful, because this class is also used in a build with CLICKHOUSE_KEEPER_STANDALONE_BUILD=1
|
||||
@ -65,7 +66,8 @@ try
|
||||
|
||||
response_json.set("ch_version", VERSION_DESCRIBE);
|
||||
|
||||
if (keeper_dispatcher->isServerActive()) {
|
||||
if (keeper_dispatcher->isServerActive())
|
||||
{
|
||||
Poco::JSON::Object keeper_details;
|
||||
auto & stats = keeper_dispatcher->getKeeperConnectionStats();
|
||||
Keeper4LWInfo keeper_info = keeper_dispatcher->getKeeper4LWInfo();
|
||||
@ -78,11 +80,11 @@ try
|
||||
response_json.set("keeper_details", keeper_details);
|
||||
}
|
||||
|
||||
std::ostringstream oss; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
|
||||
std::ostringstream oss; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
|
||||
oss.exceptions(std::ios::failbit);
|
||||
Poco::JSON::Stringifier::stringify(response_json, oss);
|
||||
|
||||
response.setContentType("application/json");
|
||||
response.setContentType("application/json");
|
||||
*response.send() << oss.str();
|
||||
}
|
||||
catch (...)
|
||||
|
@ -19,7 +19,7 @@ private:
|
||||
const IServer & server;
|
||||
|
||||
public:
|
||||
explicit KeeperDashboardWebUIRequestHandler(const IServer & server_) : server(server_) {}
|
||||
explicit KeeperDashboardWebUIRequestHandler(const IServer & server_) : server(server_) { }
|
||||
void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override;
|
||||
};
|
||||
|
||||
@ -30,7 +30,10 @@ private:
|
||||
std::shared_ptr<KeeperDispatcher> keeper_dispatcher;
|
||||
|
||||
public:
|
||||
explicit KeeperDashboardContentRequestHandler(std::shared_ptr<KeeperDispatcher> keeper_dispatcher_) : keeper_dispatcher(keeper_dispatcher_) {}
|
||||
explicit KeeperDashboardContentRequestHandler(std::shared_ptr<KeeperDispatcher> keeper_dispatcher_)
|
||||
: keeper_dispatcher(keeper_dispatcher_)
|
||||
{
|
||||
}
|
||||
void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override;
|
||||
};
|
||||
|
||||
|
@ -28,20 +28,26 @@ namespace DB
|
||||
|
||||
namespace ErrorCodes
|
||||
{
|
||||
extern const int INVALID_CONFIG_PARAMETER;
|
||||
extern const int INVALID_CONFIG_PARAMETER;
|
||||
}
|
||||
|
||||
KeeperHTTPRequestHandlerFactory::KeeperHTTPRequestHandlerFactory(const std::string & name_)
|
||||
: log(getLogger(name_)), name(name_)
|
||||
KeeperHTTPRequestHandlerFactory::KeeperHTTPRequestHandlerFactory(const std::string & name_) : log(getLogger(name_)), name(name_)
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<HTTPRequestHandler> KeeperHTTPRequestHandlerFactory::createRequestHandler(const HTTPServerRequest & request)
|
||||
{
|
||||
LOG_TRACE(log, "HTTP Request for {}. Method: {}, Address: {}, User-Agent: {}{}, Content Type: {}, Transfer Encoding: {}, X-Forwarded-For: {}",
|
||||
name, request.getMethod(), request.clientAddress().toString(), request.get("User-Agent", "(none)"),
|
||||
LOG_TRACE(
|
||||
log,
|
||||
"HTTP Request for {}. Method: {}, Address: {}, User-Agent: {}{}, Content Type: {}, Transfer Encoding: {}, X-Forwarded-For: {}",
|
||||
name,
|
||||
request.getMethod(),
|
||||
request.clientAddress().toString(),
|
||||
request.get("User-Agent", "(none)"),
|
||||
(request.hasContentLength() ? (", Length: " + std::to_string(request.getContentLength())) : ("")),
|
||||
request.getContentType(), request.getTransferEncoding(), request.get("X-Forwarded-For", "(none)"));
|
||||
request.getContentType(),
|
||||
request.getTransferEncoding(),
|
||||
request.get("X-Forwarded-For", "(none)"));
|
||||
|
||||
for (auto & handler_factory : child_factories)
|
||||
{
|
||||
@ -50,8 +56,7 @@ std::unique_ptr<HTTPRequestHandler> KeeperHTTPRequestHandlerFactory::createReque
|
||||
return handler;
|
||||
}
|
||||
|
||||
if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET
|
||||
|| request.getMethod() == Poco::Net::HTTPRequest::HTTP_HEAD
|
||||
if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET || request.getMethod() == Poco::Net::HTTPRequest::HTTP_HEAD
|
||||
|| request.getMethod() == Poco::Net::HTTPRequest::HTTP_POST)
|
||||
{
|
||||
return std::unique_ptr<HTTPRequestHandler>(new KeeperNotFoundHandler(hints.getHints(request.getURI())));
|
||||
@ -66,8 +71,7 @@ void addDashboardHandlersToFactory(
|
||||
auto dashboard_ui_creator = [&server]() -> std::unique_ptr<KeeperDashboardWebUIRequestHandler>
|
||||
{ return std::make_unique<KeeperDashboardWebUIRequestHandler>(server); };
|
||||
|
||||
auto dashboard_handler
|
||||
= std::make_shared<HandlingRuleHTTPHandlerFactory<KeeperDashboardWebUIRequestHandler>>(dashboard_ui_creator);
|
||||
auto dashboard_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<KeeperDashboardWebUIRequestHandler>>(dashboard_ui_creator);
|
||||
dashboard_handler->attachStrictPath("/dashboard");
|
||||
dashboard_handler->allowGetAndHeadRequest();
|
||||
factory.addPathToHints("/dashboard");
|
||||
@ -118,9 +122,7 @@ void addDefaultHandlersToFactory(
|
||||
const Poco::Util::AbstractConfiguration & config)
|
||||
{
|
||||
auto readiness_creator = [keeper_dispatcher]() -> std::unique_ptr<KeeperHTTPReadinessHandler>
|
||||
{
|
||||
return std::make_unique<KeeperHTTPReadinessHandler>(keeper_dispatcher);
|
||||
};
|
||||
{ return std::make_unique<KeeperHTTPReadinessHandler>(keeper_dispatcher); };
|
||||
auto readiness_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<KeeperHTTPReadinessHandler>>(std::move(readiness_creator));
|
||||
readiness_handler->attachStrictPath(config.getString("keeper_server.http_control.readiness.endpoint", "/ready"));
|
||||
readiness_handler->allowGetAndHeadRequest();
|
||||
@ -155,28 +157,34 @@ static inline auto createHandlersFactoryFromConfig(
|
||||
const auto & handler_type = config.getString(prefix + "." + key + ".handler.type", "");
|
||||
|
||||
if (handler_type.empty())
|
||||
throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, "Handler type in config is not specified here: "
|
||||
"{}.{}.handler.type", prefix, key);
|
||||
throw Exception(
|
||||
ErrorCodes::INVALID_CONFIG_PARAMETER,
|
||||
"Handler type in config is not specified here: "
|
||||
"{}.{}.handler.type",
|
||||
prefix,
|
||||
key);
|
||||
|
||||
if (handler_type == "dashboard")
|
||||
{
|
||||
addDashboardHandlersToFactory(*main_handler_factory, server, keeper_dispatcher);
|
||||
}
|
||||
if (handler_type == "commands")
|
||||
{
|
||||
addCommandsHandlersToFactory(*main_handler_factory, server, keeper_dispatcher);
|
||||
}
|
||||
if (handler_type == "storage")
|
||||
{
|
||||
addStorageHandlersToFactory(*main_handler_factory, server, keeper_dispatcher);
|
||||
}
|
||||
else
|
||||
throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, "Unknown handler type '{}' in config here: {}.{}.handler.type",
|
||||
handler_type, prefix, key);
|
||||
throw Exception(
|
||||
ErrorCodes::INVALID_CONFIG_PARAMETER,
|
||||
"Unknown handler type '{}' in config here: {}.{}.handler.type",
|
||||
handler_type,
|
||||
prefix,
|
||||
key);
|
||||
}
|
||||
else
|
||||
throw Exception(ErrorCodes::UNKNOWN_ELEMENT_IN_CONFIG, "Unknown element in config: "
|
||||
"{}.{}, must be 'rule' or 'defaults'", prefix, key);
|
||||
throw Exception(
|
||||
ErrorCodes::UNKNOWN_ELEMENT_IN_CONFIG,
|
||||
"Unknown element in config: "
|
||||
"{}.{}, must be 'rule' or 'defaults'",
|
||||
prefix,
|
||||
key);
|
||||
}
|
||||
|
||||
return main_handler_factory;
|
||||
@ -185,10 +193,10 @@ static inline auto createHandlersFactoryFromConfig(
|
||||
KeeperHTTPReadinessHandler::KeeperHTTPReadinessHandler(std::shared_ptr<KeeperDispatcher> keeper_dispatcher_)
|
||||
: log(getLogger("KeeperHTTPReadinessHandler")), keeper_dispatcher(keeper_dispatcher_)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void KeeperHTTPReadinessHandler::handleRequest(HTTPServerRequest & /*request*/, HTTPServerResponse & response, const ProfileEvents::Event & /*write_event*/)
|
||||
void KeeperHTTPReadinessHandler::handleRequest(
|
||||
HTTPServerRequest & /*request*/, HTTPServerResponse & response, const ProfileEvents::Event & /*write_event*/)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -207,7 +215,7 @@ void KeeperHTTPReadinessHandler::handleRequest(HTTPServerRequest & /*request*/,
|
||||
json.set("details", details);
|
||||
json.set("status", status ? "ok" : "fail");
|
||||
|
||||
std::ostringstream oss; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
|
||||
std::ostringstream oss; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
|
||||
oss.exceptions(std::ios::failbit);
|
||||
Poco::JSON::Stringifier::stringify(json, oss);
|
||||
|
||||
@ -245,7 +253,8 @@ KeeperHTTPCommandsHandler::KeeperHTTPCommandsHandler(const IServer & server_, st
|
||||
{
|
||||
}
|
||||
|
||||
void KeeperHTTPCommandsHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & /*write_event*/)
|
||||
void KeeperHTTPCommandsHandler::handleRequest(
|
||||
HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & /*write_event*/)
|
||||
try
|
||||
{
|
||||
std::vector<std::string> uri_segments;
|
||||
@ -261,7 +270,7 @@ try
|
||||
return;
|
||||
}
|
||||
|
||||
// non-strict path "/api/v1/commands" filter is already attached
|
||||
/// non-strict path "/api/v1/commands" filter is already attached
|
||||
if (uri_segments.size() != 4)
|
||||
{
|
||||
response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_BAD_REQUEST, "Invalid command path");
|
||||
@ -339,9 +348,7 @@ HTTPRequestHandlerFactoryPtr createKeeperHTTPHandlerFactory(
|
||||
const std::string & name)
|
||||
{
|
||||
if (config.has("keeper_server.http_control.handlers"))
|
||||
{
|
||||
return createHandlersFactoryFromConfig(server, keeper_dispatcher, config, name, "keeper_server.http_control.handlers");
|
||||
}
|
||||
|
||||
auto factory = std::make_shared<KeeperHTTPRequestHandlerFactory>(name);
|
||||
addDefaultHandlersToFactory(*factory, server, keeper_dispatcher, config);
|
||||
|
@ -24,7 +24,7 @@ extern const int LOGICAL_ERROR;
|
||||
extern const int TIMEOUT_EXCEEDED;
|
||||
}
|
||||
|
||||
Poco::JSON::Object toJson(const Coordination::Stat & stat)
|
||||
Poco::JSON::Object toJSON(const Coordination::Stat & stat)
|
||||
{
|
||||
Poco::JSON::Object result;
|
||||
result.set("cZxid", stat.czxid);
|
||||
@ -173,7 +173,7 @@ void KeeperHTTPStorageHandler::performZooKeeperExistsRequest(const std::string &
|
||||
return;
|
||||
|
||||
Poco::JSON::Object response_json;
|
||||
response_json.set("stat", toJson(exists_result_ptr->stat));
|
||||
response_json.set("stat", toJSON(exists_result_ptr->stat));
|
||||
|
||||
std::ostringstream oss; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
|
||||
oss.exceptions(std::ios::failbit);
|
||||
@ -200,7 +200,7 @@ void KeeperHTTPStorageHandler::performZooKeeperListRequest(const std::string & s
|
||||
|
||||
Poco::JSON::Object response_json;
|
||||
response_json.set("child_node_names", list_result_ptr->names);
|
||||
response_json.set("stat", toJson(list_result_ptr->stat));
|
||||
response_json.set("stat", toJSON(list_result_ptr->stat));
|
||||
|
||||
std::ostringstream oss; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
|
||||
oss.exceptions(std::ios::failbit);
|
||||
|
@ -32,7 +32,8 @@ public:
|
||||
private:
|
||||
Coordination::ResponsePtr awaitKeeperResponse(std::shared_ptr<Coordination::ZooKeeperRequest> request);
|
||||
|
||||
void performZooKeeperRequest(const Coordination::OpNum opnum, const std::string & storage_path, HTTPServerRequest & request, HTTPServerResponse & response);
|
||||
void performZooKeeperRequest(
|
||||
const Coordination::OpNum opnum, const std::string & storage_path, HTTPServerRequest & request, HTTPServerResponse & response);
|
||||
|
||||
void performZooKeeperExistsRequest(const std::string & storage_path, HTTPServerResponse & response);
|
||||
void performZooKeeperListRequest(const std::string & storage_path, HTTPServerResponse & response);
|
||||
|
Loading…
Reference in New Issue
Block a user