aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZousar Shaker <[email protected]>2026-02-18 23:26:55 -0700
committerGitHub Enterprise <[email protected]>2026-02-18 23:26:55 -0700
commit183070a36ca230ac1aaa4db1f6019972d5491825 (patch)
treec00dea385597180673be6e02aca6c07d9ef6ec00
parentstructured compute basics (#714) (diff)
parentMerge branch 'main' into zs/web-ui-improvements (diff)
downloadzen-183070a36ca230ac1aaa4db1f6019972d5491825.tar.xz
zen-183070a36ca230ac1aaa4db1f6019972d5491825.zip
Merge pull request #761 from ue-foundation/zs/web-ui-improvements
Zs/web UI improvements
-rw-r--r--CHANGELOG.md3
-rw-r--r--src/zenserver/frontend/html.zipbin163229 -> 183939 bytes
-rw-r--r--src/zenserver/frontend/html/pages/cookartifacts.js397
-rw-r--r--src/zenserver/frontend/html/pages/entry.js337
-rw-r--r--src/zenserver/frontend/html/pages/oplog.js2
-rw-r--r--src/zenserver/frontend/html/pages/page.js25
-rw-r--r--src/zenserver/frontend/html/pages/start.js2
-rw-r--r--src/zenserver/frontend/html/zen.css18
8 files changed, 752 insertions, 32 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16a6b7fb1..9fa4fe031 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,9 @@
- Improvement: Reduced time project and project oplogs are locked during GC and Validation
- Improvement: `zen` now supports additional configuration of logging options, such as `--log-warn=...` for configuring log levels, etc (see `zen --help`)
+- Improvement: Web UI shows both hard and soft package dependencies
+- Improvement: Web UI presents ops with only files without resorting to json representation
+- Improvement: Web UI offers a cook artifacts view to present cook dependencies
- Bugfix: If a corrupted block (or partial block) is downloaded, handle it gracefully and end the download instead of causing an assert
## 5.7.20
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip
index 5d33302dd..d70a5a62b 100644
--- a/src/zenserver/frontend/html.zip
+++ b/src/zenserver/frontend/html.zip
Binary files differ
diff --git a/src/zenserver/frontend/html/pages/cookartifacts.js b/src/zenserver/frontend/html/pages/cookartifacts.js
new file mode 100644
index 000000000..f2ae094b9
--- /dev/null
+++ b/src/zenserver/frontend/html/pages/cookartifacts.js
@@ -0,0 +1,397 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+import { ZenPage } from "./page.js"
+import { Fetcher } from "../util/fetcher.js"
+import { Table, Toolbar, PropTable } from "../util/widgets.js"
+
+////////////////////////////////////////////////////////////////////////////////
+export class Page extends ZenPage
+{
+ main()
+ {
+ this.set_title("cook artifacts");
+
+ const project = this.get_param("project");
+ const oplog = this.get_param("oplog");
+ const opkey = this.get_param("opkey");
+ const artifact_hash = this.get_param("hash");
+
+ // Fetch the artifact content as JSON
+ this._artifact = new Fetcher()
+ .resource("prj", project, "oplog", oplog, artifact_hash + ".json")
+ .json();
+
+ // Optionally fetch entry info for display context
+ if (opkey)
+ {
+ this._entry = new Fetcher()
+ .resource("prj", project, "oplog", oplog, "entries")
+ .param("opkey", opkey)
+ .cbo();
+ }
+
+ this._build_page();
+ }
+
+ // Map CookDependency enum values to display names
+ _get_dependency_type_name(type_value)
+ {
+ const type_names = {
+ 0: "None",
+ 1: "File",
+ 2: "Function",
+ 3: "TransitiveBuild",
+ 4: "Package",
+ 5: "ConsoleVariable",
+ 6: "Config",
+ 7: "SettingsObject",
+ 8: "NativeClass",
+ 9: "AssetRegistryQuery",
+ 10: "RedirectionTarget"
+ };
+ return type_names[type_value] || `Unknown (${type_value})`;
+ }
+
+ // Check if Data content should be expandable
+ _should_make_expandable(data_string)
+ {
+ if (!data_string || data_string.length < 40)
+ return false;
+
+ // Check if it's JSON array or object
+ if (!data_string.startsWith('[') && !data_string.startsWith('{'))
+ return false;
+
+ // Check if formatting would add newlines
+ try {
+ const parsed = JSON.parse(data_string);
+ const formatted = JSON.stringify(parsed, null, 2);
+ return formatted.includes('\n');
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // Get first line of content for collapsed state
+ _get_first_line(data_string)
+ {
+ if (!data_string)
+ return "";
+
+ const newline_index = data_string.indexOf('\n');
+ if (newline_index === -1)
+ {
+ // No newline, truncate if too long
+ return data_string.length > 80 ? data_string.substring(0, 77) + "..." : data_string;
+ }
+ return data_string.substring(0, newline_index) + "...";
+ }
+
+ // Format JSON with indentation
+ _format_json(data_string)
+ {
+ try {
+ const parsed = JSON.parse(data_string);
+ return JSON.stringify(parsed, null, 2);
+ } catch (e) {
+ return data_string;
+ }
+ }
+
+ // Toggle expand/collapse state
+ _toggle_data_cell(cell)
+ {
+ const is_expanded = cell.attr("expanded") !== null;
+ const full_data = cell.attr("data-full");
+
+ // Find the text wrapper span
+ const text_wrapper = cell.first_child().next_sibling();
+
+ if (is_expanded)
+ {
+ // Collapse: show first line only
+ const first_line = this._get_first_line(full_data);
+ text_wrapper.text(first_line);
+ cell.attr("expanded", null);
+ }
+ else
+ {
+ // Expand: show formatted JSON
+ const formatted = this._format_json(full_data);
+ text_wrapper.text(formatted);
+ cell.attr("expanded", "");
+ }
+ }
+
+ // Format dependency data based on its structure
+ _format_dependency(dep_array)
+ {
+ const type = dep_array[0];
+ const formatted = {};
+
+ // Common patterns based on the example data:
+ // Type 2 (Function): [type, name, array, hash]
+ // Type 4 (Package): [type, path, hash]
+ // Type 5 (ConsoleVariable): [type, bool, array, hash]
+ // Type 8 (NativeClass): [type, path, hash]
+ // Type 9 (AssetRegistryQuery): [type, bool, object, hash]
+ // Type 10 (RedirectionTarget): [type, path, hash]
+
+ if (dep_array.length > 1)
+ {
+ // Most types have a name/path as second element
+ if (typeof dep_array[1] === "string")
+ {
+ formatted.Name = dep_array[1];
+ }
+ else if (typeof dep_array[1] === "boolean")
+ {
+ formatted.Value = dep_array[1].toString();
+ }
+ }
+
+ if (dep_array.length > 2)
+ {
+ // Third element varies
+ if (Array.isArray(dep_array[2]))
+ {
+ formatted.Data = JSON.stringify(dep_array[2]);
+ }
+ else if (typeof dep_array[2] === "object")
+ {
+ formatted.Data = JSON.stringify(dep_array[2]);
+ }
+ else if (typeof dep_array[2] === "string")
+ {
+ formatted.Hash = dep_array[2];
+ }
+ }
+
+ if (dep_array.length > 3)
+ {
+ // Fourth element is usually the hash
+ if (typeof dep_array[3] === "string")
+ {
+ formatted.Hash = dep_array[3];
+ }
+ }
+
+ return formatted;
+ }
+
+ async _build_page()
+ {
+ const project = this.get_param("project");
+ const oplog = this.get_param("oplog");
+ const opkey = this.get_param("opkey");
+ const artifact_hash = this.get_param("hash");
+
+ // Build page title
+ let title = "Cook Artifacts";
+ if (this._entry)
+ {
+ try
+ {
+ const entry = await this._entry;
+ const entry_obj = entry.as_object().find("entry").as_object();
+ const key = entry_obj.find("key").as_value();
+ title = `Cook Artifacts`;
+ }
+ catch (e)
+ {
+ console.error("Failed to fetch entry:", e);
+ }
+ }
+
+ const section = this.add_section(title);
+
+ // Fetch and parse artifact
+ let artifact;
+ try
+ {
+ artifact = await this._artifact;
+ }
+ catch (e)
+ {
+ section.text(`Failed to load artifact: ${e.message}`);
+ return;
+ }
+
+ // Display artifact info
+ const info_section = section.add_section("Artifact Info");
+ const info_table = info_section.add_widget(Table, ["Property", "Value"], Table.Flag_PackRight);
+
+ if (artifact.Version !== undefined)
+ info_table.add_row("Version", artifact.Version.toString());
+ if (artifact.HasSaveResults !== undefined)
+ info_table.add_row("HasSaveResults", artifact.HasSaveResults.toString());
+ if (artifact.PackageSavedHash !== undefined)
+ info_table.add_row("PackageSavedHash", artifact.PackageSavedHash);
+
+ // Process SaveBuildDependencies
+ if (artifact.SaveBuildDependencies && artifact.SaveBuildDependencies.Dependencies)
+ {
+ this._build_dependency_section(
+ section,
+ "Save Build Dependencies",
+ artifact.SaveBuildDependencies.Dependencies,
+ artifact.SaveBuildDependencies.StoredKey
+ );
+ }
+
+ // Process LoadBuildDependencies
+ if (artifact.LoadBuildDependencies && artifact.LoadBuildDependencies.Dependencies)
+ {
+ this._build_dependency_section(
+ section,
+ "Load Build Dependencies",
+ artifact.LoadBuildDependencies.Dependencies,
+ artifact.LoadBuildDependencies.StoredKey
+ );
+ }
+
+ // Process RuntimeDependencies
+ if (artifact.RuntimeDependencies && artifact.RuntimeDependencies.length > 0)
+ {
+ const runtime_section = section.add_section("Runtime Dependencies");
+ const runtime_table = runtime_section.add_widget(Table, ["Path"], Table.Flag_PackRight);
+ for (const dep of artifact.RuntimeDependencies)
+ {
+ const row = runtime_table.add_row(dep);
+ // Make Path clickable to navigate to entry
+ if (this._should_link_dependency(dep))
+ {
+ row.get_cell(0).text(dep).on_click((opkey) => {
+ window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`;
+ }, dep);
+ }
+ }
+ }
+ }
+
+ _should_link_dependency(name)
+ {
+ // Exclude dependencies starting with /Script/ (code-defined entries) - case insensitive
+ if (name && name.toLowerCase().startsWith("/script/"))
+ return false;
+
+ return true;
+ }
+
+ _build_dependency_section(parent_section, title, dependencies, stored_key)
+ {
+ const section = parent_section.add_section(title);
+
+ // Add stored key info
+ if (stored_key)
+ {
+ const key_toolbar = section.add_widget(Toolbar);
+ key_toolbar.left().add(`Key: ${stored_key}`);
+ }
+
+ // Group dependencies by type
+ const dependencies_by_type = {};
+
+ for (const dep_array of dependencies)
+ {
+ if (!Array.isArray(dep_array) || dep_array.length === 0)
+ continue;
+
+ const type = dep_array[0];
+ if (!dependencies_by_type[type])
+ dependencies_by_type[type] = [];
+
+ dependencies_by_type[type].push(this._format_dependency(dep_array));
+ }
+
+ // Sort types numerically
+ const sorted_types = Object.keys(dependencies_by_type).map(Number).sort((a, b) => a - b);
+
+ for (const type_value of sorted_types)
+ {
+ const type_name = this._get_dependency_type_name(type_value);
+ const deps = dependencies_by_type[type_value];
+
+ const type_section = section.add_section(type_name);
+
+ // Determine columns based on available fields
+ const all_fields = new Set();
+ for (const dep of deps)
+ {
+ for (const field in dep)
+ all_fields.add(field);
+ }
+ let columns = Array.from(all_fields);
+
+ // Remove Hash column for RedirectionTarget as it's not useful
+ if (type_value === 10)
+ {
+ columns = columns.filter(col => col !== "Hash");
+ }
+
+ if (columns.length === 0)
+ {
+ type_section.text("No data fields");
+ continue;
+ }
+
+ // Create table with dynamic columns
+ const table = type_section.add_widget(Table, columns, Table.Flag_PackRight);
+
+ // Check if this type should have clickable Name links
+ const should_link = (type_value === 3 || type_value === 4 || type_value === 10);
+ const name_col_index = columns.indexOf("Name");
+
+ for (const dep of deps)
+ {
+ const row_values = columns.map(col => dep[col] || "");
+ const row = table.add_row(...row_values);
+
+ // Make Name field clickable for Package, TransitiveBuild, and RedirectionTarget
+ if (should_link && name_col_index >= 0 && dep.Name && this._should_link_dependency(dep.Name))
+ {
+ const project = this.get_param("project");
+ const oplog = this.get_param("oplog");
+ row.get_cell(name_col_index).text(dep.Name).on_click((opkey) => {
+ window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`;
+ }, dep.Name);
+ }
+
+ // Make Data field expandable/collapsible if needed
+ const data_col_index = columns.indexOf("Data");
+ if (data_col_index >= 0 && dep.Data)
+ {
+ const data_cell = row.get_cell(data_col_index);
+
+ if (this._should_make_expandable(dep.Data))
+ {
+ // Store full data in attribute
+ data_cell.attr("data-full", dep.Data);
+
+ // Clear the cell and rebuild with icon + text
+ data_cell.inner().innerHTML = "";
+
+ // Create expand/collapse icon
+ const icon = data_cell.tag("span").classify("zen_expand_icon").text("+");
+ icon.on_click(() => {
+ this._toggle_data_cell(data_cell);
+ // Update icon text
+ const is_expanded = data_cell.attr("expanded") !== null;
+ icon.text(is_expanded ? "-" : "+");
+ });
+
+ // Add text content wrapper
+ const text_wrapper = data_cell.tag("span").classify("zen_data_text");
+ const first_line = this._get_first_line(dep.Data);
+ text_wrapper.text(first_line);
+
+ // Store reference to text wrapper for updates
+ data_cell.attr("data-text-wrapper", "true");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js
index 08589b090..f418b17ba 100644
--- a/src/zenserver/frontend/html/pages/entry.js
+++ b/src/zenserver/frontend/html/pages/entry.js
@@ -26,6 +26,9 @@ export class Page extends ZenPage
this._indexer = this.load_indexer(project, oplog);
+ this._files_index_start = Number(this.get_param("files_start", 0)) || 0;
+ this._files_index_count = Number(this.get_param("files_count", 50)) || 0;
+
this._build_page();
}
@@ -40,25 +43,39 @@ export class Page extends ZenPage
return indexer;
}
- async _build_deps(section, tree)
+ _build_deps(section, tree)
{
- const indexer = await this._indexer;
+ const project = this.get_param("project");
+ const oplog = this.get_param("oplog");
for (const dep_name in tree)
{
const dep_section = section.add_section(dep_name);
const table = dep_section.add_widget(Table, ["name", "id"], Table.Flag_PackRight);
+
for (const dep_id of tree[dep_name])
{
- const cell_values = ["", dep_id.toString(16).padStart(16, "0")];
+ const hex_id = dep_id.toString(16).padStart(16, "0");
+ const cell_values = ["loading...", hex_id];
const row = table.add_row(...cell_values);
- var opkey = indexer.lookup_id(dep_id);
- row.get_cell(0).text(opkey).on_click((k) => this.view_opkey(k), opkey);
+ // Asynchronously resolve the name
+ this._resolve_dep_name(row.get_cell(0), dep_id, project, oplog);
}
}
}
+ async _resolve_dep_name(cell, dep_id, project, oplog)
+ {
+ const indexer = await this._indexer;
+ const opkey = indexer.lookup_id(dep_id);
+
+ if (opkey)
+ {
+ cell.text(opkey).on_click((k) => this.view_opkey(k), opkey);
+ }
+ }
+
_find_iohash_field(container, name)
{
const found_field = container.find(name);
@@ -76,6 +93,21 @@ export class Page extends ZenPage
return null;
}
+ _is_null_io_hash_string(io_hash)
+ {
+ if (!io_hash)
+ return true;
+
+ for (let char of io_hash)
+ {
+ if (char != '0')
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
async _build_meta(section, entry)
{
var tree = {}
@@ -123,11 +155,23 @@ export class Page extends ZenPage
const project = this.get_param("project");
const oplog = this.get_param("oplog");
+ const opkey = this.get_param("opkey");
const link = row.get_cell(0).link(
- "/" + ["prj", project, "oplog", oplog, value+".json"].join("/")
+ (key === "cook.artifacts") ?
+ `?page=cookartifacts&project=${project}&oplog=${oplog}&opkey=${opkey}&hash=${value}`
+ : "/" + ["prj", project, "oplog", oplog, value+".json"].join("/")
);
const action_tb = new Toolbar(row.get_cell(-1), true);
+
+ // Add "view-raw" button for cook.artifacts
+ if (key === "cook.artifacts")
+ {
+ action_tb.left().add("view-raw").on_click(() => {
+ window.location = "/" + ["prj", project, "oplog", oplog, value+".json"].join("/");
+ });
+ }
+
action_tb.left().add("copy-hash").on_click(async (v) => {
await navigator.clipboard.writeText(v);
}, value);
@@ -137,35 +181,55 @@ export class Page extends ZenPage
async _build_page()
{
var entry = await this._entry;
+
+ // Check if entry exists
+ if (!entry || entry.as_object().find("entry") == null)
+ {
+ const opkey = this.get_param("opkey");
+ var section = this.add_section("Entry Not Found");
+ section.tag("p").text(`The entry "${opkey}" is not present in this dataset.`);
+ section.tag("p").text("This could mean:");
+ const list = section.tag("ul");
+ list.tag("li").text("The entry is for an instance defined in code");
+ list.tag("li").text("The entry has not been added to the oplog yet");
+ list.tag("li").text("The entry key is misspelled");
+ list.tag("li").text("The entry was removed or never existed");
+ return;
+ }
+
entry = entry.as_object().find("entry").as_object();
const name = entry.find("key").as_value();
var section = this.add_section(name);
+ var has_package_data = false;
// tree
{
var tree = entry.find("$tree");
if (tree == undefined)
tree = this._convert_legacy_to_tree(entry);
- if (tree == undefined)
- return this._display_unsupported(section, entry);
-
- delete tree["$id"];
-
- if (Object.keys(tree).length != 0)
+ if (tree != undefined)
{
- const sub_section = section.add_section("deps");
- this._build_deps(sub_section, tree);
+ delete tree["$id"];
+
+ if (Object.keys(tree).length != 0)
+ {
+ const sub_section = section.add_section("dependencies");
+ this._build_deps(sub_section, tree);
+ }
+ has_package_data = true;
}
}
// meta
+ if (has_package_data)
{
this._build_meta(section, entry);
}
// data
+ if (has_package_data)
{
const sub_section = section.add_section("data");
const table = sub_section.add_widget(
@@ -181,7 +245,7 @@ export class Page extends ZenPage
for (const item of pkg_data.as_array())
{
- var io_hash, size, raw_size, file_name;
+ var io_hash = undefined, size = undefined, raw_size = undefined, file_name = undefined;
for (const field of item.as_object())
{
if (field.is_named("data")) io_hash = field.as_value();
@@ -219,12 +283,76 @@ export class Page extends ZenPage
}
}
+ // files
+ var has_file_data = false;
+ {
+ var file_data = entry.find("files");
+ if (file_data != undefined)
+ {
+ has_file_data = true;
+
+ // Extract files into array
+ this._files_data = [];
+ for (const item of file_data.as_array())
+ {
+ var io_hash = undefined, cid = undefined, server_path = undefined, client_path = undefined;
+ for (const field of item.as_object())
+ {
+ if (field.is_named("data")) io_hash = field.as_value();
+ else if (field.is_named("id")) cid = field.as_value();
+ else if (field.is_named("serverpath")) server_path = field.as_value();
+ else if (field.is_named("clientpath")) client_path = field.as_value();
+ }
+
+ if (io_hash instanceof Uint8Array)
+ {
+ var ret = "";
+ for (var x of io_hash)
+ ret += x.toString(16).padStart(2, "0");
+ io_hash = ret;
+ }
+
+ if (cid instanceof Uint8Array)
+ {
+ var ret = "";
+ for (var x of cid)
+ ret += x.toString(16).padStart(2, "0");
+ cid = ret;
+ }
+
+ this._files_data.push({
+ server_path: server_path,
+ client_path: client_path,
+ io_hash: io_hash,
+ cid: cid
+ });
+ }
+
+ this._files_index_max = this._files_data.length;
+
+ const sub_section = section.add_section("files");
+ this._build_files_nav(sub_section);
+
+ this._files_table = sub_section.add_widget(
+ Table,
+ ["name", "actions"], Table.Flag_PackRight
+ );
+ this._files_table.id("filetable");
+
+ this._build_files_table(this._files_index_start);
+ }
+ }
+
// props
+ if (has_package_data)
{
const object = entry.to_js_object();
var sub_section = section.add_section("props");
sub_section.add_widget(PropTable).add_object(object);
}
+
+ if (!has_package_data && !has_file_data)
+ return this._display_unsupported(section, entry);
}
_display_unsupported(section, entry)
@@ -271,16 +399,30 @@ export class Page extends ZenPage
for (const field of pkgst_entry)
{
const field_name = field.get_name();
- if (!field_name.endsWith("importedpackageids"))
- continue;
-
- var dep_name = field_name.slice(0, -18);
- if (dep_name.length == 0)
- dep_name = "imported";
-
- var out = tree[dep_name] = [];
- for (var item of field.as_array())
- out.push(item.as_value(BigInt));
+ if (field_name.endsWith("importedpackageids"))
+ {
+ var dep_name = field_name.slice(0, -18);
+ if (dep_name.length == 0)
+ dep_name = "hard";
+ else
+ dep_name = "hard." + dep_name;
+
+ var out = tree[dep_name] = [];
+ for (var item of field.as_array())
+ out.push(item.as_value(BigInt));
+ }
+ else if (field_name.endsWith("softpackagereferences"))
+ {
+ var dep_name = field_name.slice(0, -21);
+ if (dep_name.length == 0)
+ dep_name = "soft";
+ else
+ dep_name = "soft." + dep_name;
+
+ var out = tree[dep_name] = [];
+ for (var item of field.as_array())
+ out.push(item.as_value(BigInt));
+ }
}
return tree;
@@ -292,4 +434,149 @@ export class Page extends ZenPage
params.set("opkey", opkey);
window.location.search = params;
}
+
+ _build_files_nav(section)
+ {
+ const nav = section.add_widget(Toolbar);
+ const left = nav.left();
+ left.add("|<") .on_click(() => this._on_files_next_prev(-10e10));
+ left.add("<<") .on_click(() => this._on_files_next_prev(-10));
+ left.add("prev").on_click(() => this._on_files_next_prev( -1));
+ left.add("next").on_click(() => this._on_files_next_prev( 1));
+ left.add(">>") .on_click(() => this._on_files_next_prev( 10));
+ left.add(">|") .on_click(() => this._on_files_next_prev( 10e10));
+
+ left.sep();
+ for (var count of [10, 25, 50, 100])
+ {
+ var handler = (n) => this._on_files_change_count(n);
+ left.add(count).on_click(handler, count);
+ }
+
+ const right = nav.right();
+ right.add(Friendly.sep(this._files_index_max));
+
+ right.sep();
+ var search_input = right.add("search:", "label").tag("input");
+ search_input.on("change", (x) => this._search_files(x.inner().value), search_input);
+ }
+
+ _build_files_table(index)
+ {
+ this._files_index_count = Math.max(this._files_index_count, 1);
+ index = Math.min(index, this._files_index_max - this._files_index_count);
+ index = Math.max(index, 0);
+
+ const project = this.get_param("project");
+ const oplog = this.get_param("oplog");
+
+ const end_index = Math.min(index + this._files_index_count, this._files_index_max);
+
+ this._files_table.clear(index);
+ for (var i = index; i < end_index; i++)
+ {
+ const file_item = this._files_data[i];
+ const row = this._files_table.add_row(file_item.server_path);
+
+ var base_name = file_item.server_path.split("/").pop().split("\\").pop();
+ if (this._is_null_io_hash_string(file_item.io_hash))
+ {
+ const link = row.get_cell(0).link(
+ "/" + ["prj", project, "oplog", oplog, file_item.cid].join("/")
+ );
+ link.first_child().attr("download", `${file_item.cid}_${base_name}`);
+
+ const action_tb = new Toolbar(row.get_cell(-1), true);
+ action_tb.left().add("copy-id").on_click(async (v) => {
+ await navigator.clipboard.writeText(v);
+ }, file_item.cid);
+ }
+ else
+ {
+ const link = row.get_cell(0).link(
+ "/" + ["prj", project, "oplog", oplog, file_item.io_hash].join("/")
+ );
+ link.first_child().attr("download", `${file_item.io_hash}_${base_name}`);
+
+ const action_tb = new Toolbar(row.get_cell(-1), true);
+ action_tb.left().add("copy-hash").on_click(async (v) => {
+ await navigator.clipboard.writeText(v);
+ }, file_item.io_hash);
+ }
+ }
+
+ this.set_param("files_start", index);
+ this.set_param("files_count", this._files_index_count);
+ this._files_index_start = index;
+ }
+
+ _on_files_change_count(value)
+ {
+ this._files_index_count = parseInt(value);
+ this._build_files_table(this._files_index_start);
+ }
+
+ _on_files_next_prev(direction)
+ {
+ var index = this._files_index_start + (this._files_index_count * direction);
+ index = Math.max(0, index);
+ this._build_files_table(index);
+ }
+
+ _search_files(needle)
+ {
+ if (needle.length == 0)
+ {
+ this._build_files_table(this._files_index_start);
+ return;
+ }
+ needle = needle.trim().toLowerCase();
+
+ this._files_table.clear(this._files_index_start);
+
+ const project = this.get_param("project");
+ const oplog = this.get_param("oplog");
+
+ var added = 0;
+ const truncate_at = this.get_param("searchmax") || 250;
+ for (const file_item of this._files_data)
+ {
+ if (!file_item.server_path.toLowerCase().includes(needle))
+ continue;
+
+ const row = this._files_table.add_row(file_item.server_path);
+
+ var base_name = file_item.server_path.split("/").pop().split("\\").pop();
+ if (this._is_null_io_hash_string(file_item.io_hash))
+ {
+ const link = row.get_cell(0).link(
+ "/" + ["prj", project, "oplog", oplog, file_item.cid].join("/")
+ );
+ link.first_child().attr("download", `${file_item.cid}_${base_name}`);
+
+ const action_tb = new Toolbar(row.get_cell(-1), true);
+ action_tb.left().add("copy-id").on_click(async (v) => {
+ await navigator.clipboard.writeText(v);
+ }, file_item.cid);
+ }
+ else
+ {
+ const link = row.get_cell(0).link(
+ "/" + ["prj", project, "oplog", oplog, file_item.io_hash].join("/")
+ );
+ link.first_child().attr("download", `${file_item.io_hash}_${base_name}`);
+
+ const action_tb = new Toolbar(row.get_cell(-1), true);
+ action_tb.left().add("copy-hash").on_click(async (v) => {
+ await navigator.clipboard.writeText(v);
+ }, file_item.io_hash);
+ }
+
+ if (++added >= truncate_at)
+ {
+ this._files_table.add_row("...truncated");
+ break;
+ }
+ }
+ }
}
diff --git a/src/zenserver/frontend/html/pages/oplog.js b/src/zenserver/frontend/html/pages/oplog.js
index 879fc4c97..a286f8651 100644
--- a/src/zenserver/frontend/html/pages/oplog.js
+++ b/src/zenserver/frontend/html/pages/oplog.js
@@ -32,7 +32,7 @@ export class Page extends ZenPage
this.set_title("oplog - " + oplog);
- var section = this.add_section(project + " - " + oplog);
+ var section = this.add_section(oplog);
oplog_info = await oplog_info;
this._index_max = oplog_info["opcount"];
diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js
index 9a9541904..3ec0248cb 100644
--- a/src/zenserver/frontend/html/pages/page.js
+++ b/src/zenserver/frontend/html/pages/page.js
@@ -97,7 +97,7 @@ export class ZenPage extends PageBase
generate_crumbs()
{
- const auto_name = this.get_param("page") || "start";
+ var auto_name = this.get_param("page") || "start";
if (auto_name == "start")
return;
@@ -114,15 +114,30 @@ export class ZenPage extends PageBase
var project = this.get_param("project");
if (project != undefined)
{
+ auto_name = project;
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, `?page=project&project=${project}`);
+ auto_name = oplog;
+ var opkey = this.get_param("opkey")
+ if (opkey != undefined)
+ {
+ new_crumb(auto_name, `?page=oplog&project=${project}&oplog=${oplog}`);
+ auto_name = opkey.split("/").pop().split("\\").pop();
+
+ // Check if we're viewing cook artifacts
+ var page = this.get_param("page");
+ var hash = this.get_param("hash");
+ if (hash != undefined && page == "cookartifacts")
+ {
+ new_crumb(auto_name, `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey}`);
+ auto_name = "cook artifacts";
+ }
+ }
}
}
- new_crumb(auto_name.toLowerCase());
+ new_crumb(auto_name);
}
}
diff --git a/src/zenserver/frontend/html/pages/start.js b/src/zenserver/frontend/html/pages/start.js
index 4c8789431..2cf12bf12 100644
--- a/src/zenserver/frontend/html/pages/start.js
+++ b/src/zenserver/frontend/html/pages/start.js
@@ -46,7 +46,7 @@ export class Page extends ZenPage
}
// cache
- var section = this.add_section("z$");
+ var section = this.add_section("cache");
section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("z$"));
diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css
index cc53c0519..34c265610 100644
--- a/src/zenserver/frontend/html/zen.css
+++ b/src/zenserver/frontend/html/zen.css
@@ -172,6 +172,24 @@ a {
}
}
+/* expandable cell ---------------------------------------------------------- */
+
+.zen_expand_icon {
+ cursor: pointer;
+ margin-right: 0.5em;
+ color: var(--theme_g1);
+ font-weight: bold;
+ user-select: none;
+}
+
+.zen_expand_icon:hover {
+ color: var(--theme_ln);
+}
+
+.zen_data_text {
+ user-select: text;
+}
+
/* toolbar ------------------------------------------------------------------ */
.zen_toolbar {