diff options
| author | Martin Ridgers <[email protected]> | 2024-10-03 13:54:59 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-10-03 13:54:59 +0200 |
| commit | 61ec0fbdf3f87a41486e5d5dfde5d23e91941b42 (patch) | |
| tree | 66775461556506cd39c84256efe3f119c29fecfc /src/zenserver | |
| parent | cache get command (#183) (diff) | |
| download | zen-61ec0fbdf3f87a41486e5d5dfde5d23e91941b42.tar.xz zen-61ec0fbdf3f87a41486e5d5dfde5d23e91941b42.zip | |
- Improvement: Self-hosted dashboard (#181)
Self-hosted dashboard gets oplog entry view and a stats browser
Diffstat (limited to 'src/zenserver')
| -rw-r--r-- | src/zenserver/frontend/html.zip | bin | 87642 -> 116085 bytes | |||
| -rw-r--r-- | src/zenserver/frontend/html/compactbinary.js | 440 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/index.html | 1 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.css | 41 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.js | 840 |
5 files changed, 1161 insertions, 161 deletions
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip Binary files differindex 11e039bc8..9080591e5 100644 --- a/src/zenserver/frontend/html.zip +++ b/src/zenserver/frontend/html.zip diff --git a/src/zenserver/frontend/html/compactbinary.js b/src/zenserver/frontend/html/compactbinary.js new file mode 100644 index 000000000..c648e6052 --- /dev/null +++ b/src/zenserver/frontend/html/compactbinary.js @@ -0,0 +1,440 @@ +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +class VarInt +{ +} + +//////////////////////////////////////////////////////////////////////////////// +VarInt.measure = function(data_view) +{ + var value = data_view[0]; + var ret = 1; + for (; value & 0x80; value <<= 1, ++ret); + return ret; +} + +//////////////////////////////////////////////////////////////////////////////// +VarInt.read_uint = function(data_view, return_type=Number) +{ + const length = VarInt.measure(data_view); + var value = return_type(data_view[0] & (0xff >> length)); + for (var i = 1; i < length; ++i) + { + value <<= return_type(8); + value |= return_type(data_view[i]); + } + return [value, length]; +} + +//////////////////////////////////////////////////////////////////////////////// +VarInt.read_int = function(data_view, return_type=Number) +{ + var [value, length] = VarInt.read_uint(data_view, return_type); + value = -(value & return_type(1)) ^ (value >> return_type(1)); + return [value, length]; +} + + + +//////////////////////////////////////////////////////////////////////////////// +function cb_assert(expr_result) +{ + if (Boolean(expr_result) == false) + throw Error("compactbinary error"); +} + + + +//////////////////////////////////////////////////////////////////////////////// +const CbFieldType = { + None : 0x00, + Null : 0x01, + Object : 0x02, + UniformObject : 0x03, + Array : 0x04, + UniformArray : 0x05, + Binary : 0x06, + String : 0x07, + IntegerPositive : 0x08, + IntegerNegative : 0x09, + Float32 : 0x0a, + Float64 : 0x0b, + BoolFalse : 0x0c, + BoolTrue : 0x0d, + ObjectAttachment : 0x0e, + BinaryAttachment : 0x0f, + Hash : 0x10, + Uuid : 0x11, + DateTime : 0x12, + TimeSpan : 0x13, + ObjectId : 0x14, + CustomById : 0x1e, + CustomByName : 0x1f, + Reserved : 0x20, + HasFieldType : 0x40, + HasFieldName : 0x80, +} + +//////////////////////////////////////////////////////////////////////////////// +class CbFieldTypeOps +{ + static SerializedTypeMask = 0b10111111; + static TypeMask = 0b00111111; + static ObjectMask = 0b00111110; + static ObjectBase = 0b00000010; + static ArrayMask = 0b00111110; + static ArrayBase = 0b00000100; + static IntegerMask = 0b00111110; + static IntegerBase = 0b00001000; + static FloatMask = 0b00111100; + static FloatBase = 0b00001000; + static BoolMask = 0b00111110; + static BoolBase = 0b00001100; + static AttachmentMask = 0b00111110; + static AttachmentBase = 0b00001110; + + static get_type(type) { return type & CbFieldTypeOps.TypeMask; } + static get_serialized_type(type) { return type & CbFieldTypeOps.SerializedTypeMask; } + static has_field_type(type) { return (type & CbFieldType.HasFieldType) != 0; } + static has_field_name(type) { return (type & CbFieldType.HasFieldName) != 0; } + static is_none(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.None; } + static is_null(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.Null; } + static is_object(type) { return (type & CbFieldTypeOps.ObjectMask) == CbFieldTypeOps.ObjectBase; } + static is_array(type) { return (type & CbFieldTypeOps.ArrayMask) == CbFieldTypeOps.ArrayBase; } + static is_binary(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.Binary; } + static is_string(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.String; } + static is_integer(type) { return (type & CbFieldTypeOps.IntegerMask) == CbFieldTypeOps.IntegerBase; } + static is_float(type) { return (type & CbFieldTypeOps.FloatMask) == CbFieldTypeOps.FloatBase; } + static is_bool(type) { return (type & CbFieldTypeOps.BoolMask) == CbFieldTypeOps.BoolBase; } + static is_object_attachment(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.ObjectAttachment; } + static is_binary_attachment(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.BinaryAttachment; } + static is_attachment(type) { return (type & CbFieldTypeOps.AttachmentMask) == CbFieldTypeOps.AttachmentBase; } + static is_uuid(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.Uuid; } + static is_object_id(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.ObjectId; } + static is_custom_by_id(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.CustomById; } + static is_custom_by_name(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.CustomByName; } + static is_date_time(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.DateTime; } + static is_time_span(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.TimeSpan; } + static is_hash(type) { var t = CbFieldTypeOps.get_type(type); return t >= CbFieldType.ObjectAttachment && t <= CbFieldType.Hash; } + static may_contain_attachments(type){ var t = CbFieldTypeOps.get_type(type); return is_object(t) || is_array(t) || is_attachement(t); } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class CbFieldView +{ + constructor() + { + this._type = CbFieldType.None; + this._name = ""; + this._data_view = undefined; + } +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype._from_field = function(field) +{ + this._type = field._type; + this._name = field._name; + this._data_view = field._data_view; + return this; +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype._from_data = function(data_view, type=CbFieldType.HasFieldType) +{ + if (CbFieldTypeOps.has_field_type(type)) + { + type = data_view[0] | CbFieldType.HasFieldType; + data_view = data_view.subarray(1); + } + + if (CbFieldTypeOps.has_field_name(type)) + { + const [n, varint_len] = VarInt.read_uint(data_view); + this._name = new TextDecoder().decode(data_view.subarray(varint_len, n + varint_len)); + data_view = data_view.subarray(n + varint_len); + } + + this._type = type; + this._data_view = data_view; + return this; +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView._iterate = function*(data_view, uniform_type) +{ + while (data_view.length > 0) + { + const field = new CbFieldView()._from_data(data_view, uniform_type); + yield field; + + const field_size = field.get_payload_size(); + cb_assert(field_size <= data_view.length); + data_view = field.get_payload().subarray(field_size); + } +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype.get_type = function() +{ + return this._type; +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype.get_name = function() +{ + return this._name; +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype.get_payload = function() +{ + return this._data_view; +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype.get_payload_size = function() +{ + switch (CbFieldTypeOps.get_type(this.get_type())) + { + case CbFieldType.None: + case CbFieldType.Null: + return 0; + case CbFieldType.Object: + case CbFieldType.UniformObject: + case CbFieldType.Array: + case CbFieldType.UniformArray: + case CbFieldType.Binary: + case CbFieldType.String: + const [value, varint_len] = VarInt.read_uint(this._data_view); + return value + varint_len; + case CbFieldType.IntegerPositive: + case CbFieldType.IntegerNegative: + return VarInt.measure(this._data_view); + case CbFieldType.Float32: + return 4; + case CbFieldType.Float64: + return 8; + case CbFieldType.BoolFalse: + case CbFieldType.BoolTrue: + return 0; + case CbFieldType.ObjectAttachment: + case CbFieldType.BinaryAttachment: + case CbFieldType.Hash: + return 20; + case CbFieldType.Uuid: + return 16; + case CbFieldType.ObjectId: + return 12; + case CbFieldType.DateTime: + case CbFieldType.TimeSpan: + return 8; + } + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype._is = function(func) { return func(this.get_type()); } +CbFieldView.prototype.is_null = function() { return this._is(CbFieldTypeOps.is_null); } +CbFieldView.prototype.is_object = function() { return this._is(CbFieldTypeOps.is_object); } +CbFieldView.prototype.is_array = function() { return this._is(CbFieldTypeOps.is_array); } +CbFieldView.prototype.is_binary = function() { return this._is(CbFieldTypeOps.is_binary); } +CbFieldView.prototype.is_string = function() { return this._is(CbFieldTypeOps.is_string); } +CbFieldView.prototype.is_integer = function() { return this._is(CbFieldTypeOps.is_integer); } +CbFieldView.prototype.is_float = function() { return this._is(CbFieldTypeOps.is_float); } +CbFieldView.prototype.is_bool = function() { return this._is(CbFieldTypeOps.is_bool); } +CbFieldView.prototype.is_object_attachment = function() { return this._is(CbFieldTypeOps.is_object_attachment); } +CbFieldView.prototype.is_binary_attachment = function() { return this._is(CbFieldTypeOps.is_binary_attachment); } +CbFieldView.prototype.is_attachment = function() { return this._is(CbFieldTypeOps.is_attachment); } +CbFieldView.prototype.is_hash = function() { return this._is(CbFieldTypeOps.is_hash); } +CbFieldView.prototype.is_uuid = function() { return this._is(CbFieldTypeOps.is_uuid); } +CbFieldView.prototype.is_object_id = function() { return this._is(CbFieldTypeOps.is_object_id); } +CbFieldView.prototype.is_date_time = function() { return this._is(CbFieldTypeOps.is_date_time); } +CbFieldView.prototype.is_time_span = function() { return this._is(CbFieldTypeOps.is_time_span); } + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype.as_object = function() +{ + cb_assert(CbFieldTypeOps.is_object(this.get_type())); + return new CbObjectView()._from_field(this); +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype.as_array = function() +{ + cb_assert(CbFieldTypeOps.is_array(this.get_type())); + return new CbArrayView()._from_field(this); +} + +//////////////////////////////////////////////////////////////////////////////// +CbFieldView.prototype.as_value = function(int_type=Number) +{ + switch (CbFieldTypeOps.get_type(this.get_type())) + { + case CbFieldType.None: return undefined; + case CbFieldType.Null: return null; + + case CbFieldType.Object: + case CbFieldType.UniformObject: return this.as_object(); + + case CbFieldType.Array: + case CbFieldType.UniformArray: return this.as_array(); + + case CbFieldType.Binary: { + const [n, vn] = VarInt.read_uint(this._data_view); + return this._data_view.subarray(vn, n + vn); + } + + case CbFieldType.String: { + const [n, vn] = VarInt.read_uint(this._data_view); + return new TextDecoder().decode(this._data_view.subarray(vn, n + vn)); + } + + case CbFieldType.IntegerPositive: return VarInt.read_uint(this._data_view, int_type)[0]; + case CbFieldType.IntegerNegative: return VarInt.read_int(this._data_view, int_type)[0]; + + case CbFieldType.Float32: return new DataView(this._data_view.subarray(0, 4)).getFloat32(0, false); + case CbFieldType.Float64: return new DataView(this._data_view.subarray(0, 8)).getFloat64(0, false); + case CbFieldType.BoolFalse: return false; + case CbFieldType.BoolTrue: return true; + + case CbFieldType.ObjectAttachment: + case CbFieldType.BinaryAttachment: + case CbFieldType.Hash: return this._data_view.subarray(0, 20); + + case CbFieldType.Uuid: return this._data_view.subarray(0, 16); + case CbFieldType.ObjectId: return this._data_view.subarray(0, 12); + + case CbFieldType.DateTime: + case CbFieldType.TimeSpan: return this._data_view.subarray(0, 8); + } + + cb_assert(false); +} + + + +//////////////////////////////////////////////////////////////////////////////// +class CbObjectView extends CbFieldView +{ +} + +//////////////////////////////////////////////////////////////////////////////// +CbObjectView.prototype[Symbol.iterator] = function() +{ + var data_view = this.get_payload(); + + const [payload_size, varint_len] = VarInt.read_uint(data_view); + if (payload_size == 0) + return {}; + data_view = data_view.subarray(varint_len, payload_size + varint_len); + + var uniform_type = CbFieldType.HasFieldType; + if (CbFieldTypeOps.get_type(this.get_type()) == CbFieldType.UniformObject) + { + uniform_type = data_view[0]; + data_view = data_view.subarray(1); + } + + return CbFieldView._iterate(data_view, uniform_type); +} + +//////////////////////////////////////////////////////////////////////////////// +CbObjectView.prototype.to_js_object = function() +{ + const impl = function(node) + { + if (node.is_object()) + { + const ret = {}; + for (var item of node.as_object()) + ret[item.get_name()] = impl(item); + return ret; + } + + if (node.is_array()) + { + const ret = []; + for (var item of node.as_array()) + ret.push(impl(item)); + return ret; + } + + if (node.is_string()) return node.as_value(); + if (node.is_float()) return node.as_value(); + if (node.is_integer()) return node.as_value(); + + var ret = node.as_value(); + if (ret instanceof Uint8Array) + { + ret = ""; + for (var x of node.as_value()) + ret += x.toString(16).padStart(2, "0"); + } + return ret; + }; + + return impl(this); +} + +//////////////////////////////////////////////////////////////////////////////// +CbObjectView.prototype.find = function(name) +{ + for (const field of this) + if (field.get_name() == name) + return field; +} + + + +//////////////////////////////////////////////////////////////////////////////// +class CbArrayView extends CbFieldView +{ +} + +//////////////////////////////////////////////////////////////////////////////// +CbArrayView.prototype[Symbol.iterator] = function() +{ + var data_view = this.get_payload(); + + const [payload_size, varint_len] = VarInt.read_uint(data_view); + data_view = data_view.subarray(varint_len, payload_size + varint_len); + + const item_count_bytes = VarInt.measure(data_view); + if (item_count_bytes >= payload_size) + return {}; + data_view = data_view.subarray(item_count_bytes); + + var uniform_type = CbFieldType.HasFieldType; + if (CbFieldTypeOps.get_type(this.get_type()) == CbFieldType.UniformArray) + { + uniform_type = data_view[0]; + data_view = data_view.subarray(1); + } + + return CbFieldView._iterate(data_view, uniform_type); +} + +//////////////////////////////////////////////////////////////////////////////// +CbArrayView.prototype.num = function() +{ + var data_view = this._data_view; + const [n, n_len] = VarInt.read_uint(data_view); + data_view = data_view.subarray(n_len); + return VarInt.read_uint(data_view)[0]; +} + + + +//////////////////////////////////////////////////////////////////////////////// +class CbObject extends CbFieldView +{ + constructor(uint8_array) + { + super(); + this._from_data(uint8_array); + } +} diff --git a/src/zenserver/frontend/html/index.html b/src/zenserver/frontend/html/index.html index 8bc690c22..300b8c62e 100644 --- a/src/zenserver/frontend/html/index.html +++ b/src/zenserver/frontend/html/index.html @@ -4,6 +4,7 @@ <head> <link rel="shortcut icon" href="favicon.ico"> <link rel="stylesheet" type="text/css" href="zen.css" /> + <script src="compactbinary.js"></script> <script src="zen.js"></script> </head> <body onload="main()"> diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css index 9e6eb8f78..d9b983ff6 100644 --- a/src/zenserver/frontend/html/zen.css +++ b/src/zenserver/frontend/html/zen.css @@ -27,6 +27,10 @@ body { background-color: var(--theme_g4); } +pre { + margin: 0; +} + * { box-sizing: border-box; } @@ -93,25 +97,33 @@ h3 { margin-bottom: 1.2em; } -.zen_table > .zen_row { +.zen_table > div { display: contents; } -.zen_table > .zen_row:nth-child(odd) > .zen_cell { +.zen_table > div:nth-of-type(odd) { background-color: var(--theme_g3); } -.zen_table > .zen_row:first-child > .zen_cell { +.zen_table > div:first-of-type { font-weight: bold; background-color: var(--theme_p3); } -.zen_table > .zen_row > .zen_cell { +.zen_table > hidden { + visibility: hidden; + display: none; +} + +.zen_table > div > div { padding: 0.3em; padding-left: 0.75em; padding-right: 0.75em; align-content: center; border-left: 1px solid var(--theme_g2); + overflow: auto; + overflow-wrap: break-word; + background-color: inherit; } /* toolbar ------------------------------------------------------------------ */ @@ -159,6 +171,7 @@ h3 { display: flex; justify-content: center; align-items: center; + backdrop-filter: blur(5px); } .zen_modal .zen_modal_bg { @@ -169,7 +182,7 @@ h3 { width: 100%; height: 100%; background: var(--theme_g0); - opacity: 0.5; + opacity: 0.4; } .zen_modal > div { @@ -217,6 +230,24 @@ h3 { align-content: center; } +/* crumbs ------------------------------------------------------------------- */ + +#crumbs { + display: flex; + position: relative; + top: -1em; +} + +#crumbs > div { + padding-right: 0.5em; +} + +#crumbs > div:nth-child(odd)::after { + content: ":"; + font-weight: bolder; + color: var(--theme_p2); +} + /* branding ----------------------------------------------------------------- */ #branding { diff --git a/src/zenserver/frontend/html/zen.js b/src/zenserver/frontend/html/zen.js index 6bd74de0c..3fac5f312 100644 --- a/src/zenserver/frontend/html/zen.js +++ b/src/zenserver/frontend/html/zen.js @@ -1,10 +1,35 @@ +"use strict"; + //////////////////////////////////////////////////////////////////////////////// -class Component +class Friendly +{ + static sep(value, prec=0) + { + return value.toLocaleString("en", { + style: "decimal", + minimumFractionDigits : prec, + maximumFractionDigits : prec, + }); + } + + static k(x) { return Friendly.sep((x + 999) / Math.pow(10, 3), 0) + "K"; } + static m(x) { return Friendly.sep( x / Math.pow(10, 6), 1) + "M"; } + static g(x) { return Friendly.sep( x / Math.pow(10, 6), 2) + "G"; } + static kib(x) { return Friendly.sep((x + 1023) / (1 << 10), 0) + " KiB"; } + static mib(x) { return Friendly.sep( x / (1 << 20), 1) + " MiB"; } + static gib(x) { return Friendly.sep( x / (1 << 30), 2) + " GiB"; } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class ComponentBase { constructor(element) { - if (element instanceof Component) + if (element instanceof ComponentBase) element = element._element; + this._element = element; } @@ -17,17 +42,33 @@ class Component { this._element.parentNode.removeChild(this._element); } +} +//////////////////////////////////////////////////////////////////////////////// +class ComponentDom extends ComponentBase +{ tag(tag="div") { var element = document.createElement(tag); this._element.appendChild(element); - return new Component(element); + return this.new_component(element); + } + + retag(new_tag) + { + if (this._element.tagName == new_tag.toUpperCase()) + return this; + + var element = document.createElement(new_tag); + element.innerHTML = this._element.innerHTML; + this._element.parentNode.replaceChild(element, this._element); + this._element = element; + return this; } text(value) { - value = String(value); + value = value.toString(); this._element.innerHTML = (value != "") ? value : ""; return this; } @@ -40,10 +81,7 @@ class Component classify(value) { - var cur = this._element.className; - cur += cur ? " " : ""; - cur += value; - this._element.className = cur; + this._element.classList.add(value); return this; } @@ -58,7 +96,11 @@ class Component this._element.setAttribute(key, value); return this; } +} +//////////////////////////////////////////////////////////////////////////////// +class ComponentInteract extends ComponentDom +{ link(resource=undefined, query_params={}) { if (resource != undefined) @@ -80,31 +122,71 @@ class Component return this; } - on(what, func) + on(what, func, ...args) { const thunk = (src) => { if (src.target != this._element) return; - func(src.target); + func(...args); src.stopPropagation(); }; + this._element.addEventListener(what, thunk); return this; } - on_click(func) + on_click(func, ...args) { this.classify("zen_action"); - return this.on("click", func); + return this.on("click", func, ...args); + } +} + +//////////////////////////////////////////////////////////////////////////////// +class Component extends ComponentInteract +{ + new_component(...args) + { + return new Component(...args); } } //////////////////////////////////////////////////////////////////////////////// -class Cell extends Component +class TableCell extends Component { + constructor(element, row) + { + super(element); + this._row = row + } + + get_table() { return this.get_row().get_table(); } + get_row() { return this._row; } +} + +//////////////////////////////////////////////////////////////////////////////// +class TableRow extends Component +{ + constructor(element, table, index, cells) + { + super(element); + this._table = table; + this._index = index; + this._cells = cells; + } + + *[Symbol.iterator]() + { + for (var cell of this._cells) + yield cell; + } + + get_table() { return this._table; } + get_index() { return this._index; } + get_cell(index) { return this._cells.at(index); } } //////////////////////////////////////////////////////////////////////////////// @@ -123,35 +205,63 @@ class Table extends Component root.css_var("zen_columns", this._num_columns); this._num_columns = column_names.length; - this.add_row(column_names, false); + this._add_row(column_names, false); + + this._rows = []; + } + + *[Symbol.iterator]() + { + for (var row of this._rows) + yield row; } - add_row(cells, indexed=true) + get_row(index) { + return this._rows.at(index); + } + + _add_row(cells, indexed=true) + { + var index = -1; if (indexed && this._index >= 0) - cells = [this._index++, ...cells]; + { + index = this._index++; + cells = [index, ...cells]; + } cells = cells.slice(0, this._num_columns); while (cells.length < this._num_columns) cells.push(""); var ret = []; - var row = this.tag().classify("zen_row"); + var row = this.tag() + row = new TableRow(row, this, index, ret); for (const cell of cells) { - var leaf = row.tag().classify("zen_cell").text(cell); - ret.push(new Cell(leaf)); + var leaf = row.tag().text(cell); + ret.push(new TableCell(leaf, row)); } - var bias = (this._index >= 0) ? 1 : 0; - return ret.slice(bias); + if (this._index >= 0) + ret.shift(); + + return row + } + + add_row(...args) + { + var row = this._add_row(...args); + this._rows.push(row); + return row; } clear(index=0) { const elem = this._element; elem.replaceChildren(elem.firstElementChild); - this._index = index; + this._index = (this._index >= 0) ? index : -1; + this._rows = []; } } @@ -163,14 +273,62 @@ class PropTable extends Table constructor(parent) { super(parent, ["prop", "value"], -1); + this.classify("zen_proptable"); } add_property(key, value) { - var ret = this.add_row([key, value]); - ret[0].classify("zen_prop_key"); - ret[1].classify("zen_prop_value"); - return ret; + return this.add_row([key, value]); + } + + add_object(object, friendly=false, prec=2) + { + const impl = (node, prefix="") => { + for (const key in node) + { + var value = node[key]; + if (value instanceof Object && + (value.constructor.name == "Object" || + value.constructor.name == "Array")) + { + impl(value, prefix + key + "."); + continue; + } + + if (friendly && typeof value == "number") + { + if (key.indexOf("memory") >= 0) value = Friendly.kib(value); + else if (key.indexOf("disk") >= 0) value = Friendly.kib(value); + else if (value > 100000) value = Friendly.k(value); + else if (value % 1) value = Friendly.sep(value, 3); + else value = Friendly.sep(value, 0); + } + + this.add_property(prefix + key, value); + } + }; + + return impl(object); + } + + filter(...needles) + { + for (var row of this) + row.retag("div") + + if (needles.length == 0) + return; + + for (var row of this) + { + var hide = false; + var cell = row.get_cell(0); + for (var needle of needles) + hide = hide || (cell.inner().innerHTML.indexOf(needle) < 0); + + if (hide) + row.retag("hidden"); + } } } @@ -205,11 +363,11 @@ class Modal return this; } - option(name, func) + option(name, func, ...args) { - const thunk = (src) => { + const thunk = () => { this._root.destroy(); - func(src); + func(...args); }; this._buttons.tag().text(name).on("click", thunk); return this; @@ -268,35 +426,74 @@ class Sectormatron extends Component //////////////////////////////////////////////////////////////////////////////// -async function zen_fetch(resource, method="GET") +class Fetcher { - var response = await fetch(resource, { - "method" : method, - "headers" : { "Accept": "application/json" }, - }); + constructor() + { + this._resource = ""; + this._query = {}; + } - return await response.json(); -} + resource(...parts) + { + var value = parts.join("/"); + if (!value.startsWith("/")) + value= "/" + value; + this._resource = value; + return this; + } -//////////////////////////////////////////////////////////////////////////////// -function zen_flatten(object, ret=null, prefix="") -{ - if (ret == null) - ret = new Object(); + param(name, value) + { + this._query[name] = value; + return this; + } + + async json() + { + const response = await this._get("application/json"); + return response ? (await response.json()) : {}; + } - for (const key in object) + async cbo() { - const value = object[key]; - if (value instanceof Object) + const response = await this._get("application/x-ue-cb"); + if (!response) + return null; + + const buffer = await response.arrayBuffer(); + const data = new Uint8Array(buffer); + return new CbObject(data); + } + + async delete() + { + const resource = this._build_uri(); + const response = await fetch(resource, { "method" : "DELETE" }); + } + + _build_uri() + { + var suffix = ""; + for (var key in this._query) { - zen_flatten(value, ret, key + "."); - continue; + suffix += suffix ? "&" : "?"; + suffix += key + "=" + this._query[key]; } - - ret[prefix + key] = value; + return this._resource + suffix; } - return ret; + async _get(accept="*") + { + const resource = this._build_uri(); + const response = await fetch(resource, { + "method" : "GET", + "headers" : { "Accept": accept }, + }); + + if (response.status >= 200 || response.status <= 299) + return response; + } } @@ -313,25 +510,102 @@ class Page set_title(name) { - document.title = "zen - " + name; + var value = document.title; + if (name.length && value.length) + name = value + " - " + name; + document.title = name; } get_param(name, fallback=undefined) { var ret = this._params.get(name); - return (ret != undefined) ? ret : fallback; + if (ret != undefined) + return ret; + + if (fallback != undefined) + this.set_param(name, fallback); + + return fallback; + } + + set_param(name, value, update=true) + { + this._params.set(name, value); + if (!update) + return value; + + const url = new URL(window.location); + for (var [key, xfer] of this._params) + url.searchParams.set(key, xfer) + history.replaceState(null, "", url); + + return value; } add_section(name) { return this._sectormatron.add_section(name); } + + reload() + { + window.location.reload(); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class ZenPage extends Page +{ + constructor(...args) + { + super(...args); + super.set_title("zen"); + this.generate_crumbs(); + } + + set_title(...args) + { + super.set_title(...args); + } + + generate_crumbs() + { + const auto_name = this.constructor.name; + if (auto_name == "Start") + return; + + const crumbs = this._parent.tag().id("crumbs"); + const new_crumb = function(name, search=undefined) { + crumbs.tag(); + var crumb = crumbs.tag().text(name); + if (search != undefined) + crumb.on_click((x) => window.location.search = x, search); + }; + + new_crumb("home", ""); + + var project = this.get_param("project"); + if (project != undefined) + { + var oplog = this.get_param("oplog"); + if (oplog != undefined) + { + new_crumb("project", `?page=project&project=${project}`); + if (this.get_param("opkey")) + new_crumb("oplog", `?page=oplog&project=${project}&oplog=${oplog}`); + } + } + + new_crumb(auto_name.toLowerCase()); + } } //////////////////////////////////////////////////////////////////////////////// -class Entry extends Page +class Entry extends ZenPage { async main() { @@ -339,36 +613,102 @@ class Entry extends Page const project = this.get_param("project"); const oplog = this.get_param("oplog"); - const key = this.get_param("key"); - const uri = `/prj/${project}/oplog/${oplog}/entries?opkey=${key}`; + const opkey = this.get_param("opkey"); + + var entry = new Fetcher() + .resource("prj", project, "oplog", oplog, "entries") + .param("opkey", opkey) + .cbo() + entry = await entry; + entry = entry.as_object().find("entry").as_object(); + + const name = entry.find("key").as_value(); + var section = this.add_section(name); - var entry = await zen_fetch(uri); - var text = JSON.stringify(entry, null, 2); - this._parent.tag("pre").text(text); + // tree + { + var tree = entry.find("$tree"); + if (tree == undefined) + tree = this._convert_legacy_to_tree(entry); + + delete tree["$id"]; + + const sub_section = section.add_section("deps"); + for (const dep_name in tree) + { + const dep_section = sub_section.add_section(dep_name); + const table = new Table(dep_section, ["id", "name"]); + for (const dep_id of tree[dep_name]) + { + const cell_values = [dep_id.toString(16)]; + const row = table.add_row(cell_values); + row.get_cell(0).on_click(() => void(0)); + } + } + } + + // props + { + const object = entry.to_js_object(); + var sub_section = section.add_section("props"); + new PropTable(sub_section).add_object(object); + } + } + + _convert_legacy_to_tree(entry) + { + const tree = {}; + + var id = 0n; + for (var item of entry.find("packagedata").as_array()) + { + var pkg_id = item.as_object().find("id"); + if (pkg_id == undefined) + continue; + + for (var x of pkg_id.as_value().subarray(0, 8)) + { + id <<= 8n; + id |= BigInt(x); + } + break; + } + tree["$id"] = id; + + const pkgst_entry = entry.find("packagestoreentry").as_object(); + const imported = pkgst_entry.find("importedpackageids"); + if (imported == undefined) + return tree; + + var out = tree["imported"] = []; + for (var item of imported.as_array()) + out.push(item.as_value(BigInt)); + + return tree; } } //////////////////////////////////////////////////////////////////////////////// -class Oplog extends Page +class Oplog extends ZenPage { constructor(...args) { super(...args); - this._index_start = this.get_param("start", 0); - this._index_count = this.get_param("start", 50); + this._index_start = Number(this.get_param("start", 0)); + this._index_count = Number(this.get_param("count", 50)); this._entry_table = undefined; } async main() { - this.set_title("oplog"); - const project = this.get_param("project"); const oplog = this.get_param("oplog"); + this.set_title("oplog - " + oplog); + var section = this.add_section(project + " - " + oplog); this._build_nav(section) @@ -380,17 +720,19 @@ class Oplog extends Page _build_nav(section) { var nav = new Toolbar(section); - nav.left().add("prev").on_click(() => this._on_next_prev(-1)); - nav.left().add("next").on_click(() => this._on_next_prev( 1)); + nav.left().add("<<").on_click(() => this._on_next_prev(-10)); + nav.left().add("prev") .on_click(() => this._on_next_prev( -1)); + nav.left().add("next") .on_click(() => this._on_next_prev( 1)); + nav.left().add(">>").on_click(() => this._on_next_prev( 10)); nav.left().sep(); for (var count of [10, 25, 50, 100]) { - var handler = (e) => this._on_change_count(e.innerHTML); - nav.left().add(count).on_click(handler); + var handler = (n) => this._on_change_count(n); + nav.left().add(count).on_click(handler, count); } - nav.right().add("search:", "span"); + nav.right().add("search:", "label"); nav.right().add("", "input").attr("disabled", ""); } @@ -399,25 +741,29 @@ class Oplog extends Page const project = this.get_param("project"); const oplog = this.get_param("oplog"); - var uri = `/prj/${project}/oplog/${oplog}/entries`; - uri += "?start=" + this._index_start; - uri += "&count=" + this._index_count; - - var entries = zen_fetch(uri); + var entries = new Fetcher() + .resource("prj", project, "oplog", oplog, "entries") + .param("start", this.set_param("start", this._index_start)) + .param("count", this.set_param("count", this._index_count)) + .json(); this._entry_table.clear(this._index_start); - for (const entry of (await entries)["entries"]) + entries = (await entries)["entries"]; + if (entries == undefined) + return + + for (const entry of entries) { - var cells = this._entry_table.add_row([ + var row = this._entry_table.add_row([ entry["key"] ]); - cells[0].link("", { + row.get_cell(0).link("", { "page" : "entry", "project" : project, "oplog" : oplog, - "key" : entry["key"], + "opkey" : entry["key"], }); } } @@ -439,19 +785,18 @@ class Oplog extends Page //////////////////////////////////////////////////////////////////////////////// -class Project extends Page +class Project extends ZenPage { async main() { - this.set_title("project"); - // info var section = this.add_section("info"); const project = this.get_param("project"); - const prefix = "/prj/" + project; - var info = await zen_fetch(prefix); + this.set_title("project - " + project); + + var info = await new Fetcher().resource("prj", project).json(); var prop_table = new PropTable(section); for (const key in info) { @@ -471,73 +816,44 @@ class Project extends Page { const name = oplog["id"]; - var cells = oplog_table.add_row([ + var row = oplog_table.add_row([ name, ]); - var cell = cells[0]; + var cell = row.get_cell(0); cell.link("", { "page" : "oplog", "project" : project, "oplog" : name, }); - var action_tb = new Toolbar(cells.at(-1), true); - action_tb.left().add("drop").attr("zen_param", name).on_click((e) => this._on_drop(e)); - } - - // files - /* - section = this.add_section("files"); - for (const oplog of info["oplogs"]) - { - const name = oplog["id"]; - - var files = await zen_fetch(prefix + "/oplog/" + name + "/files"); - if (files["files"].length == 0) - continue; - - var sub_section = section.add_section(name); - var table = new Table(sub_section, [ - "id", - "clientpath", - "serverpath", - ]); - var count = 0; - for (const file of files["files"]) - { - table.add_row([ - file["id"], - file["clientpath"], - file["serverpath"], - ]); - - if (++count > 10) - break; - } + cell = row.get_cell(-1); + var action_tb = new Toolbar(cell, true); + action_tb.left().add("drop").on_click((x) => this.drop_oplog(x), name); } - */ } - drop() + drop_oplog(oplog_id) { - alert("\\o/"); - } + const drop = async () => { + await new Fetcher() + .resource("prj", this.get_param("project"), "oplog", oplog_id) + .delete(); + this.reload(); + }; - _on_drop(e) - { new Modal() .title("Confirmation") - .message(`Drop oplog '${e.getAttribute("zen_param")}'?`) - .option("Yes", () => this.drop()) - .option("No", () => void(0)) + .message(`Drop oplog '${oplog_id}'?`) + .option("Yes", () => drop()) + .option("No") } } //////////////////////////////////////////////////////////////////////////////// -class Test extends Page +class Test extends ZenPage { main() { @@ -613,27 +929,57 @@ class Test extends Page side.add("tb_item2"); } - var cell = prop_table.add_property("toolbar", ""); - toolbar = new Toolbar(cell[1], true); - toolbar.left().add("tbitem0"); - toolbar.left().add("tbitem1"); - toolbar.right().add("tbitem2"); - toolbar.right().add("tbitem3"); + var tb_item_clicked = function(arg0, arg1) { + alert(arg0 + " != " + arg1); + }; + var row = prop_table.add_property("toolbar", ""); + toolbar = new Toolbar(row.get_cell(-1), true); + toolbar.left() .add("tbitem0").on_click(tb_item_clicked, 11, -22); + toolbar.left() .add("tbitem1").on_click(tb_item_clicked, 22, -33); + toolbar.right().add("tbitem2").on_click(tb_item_clicked, 33, -55); + toolbar.right().add("tbitem3").on_click(tb_item_clicked, 44, -88); + + this._cbo_test(); // error throw Error("deliberate error"); } + + async _cbo_test() + { + var data = new Uint8Array(await (await fetch("/prj/list")).arrayBuffer()); + for (var item of new CbObject(data).as_array()) + { + for (var subitem of item.as_object()) + { + console.log(subitem.get_name(), subitem.as_value()); + } + } + + data = new Uint8Array(await (await fetch("/stats")).arrayBuffer()); + { + var item = new CbObject(data).as_object().find("providers").as_array(); + console.log(item.num()); + for (var subitem of item) + { + console.log(subitem.as_value()); + data = new Uint8Array(await (await fetch("/stats/" + subitem.as_value())).arrayBuffer()); + for (var ssitem of new CbObject(data).as_object()) + { + console.log(ssitem.get_name(), ssitem.as_value()); + } + } + } + } } //////////////////////////////////////////////////////////////////////////////// -class Start extends Page +class Start extends ZenPage { async main() { - this.set_title("main"); - var section = this.add_section("projects"); // project list @@ -645,43 +991,224 @@ class Start extends Page ]; var table = new Table(section, columns); - for (const project of await zen_fetch("/prj/list")) + for (const project of await new Fetcher().resource("/prj/list").json()) { - var cells = table.add_row([ - project.Id, + var row = table.add_row([ + "", project.ProjectRootDir, project.EngineRootDir, ]); - cells[0].link("", {"page" : "project", "project" : project.Id}); + var cell = row.get_cell(0); + cell.tag().text(project.Id).on_click((x) => this.view_project(x), project.Id); - var action_tb = new Toolbar(cells.at(-1), true); - action_tb.left().add("view").on_click(() => void(0)); - action_tb.left().add("drop").on_click(() => this.on_drop()); + var cell = row.get_cell(-1); + var action_tb = new Toolbar(cell, true); + action_tb.left().add("view").on_click((x) => this.view_project(x), project.Id); + action_tb.left().add("drop").on_click((x) => this.drop_project(x), project.Id); } // stats section = this.add_section("stats"); - var providers = zen_fetch("/stats"); + columns = [ + "name", + "req count", + "size disk", + "size mem", + "cid total", + ]; + const stats_table = new Table(section, columns); + var providers = new Fetcher().resource("stats").json(); for (var provider of (await providers)["providers"]) { - var stats = zen_fetch("/stats/" + provider); + var stats = await new Fetcher().resource("stats", provider).json(); + var values = [""]; + try { + values.push(stats.requests.count); + const size_stat = (stats.store || stats.cache).size; + values.push(Friendly.kib(size_stat.disk)); + values.push(Friendly.kib(size_stat.memory)); + values.push(stats.cid.size.total); + } + catch {} + row = stats_table.add_row(values); + row.get_cell(0).tag().text(provider).on_click((x) => this.view_stat(x), provider); + } + } - var section_provider = section.add_section(provider); - var table = new PropTable(section_provider); + view_stat(provider) + { + window.location = "?page=stat&provider=" + provider; + } - stats = zen_flatten(await stats); - for (const key in stats) - table.add_property(key, stats[key]); + view_project(project_id) + { + window.location = "?page=project&project=" + project_id; + } + + drop_project(project_id) + { + const drop = async () => { + await new Fetcher().resource("prj", project_id).delete(); + this.reload(); + }; + + new Modal() + .title("Confirmation") + .message(`Drop project '${project_id}'?`) + .option("Yes", () => drop()) + .option("No"); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Stat extends ZenPage +{ + static TemporalStat = class + { + constructor(data) + { + this._data = data; } + + toString() + { + const columns = [ + [], + ["rate;"], [], + ["t;"], [], [], + ]; + const data = this._data; + for (var key in data) + { + var value = key + ": " + Friendly.sep(data[key], 2); + value = value.replace("rate_", ""); + value = value.replace("t_", ""); + + if (key.startsWith("rate_")) columns[2].push(value); + else if (key.startsWith("t_p")) columns[5].push(value); + else if (key.startsWith("t_")) columns[4].push(value); + else columns[0].push(Friendly.sep(data[key])); + } + + var line_count = 0 + for (var column of columns) + line_count = Math.max(line_count, column.length); + + const widths = [13, 5, 19, 2, 23, -1]; + + var content = ""; + for (var i = 0; i < line_count; ++i) + { + for (var j in columns) + { + const column = columns[j]; + var cell = (column.length > i) ? column[i] : ""; + var lead = cell.indexOf(":"); + if (lead >= 0) + cell = " ".repeat(7 - lead) + cell; + if (widths[j] > 0) + cell = cell.padEnd(widths[j]); + content += cell; + } + content += "\n"; + } + + return "<pre>" + content + "</pre>"; + } + } + + async main() + { + const provider = this.get_param("provider", "z$"); + var stats = new Fetcher() + .resource("stats", provider) + .param("cidstorestats", "true") + .param("cachestorestats", "true") + .json(); + + this.set_title("stat - " + provider); + const section = this.add_section(provider); + + var toolbar = new Toolbar(section); + var tb_right = toolbar.right(); + tb_right.add("filter:"); + tb_right.add("-none-").on_click((x) => this.update_filter("")); + for (var preset of ["read.", "write.", ".request", ".bytes"]) + tb_right.add(preset).on_click((x) => this.update_filter(x), preset); + { + var input = tb_right.add("", "input") + input.on("change", (x) => this.update_filter(x.inner().value), input); + } + + this._table = new PropTable(section); + + this._stats = stats = await stats; + this._condense(stats); + + var first = undefined; + for (var name in stats) + { + first = first || name; + toolbar.left().add(name).on_click((x) => this.view_category(x), name); + } + + first = this.get_param("view", first); + this.view_category(first); + + var filter = this.get_param("filter"); + if (filter) + this.update_filter(filter); } - on_drop() + view_category(name) { - new Modal().title("Confirmation").message("TODO").option("Okay"); + const friendly = (this.get_param("raw") == undefined); + this._table.clear(); + this._table.add_object(this._stats[name], friendly, 3); + this.set_param("view", name); + this.set_param("filter", ""); + } + + update_filter(needle) + { + this.set_param("filter", needle); + if (!needle) + return this._table.filter(); + + var needles = needle.split(" "); + this._table.filter(...needles); + } + + _condense(stats) + { + const impl = function(node) + { + for (var name in node) + { + const candidate = node[name]; + if (!(candidate instanceof Object)) + continue; + + if (candidate["rate_mean"] != undefined) + { + node[name] = new Stat.TemporalStat(candidate); + continue; + } + + impl(candidate); + } + } + + for (var name in stats) + impl(stats[name]); } } + + //////////////////////////////////////////////////////////////////////////////// function add_branding(parent) { @@ -709,11 +1236,12 @@ async function main_guarded() const page = params.get("page"); var impl = undefined; - if (page == "project") impl = new Project(root, params); - if (page == "oplog") impl = new Oplog(root, params); - if (page == "entry") impl = new Entry(root, params); - if (page == "test") impl = new Test(root, params); - if (page == undefined) impl = new Start(root, params); + if (page == "project") impl = new Project(root, params); + else if (page == "stat") impl = new Stat(root, params); + else if (page == "oplog") impl = new Oplog(root, params); + else if (page == "entry") impl = new Entry(root, params); + else if (page == "test") impl = new Test(root, params); + else if (page == undefined) impl = new Start(root, params); if (impl == undefined) { |