aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorzousar <[email protected]>2026-02-16 16:39:44 -0700
committerzousar <[email protected]>2026-02-16 16:39:44 -0700
commitccfcb14ef1b837ed6f752ae4f27e0ef88a5b18da (patch)
treeedf8639f2a4ae32bc002779e1186f91dbba1ce08 /src
parentChange breadcrumbs for oplogs to be more descriptive (diff)
downloadzen-ccfcb14ef1b837ed6f752ae4f27e0ef88a5b18da.tar.xz
zen-ccfcb14ef1b837ed6f752ae4f27e0ef88a5b18da.zip
Added custom page for cook.artifacts
Diffstat (limited to 'src')
-rw-r--r--src/zenserver/frontend/html/pages/cookartifacts.js385
-rw-r--r--src/zenserver/frontend/html/pages/entry.js14
-rw-r--r--src/zenserver/frontend/html/pages/page.js15
-rw-r--r--src/zenserver/frontend/html/zen.css18
4 files changed, 428 insertions, 4 deletions
diff --git a/src/zenserver/frontend/html/pages/cookartifacts.js b/src/zenserver/frontend/html/pages/cookartifacts.js
new file mode 100644
index 000000000..6c36c7f32
--- /dev/null
+++ b/src/zenserver/frontend/html/pages/cookartifacts.js
@@ -0,0 +1,385 @@
+// 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
+ row.get_cell(0).text(dep).on_click((opkey) => {
+ window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`;
+ }, dep);
+ }
+ }
+ }
+
+ _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)
+ {
+ 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 26ea78142..dca3a5c25 100644
--- a/src/zenserver/frontend/html/pages/entry.js
+++ b/src/zenserver/frontend/html/pages/entry.js
@@ -138,11 +138,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);
diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js
index 2f9643008..3ec0248cb 100644
--- a/src/zenserver/frontend/html/pages/page.js
+++ b/src/zenserver/frontend/html/pages/page.js
@@ -118,13 +118,22 @@ export class ZenPage extends PageBase
var oplog = this.get_param("oplog");
if (oplog != undefined)
{
+ new_crumb(auto_name, `?page=project&project=${project}`);
auto_name = oplog;
- new_crumb(project, `?page=project&project=${project}`);
var opkey = this.get_param("opkey")
if (opkey != undefined)
{
- auto_name = opkey.split("/").pop().split("\\").pop();;
- new_crumb(oplog, `?page=oplog&project=${project}&oplog=${oplog}`);
+ 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";
+ }
}
}
}
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 {