Skip to content

web: Remove 'Update' button in static HTML GUI (#10220)#10349

Merged
maliberty merged 3 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:remove-button-update-static-GUI
May 21, 2026
Merged

web: Remove 'Update' button in static HTML GUI (#10220)#10349
maliberty merged 3 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:remove-button-update-static-GUI

Conversation

@openroad-ci
Copy link
Copy Markdown
Collaborator

fix #10220

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

clang-tidy review says "All clean, LGTM! 👍"

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a "static mode" for the ChartsWidget, ClockTreeWidget, HierarchyBrowser, and TimingWidget components, which hides the "Update" button, automates data fetching on initialization, and updates empty-state messages. The reviewer noted that the PR contains extensive unrelated formatting and indentation changes across all modified files and requested that these refactoring efforts be moved to a separate pull request to keep the functional changes focused.

Comment thread src/web/src/clock-tree-widget.js Outdated
// Canvas-based clock tree viewer widget.

import { getThemeColors } from './theme.js';
import {getThemeColors} from './theme.js';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pull request includes formatting changes (e.g., import style) that are unrelated to the main task of removing the 'Update' button. Please separate these unrelated refactoring changes into a distinct pull request to keep the current PR focused.

Comment thread src/web/src/hierarchy-browser.js Outdated
Comment on lines 6 to 19
import {CheckboxTreeModel} from './checkbox-tree-model.js';
import {makeResizableHeaders} from './ui-utils.js';

const COLS = [
'Instance', 'Module', 'Instances', 'Macros', 'Modules',
'Area', 'Local Inst', 'Local Macros', 'Local Modules',
'Instance',
'Module',
'Instances',
'Macros',
'Modules',
'Area',
'Local Inst',
'Local Macros',
'Local Modules',
];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pull request includes several formatting changes (e.g., import style, array formatting) that are unrelated to the main task of removing the 'Update' button. Please separate these unrelated refactoring changes into a distinct pull request to keep the current PR focused.

Comment thread src/web/src/timing-widget.js Outdated
Comment on lines +6 to +407
import {makeResizableHeaders} from './ui-utils.js';

export class TimingWidget {
constructor(app, redrawAllLayers) {
this._app = app;
this._redrawAllLayers = redrawAllLayers;

this._currentTab = 'setup';
this._setupPaths = [];
this._holdPaths = [];
this._selectedPathIndex = -1;
this._detailTab = 'data';
this._selectedDetailIndex = -1;

this._build();
constructor(app, redrawAllLayers) {
this._app = app;
this._redrawAllLayers = redrawAllLayers;

this._currentTab = 'setup';
this._setupPaths = [];
this._holdPaths = [];
this._selectedPathIndex = -1;
this._detailTab = 'data';
this._selectedDetailIndex = -1;

this._build();
}

_build() {
const el = document.createElement('div');
el.className = 'timing-widget';

// --- Toolbar ---
const toolbar = document.createElement('div');
toolbar.className = 'timing-toolbar';

this._updateBtn = document.createElement('button');
this._updateBtn.className = 'timing-btn';
this._updateBtn.textContent = 'Update';
if (this._app.websocketManager && this._app.websocketManager.isStaticMode) {
this._updateBtn.style.display = 'none';
}

_build() {
const el = document.createElement('div');
el.className = 'timing-widget';

// --- Toolbar ---
const toolbar = document.createElement('div');
toolbar.className = 'timing-toolbar';

this._updateBtn = document.createElement('button');
this._updateBtn.className = 'timing-btn';
this._updateBtn.textContent = 'Update';

this._pathCountLabel = document.createElement('span');
this._pathCountLabel.className = 'timing-path-count';

toolbar.appendChild(this._updateBtn);
toolbar.appendChild(this._pathCountLabel);
el.appendChild(toolbar);

// --- Setup/Hold Tab Bar ---
const tabBar = document.createElement('div');
tabBar.className = 'timing-tab-bar';

this._setupTab = this._makeTab('Setup', true);
this._holdTab = this._makeTab('Hold', false);
tabBar.appendChild(this._setupTab);
tabBar.appendChild(this._holdTab);
el.appendChild(tabBar);

// --- Path listing table ---
this._pathTableContainer = document.createElement('div');
this._pathTableContainer.className = 'timing-path-table-container';
this._pathTable = document.createElement('table');
this._pathTable.className = 'timing-table';
this._pathTableContainer.appendChild(this._pathTable);
el.appendChild(this._pathTableContainer);

// --- Detail Tab Bar ---
const detailTabBar = document.createElement('div');
detailTabBar.className = 'timing-tab-bar';
this._dataTab = this._makeTab('Data Path', true);
this._captureTab = this._makeTab('Capture Path', false);
detailTabBar.appendChild(this._dataTab);
detailTabBar.appendChild(this._captureTab);
el.appendChild(detailTabBar);

// --- Detail table ---
this._detailTableContainer = document.createElement('div');
this._detailTableContainer.className = 'timing-detail-table-container';
this._detailTable = document.createElement('table');
this._detailTable.className = 'timing-table';
this._detailTableContainer.appendChild(this._detailTable);
el.appendChild(this._detailTableContainer);

this.element = el;

this._bindEvents();
this._pathCountLabel = document.createElement('span');
this._pathCountLabel.className = 'timing-path-count';

toolbar.appendChild(this._updateBtn);
toolbar.appendChild(this._pathCountLabel);
el.appendChild(toolbar);

// --- Setup/Hold Tab Bar ---
const tabBar = document.createElement('div');
tabBar.className = 'timing-tab-bar';

this._setupTab = this._makeTab('Setup', true);
this._holdTab = this._makeTab('Hold', false);
tabBar.appendChild(this._setupTab);
tabBar.appendChild(this._holdTab);
el.appendChild(tabBar);

// --- Path listing table ---
this._pathTableContainer = document.createElement('div');
this._pathTableContainer.className = 'timing-path-table-container';
this._pathTable = document.createElement('table');
this._pathTable.className = 'timing-table';
this._pathTableContainer.appendChild(this._pathTable);
el.appendChild(this._pathTableContainer);

// --- Detail Tab Bar ---
const detailTabBar = document.createElement('div');
detailTabBar.className = 'timing-tab-bar';
this._dataTab = this._makeTab('Data Path', true);
this._captureTab = this._makeTab('Capture Path', false);
detailTabBar.appendChild(this._dataTab);
detailTabBar.appendChild(this._captureTab);
el.appendChild(detailTabBar);

// --- Detail table ---
this._detailTableContainer = document.createElement('div');
this._detailTableContainer.className = 'timing-detail-table-container';
this._detailTable = document.createElement('table');
this._detailTable.className = 'timing-table';
this._detailTableContainer.appendChild(this._detailTable);
el.appendChild(this._detailTableContainer);

this.element = el;

this._bindEvents();

if (this._app.websocketManager && this._app.websocketManager.isStaticMode) {
setTimeout(() => this.update(), 0);
}

_makeTab(label, active) {
const btn = document.createElement('button');
btn.className = 'timing-tab' + (active ? ' active' : '');
btn.textContent = label;
return btn;
}

_bindEvents() {
// Tab switching
this._setupTab.addEventListener('click', () => {
this._currentTab = 'setup';
this._setupTab.classList.add('active');
this._holdTab.classList.remove('active');
this._selectedPathIndex = -1;
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
});
this._holdTab.addEventListener('click', () => {
this._currentTab = 'hold';
this._holdTab.classList.add('active');
this._setupTab.classList.remove('active');
this._selectedPathIndex = -1;
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
});
this._dataTab.addEventListener('click', () => {
this._detailTab = 'data';
this._dataTab.classList.add('active');
this._captureTab.classList.remove('active');
this._renderDetailTable();
});
this._captureTab.addEventListener('click', () => {
this._detailTab = 'capture';
this._captureTab.classList.add('active');
this._dataTab.classList.remove('active');
this._renderDetailTable();
});

// Fetch paths
this._updateBtn.addEventListener('click', () => this.update());

// Keyboard navigation — path table
this._pathTableContainer.setAttribute('tabindex', '0');
this._pathTableContainer.style.outline = 'none';
this._pathTableContainer.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const rows = this._pathTable.querySelectorAll('tbody tr');
if (this._selectedPathIndex < rows.length - 1) {
this._selectPathRow(this._selectedPathIndex + 1);
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (this._selectedPathIndex > 0) {
this._selectPathRow(this._selectedPathIndex - 1);
}
}
});

// Keyboard navigation — detail table
this._detailTableContainer.setAttribute('tabindex', '0');
this._detailTableContainer.style.outline = 'none';
this._detailTableContainer.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const rows = this._detailTable.querySelectorAll('tbody tr');
if (this._selectedDetailIndex < rows.length - 1) {
this._selectDetailRow(this._selectedDetailIndex + 1);
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (this._selectedDetailIndex > 0) {
this._selectDetailRow(this._selectedDetailIndex - 1);
}
}
});
}

showPaths(tab, paths) {
this._currentTab = tab;
if (tab === 'setup') {
this._setupPaths = paths;
this._setupTab.classList.add('active');
this._holdTab.classList.remove('active');
} else {
this._holdPaths = paths;
this._holdTab.classList.add('active');
this._setupTab.classList.remove('active');
}

_makeTab(label, active) {
const btn = document.createElement('button');
btn.className = 'timing-tab' + (active ? ' active' : '');
btn.textContent = label;
return btn;
}

_bindEvents() {
// Tab switching
this._setupTab.addEventListener('click', () => {
this._currentTab = 'setup';
this._setupTab.classList.add('active');
this._holdTab.classList.remove('active');
this._selectedPathIndex = -1;
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
});
this._holdTab.addEventListener('click', () => {
this._currentTab = 'hold';
this._holdTab.classList.add('active');
this._setupTab.classList.remove('active');
this._selectedPathIndex = -1;
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
});
this._dataTab.addEventListener('click', () => {
this._detailTab = 'data';
this._dataTab.classList.add('active');
this._captureTab.classList.remove('active');
this._renderDetailTable();
});
this._captureTab.addEventListener('click', () => {
this._detailTab = 'capture';
this._captureTab.classList.add('active');
this._dataTab.classList.remove('active');
this._renderDetailTable();
});

// Fetch paths
this._updateBtn.addEventListener('click', () => this.update());

// Keyboard navigation — path table
this._pathTableContainer.setAttribute('tabindex', '0');
this._pathTableContainer.style.outline = 'none';
this._pathTableContainer.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const rows = this._pathTable.querySelectorAll('tbody tr');
if (this._selectedPathIndex < rows.length - 1) {
this._selectPathRow(this._selectedPathIndex + 1);
}
this._selectedPathIndex = -1;
this._pathCountLabel.textContent = paths.length + ' paths';
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
}

async update() {
this._updateBtn.disabled = true;
this._updateBtn.textContent = 'Loading...';
try {
const [setupData, holdData] = await Promise.all([
this._app.websocketManager.request({ type: 'timing_report', is_setup: true, max_paths: 100 }),
this._app.websocketManager.request({ type: 'timing_report', is_setup: false, max_paths: 100 }),
]);
this._setupPaths = setupData.paths || [];
this._holdPaths = holdData.paths || [];
this._selectedPathIndex = -1;
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
} catch (e) {
console.error('Timing fetch failed:', e);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (this._selectedPathIndex > 0) {
this._selectPathRow(this._selectedPathIndex - 1);
}
}
});

// Keyboard navigation — detail table
this._detailTableContainer.setAttribute('tabindex', '0');
this._detailTableContainer.style.outline = 'none';
this._detailTableContainer.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const rows = this._detailTable.querySelectorAll('tbody tr');
if (this._selectedDetailIndex < rows.length - 1) {
this._selectDetailRow(this._selectedDetailIndex + 1);
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (this._selectedDetailIndex > 0) {
this._selectDetailRow(this._selectedDetailIndex - 1);
}
this._updateBtn.disabled = false;
this._updateBtn.textContent = 'Update';
}
});
}

showPaths(tab, paths) {
this._currentTab = tab;
if (tab === 'setup') {
this._setupPaths = paths;
this._setupTab.classList.add('active');
this._holdTab.classList.remove('active');
} else {
this._holdPaths = paths;
this._holdTab.classList.add('active');
this._setupTab.classList.remove('active');
}

_clearTimingHighlight() {
this._app.websocketManager.request({ type: 'timing_highlight', path_index: -1 })
.then(() => this._redrawAllLayers());
this._selectedPathIndex = -1;
this._pathCountLabel.textContent = paths.length + ' paths';
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
}

async update() {
this._updateBtn.disabled = true;
this._updateBtn.textContent = 'Loading...';
try {
const [setupData, holdData] = await Promise.all([
this._app.websocketManager.request(
{type: 'timing_report', is_setup: true, max_paths: 100}),
this._app.websocketManager.request(
{type: 'timing_report', is_setup: false, max_paths: 100}),
]);
this._setupPaths = setupData.paths || [];
this._holdPaths = holdData.paths || [];
this._selectedPathIndex = -1;
this._renderPathTable();
this._renderDetailTable();
this._clearTimingHighlight();
} catch (e) {
console.error('Timing fetch failed:', e);
}

_selectPathRow(idx) {
const rows = this._pathTable.querySelectorAll('tbody tr');
if (idx < 0 || idx >= rows.length) return;
this._selectedPathIndex = idx;
for (const row of rows) row.classList.remove('selected');
rows[idx].classList.add('selected');
rows[idx].scrollIntoView({ block: 'nearest' });
this._pathTableContainer.focus();
this._renderDetailTable();
// Use _originalIndex when paths were filtered (e.g. by histogram
// column click in static mode) so the overlay lookup matches.
const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
const highlightIdx = paths[idx]?._originalIndex ?? idx;
this._app.websocketManager.request({
type: 'timing_highlight',
path_index: highlightIdx,
is_setup: this._currentTab === 'setup',
}).then(() => this._redrawAllLayers())
.catch(err => console.error('timing_highlight error:', err));
this._updateBtn.disabled = false;
this._updateBtn.textContent = 'Update';
}

_clearTimingHighlight() {
this._app.websocketManager
.request({type: 'timing_highlight', path_index: -1})
.then(() => this._redrawAllLayers());
}

_selectPathRow(idx) {
const rows = this._pathTable.querySelectorAll('tbody tr');
if (idx < 0 || idx >= rows.length) return;
this._selectedPathIndex = idx;
for (const row of rows) row.classList.remove('selected');
rows[idx].classList.add('selected');
rows[idx].scrollIntoView({block: 'nearest'});
this._pathTableContainer.focus();
this._renderDetailTable();
// Use _originalIndex when paths were filtered (e.g. by histogram
// column click in static mode) so the overlay lookup matches.
const paths =
this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
const highlightIdx = paths[idx]?._originalIndex ?? idx;
this._app.websocketManager
.request({
type: 'timing_highlight',
path_index: highlightIdx,
is_setup: this._currentTab === 'setup',
})
.then(() => this._redrawAllLayers())
.catch(err => console.error('timing_highlight error:', err));
}

_renderPathTable() {
const paths =
this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
this._pathCountLabel.textContent = paths.length + ' paths';

// Preserve column widths across re-renders.
const oldHeaders = this._pathTable.querySelectorAll('thead th');
const savedWidths = Array.from(oldHeaders, th => th.style.width);

this._pathTable.innerHTML = '';

const thead = document.createElement('thead');
const hr = document.createElement('tr');
for (const col of TimingWidget.PATH_COLS) {
const th = document.createElement('th');
th.textContent = col;
hr.appendChild(th);
}

_renderPathTable() {
const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
this._pathCountLabel.textContent = paths.length + ' paths';

// Preserve column widths across re-renders.
const oldHeaders = this._pathTable.querySelectorAll('thead th');
const savedWidths = Array.from(oldHeaders, th => th.style.width);

this._pathTable.innerHTML = '';

const thead = document.createElement('thead');
const hr = document.createElement('tr');
for (const col of TimingWidget.PATH_COLS) {
const th = document.createElement('th');
th.textContent = col;
hr.appendChild(th);
}
thead.appendChild(hr);
this._pathTable.appendChild(thead);

const tbody = document.createElement('tbody');
paths.forEach((p, idx) => {
const tr = document.createElement('tr');
if (idx === this._selectedPathIndex) tr.classList.add('selected');
const vals = [
p.end_clk,
fmtTime(p.required),
fmtTime(p.arrival),
fmtTime(p.slack),
fmtTime(p.skew),
fmtTime(p.path_delay),
p.logic_depth,
p.fanout,
p.start_pin,
p.end_pin,
];
vals.forEach((v, ci) => {
const td = document.createElement('td');
td.textContent = v;
if (ci === 3 && p.slack < 0) td.classList.add('slack-negative');
tr.appendChild(td);
});
tr.style.cursor = 'pointer';
tr.addEventListener('click', () => this._selectPathRow(idx));
tbody.appendChild(tr);
});
this._pathTable.appendChild(tbody);

// Restore previous widths if available, otherwise compute fresh.
if (savedWidths.length > 0 && savedWidths[0]) {
const newHeaders = this._pathTable.querySelectorAll('thead th');
this._pathTable.style.tableLayout = 'fixed';
newHeaders.forEach((th, i) => {
if (i < savedWidths.length) th.style.width = savedWidths[i];
});
} else {
makeResizableHeaders(this._pathTable);
}
thead.appendChild(hr);
this._pathTable.appendChild(thead);

const tbody = document.createElement('tbody');
paths.forEach((p, idx) => {
const tr = document.createElement('tr');
if (idx === this._selectedPathIndex) tr.classList.add('selected');
const vals = [
p.end_clk,
fmtTime(p.required),
fmtTime(p.arrival),
fmtTime(p.slack),
fmtTime(p.skew),
fmtTime(p.path_delay),
p.logic_depth,
p.fanout,
p.start_pin,
p.end_pin,
];
vals.forEach((v, ci) => {
const td = document.createElement('td');
td.textContent = v;
if (ci === 3 && p.slack < 0) td.classList.add('slack-negative');
tr.appendChild(td);
});
tr.style.cursor = 'pointer';
tr.addEventListener('click', () => this._selectPathRow(idx));
tbody.appendChild(tr);
});
this._pathTable.appendChild(tbody);

// Restore previous widths if available, otherwise compute fresh.
if (savedWidths.length > 0 && savedWidths[0]) {
const newHeaders = this._pathTable.querySelectorAll('thead th');
this._pathTable.style.tableLayout = 'fixed';
newHeaders.forEach((th, i) => {
if (i < savedWidths.length) th.style.width = savedWidths[i];
});
} else {
makeResizableHeaders(this._pathTable);
}

_selectDetailRow(idx) {
const rows = this._detailTable.querySelectorAll('tbody tr');
if (idx < 0 || idx >= rows.length) return;
this._selectedDetailIndex = idx;
for (const row of rows) {
row.classList.remove('timing-selected-row');
}
rows[idx].classList.add('timing-selected-row');
rows[idx].scrollIntoView({ block: 'nearest' });
this._detailTableContainer.focus();

const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
const path = paths[this._selectedPathIndex];
const nodes = this._detailTab === 'data' ? path.data_nodes : path.capture_nodes;
// Use _originalIndex when paths were filtered (e.g. by histogram
// column click in static mode) so the overlay lookup matches.
const highlightIdx = path._originalIndex ?? this._selectedPathIndex;
this._app.websocketManager.request({
type: 'timing_highlight',
path_index: highlightIdx,
is_setup: this._currentTab === 'setup',
pin_name: nodes[idx].pin,
}).then(() => this._redrawAllLayers());
}

_selectDetailRow(idx) {
const rows = this._detailTable.querySelectorAll('tbody tr');
if (idx < 0 || idx >= rows.length) return;
this._selectedDetailIndex = idx;
for (const row of rows) {
row.classList.remove('timing-selected-row');
}

_renderDetailTable() {
// Preserve column widths across re-renders.
const oldHeaders = this._detailTable.querySelectorAll('thead th');
const savedWidths = Array.from(oldHeaders, th => th.style.width);

this._detailTable.innerHTML = '';
this._selectedDetailIndex = -1;
const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
if (this._selectedPathIndex < 0 || this._selectedPathIndex >= paths.length) return;

const path = paths[this._selectedPathIndex];
const nodes = this._detailTab === 'data' ? path.data_nodes : path.capture_nodes;

const thead = document.createElement('thead');
const hr = document.createElement('tr');
for (const col of TimingWidget.DETAIL_COLS) {
const th = document.createElement('th');
th.textContent = col;
hr.appendChild(th);
}
thead.appendChild(hr);
this._detailTable.appendChild(thead);

const tbody = document.createElement('tbody');
nodes.forEach((n, idx) => {
const tr = document.createElement('tr');
if (n.clk) tr.classList.add('timing-clock-row');
const vals = [
n.pin,
n.fanout,
n.rise ? '↑' : '↓',
fmtTime(n.time),
fmtTime(n.delay),
fmtTime(n.slew),
fmtTime(n.load),
];
for (const v of vals) {
const td = document.createElement('td');
td.textContent = v;
tr.appendChild(td);
}
tr.style.cursor = 'pointer';
tr.addEventListener('click', () => this._selectDetailRow(idx));
tbody.appendChild(tr);
});
this._detailTable.appendChild(tbody);

if (savedWidths.length > 0 && savedWidths[0]) {
const newHeaders = this._detailTable.querySelectorAll('thead th');
this._detailTable.style.tableLayout = 'fixed';
newHeaders.forEach((th, i) => {
if (i < savedWidths.length) th.style.width = savedWidths[i];
});
} else {
makeResizableHeaders(this._detailTable);
// Pin column: set initial width to 30 characters
const pinTh = this._detailTable.querySelector('thead th');
if (pinTh) {
pinTh.style.width = '30ch';
}
}
rows[idx].classList.add('timing-selected-row');
rows[idx].scrollIntoView({block: 'nearest'});
this._detailTableContainer.focus();

const paths =
this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
const path = paths[this._selectedPathIndex];
const nodes =
this._detailTab === 'data' ? path.data_nodes : path.capture_nodes;
// Use _originalIndex when paths were filtered (e.g. by histogram
// column click in static mode) so the overlay lookup matches.
const highlightIdx = path._originalIndex ?? this._selectedPathIndex;
this._app.websocketManager
.request({
type: 'timing_highlight',
path_index: highlightIdx,
is_setup: this._currentTab === 'setup',
pin_name: nodes[idx].pin,
})
.then(() => this._redrawAllLayers());
}

_renderDetailTable() {
// Preserve column widths across re-renders.
const oldHeaders = this._detailTable.querySelectorAll('thead th');
const savedWidths = Array.from(oldHeaders, th => th.style.width);

this._detailTable.innerHTML = '';
this._selectedDetailIndex = -1;
const paths =
this._currentTab === 'setup' ? this._setupPaths : this._holdPaths;
if (this._selectedPathIndex < 0 || this._selectedPathIndex >= paths.length)
return;

const path = paths[this._selectedPathIndex];
const nodes =
this._detailTab === 'data' ? path.data_nodes : path.capture_nodes;

const thead = document.createElement('thead');
const hr = document.createElement('tr');
for (const col of TimingWidget.DETAIL_COLS) {
const th = document.createElement('th');
th.textContent = col;
hr.appendChild(th);
}

thead.appendChild(hr);
this._detailTable.appendChild(thead);

const tbody = document.createElement('tbody');
nodes.forEach((n, idx) => {
const tr = document.createElement('tr');
if (n.clk) tr.classList.add('timing-clock-row');
const vals = [
n.pin,
n.fanout,
n.rise ? '↑' : '↓',
fmtTime(n.time),
fmtTime(n.delay),
fmtTime(n.slew),
fmtTime(n.load),
];
for (const v of vals) {
const td = document.createElement('td');
td.textContent = v;
tr.appendChild(td);
}
tr.style.cursor = 'pointer';
tr.addEventListener('click', () => this._selectDetailRow(idx));
tbody.appendChild(tr);
});
this._detailTable.appendChild(tbody);

if (savedWidths.length > 0 && savedWidths[0]) {
const newHeaders = this._detailTable.querySelectorAll('thead th');
this._detailTable.style.tableLayout = 'fixed';
newHeaders.forEach((th, i) => {
if (i < savedWidths.length) th.style.width = savedWidths[i];
});
} else {
makeResizableHeaders(this._detailTable);
// Pin column: set initial width to 30 characters
const pinTh = this._detailTable.querySelector('thead th');
if (pinTh) {
pinTh.style.width = '30ch';
}
}
}
}

export function fmtTime(v) {
if (v === undefined || v === null) return '';
return typeof v === 'number' ? v.toFixed(4) : String(v);
if (v === undefined || v === null) return '';
return typeof v === 'number' ? v.toFixed(4) : String(v);
}

TimingWidget.PATH_COLS = ['Clock', 'Required', 'Arrival', 'Slack', 'Skew',
'Logic Delay', 'Logic Depth', 'Fanout', 'Start', 'End'];
TimingWidget.DETAIL_COLS = ['Pin', 'Fanout', 'R/F', 'Time', 'Delay', 'Slew', 'Load'];
TimingWidget.PATH_COLS = [
'Clock', 'Required', 'Arrival', 'Slack', 'Skew', 'Logic Delay', 'Logic Depth',
'Fanout', 'Start', 'End'
];
TimingWidget.DETAIL_COLS =
['Pin', 'Fanout', 'R/F', 'Time', 'Delay', 'Slew', 'Load'];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pull request includes several formatting changes (e.g., import style, array formatting) that are unrelated to the main task of removing the 'Update' button. Please separate these unrelated refactoring changes into a distinct pull request to keep the current PR focused.

@openroad-ci openroad-ci force-pushed the remove-button-update-static-GUI branch from 4902230 to 8973930 Compare May 17, 2026 07:14
@openroad-ci openroad-ci requested a review from a team as a code owner May 17, 2026 07:14
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

@openroad-ci openroad-ci force-pushed the remove-button-update-static-GUI branch from 8973930 to ecec639 Compare May 17, 2026 07:19
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

@maliberty
Copy link
Copy Markdown
Member

What are you doing that is reformatting the code? It obscures whatever the real change is in this PR.

@jorge-ferreira-pii
Copy link
Copy Markdown
Contributor

What are you doing that is reformatting the code? It obscures whatever the real change is in this PR.

Hi Matt, I use Claude to refactor what I've implemented, but I accidentally refactored the entire code. I apologize.

I will redo the fix.

@openroad-ci openroad-ci force-pushed the remove-button-update-static-GUI branch from ecec639 to 3169df9 Compare May 19, 2026 00:11
@github-actions github-actions Bot added size/S and removed size/XL labels May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

@jorge-ferreira-pii
Copy link
Copy Markdown
Contributor

1.* Widgets (charts-widget.js, clock-tree-widget.js, hierarchy-browser.js, timing-widget.js)

  • Hide "Update" buttons: The update button is now hidden when the UI detects it's running in static mode, as there is no backend to process the request.
  • Auto-initialization: Instead of waiting for user interaction, widgets now automatically call update() (via setTimeout) upon initialization in static mode to load the embedded data.
  • Dynamic Empty States: The default empty state messages were improved. Instead of displaying a hardcoded "Click 'Update' to load...", the UI now checks the environment. If it's a static report with no data, it dynamically changes to "No [type] data available".

2.* Tcl Console (main.js, style.css)

  • The Tcl input row is now completely hidden in static mode, preventing users from trying to type commands without a backend.
  • A new .tcl-static-notice CSS class was added to display a muted, italicized fallback message: "Tcl console is not available in saved reports."

3.* Static Export Dependencies (web.cpp)

  • Injected the missing Three.js module import (import * as THREE from 'https://esm.sh/three@0.160.0';) into the static HTML header generation. This ensures that the 3D Viewer features work seamlessly in standalone exported files.

@maliberty
Copy link
Copy Markdown
Member

ready for review?

@jorge-ferreira-pii
Copy link
Copy Markdown
Contributor

jorge-ferreira-pii commented May 19, 2026

ready for review?

I’m almost ready, but you gotta give me just a few more moments.
I'm doing a double-check to make sure it didn't sneak any bugs past me!

Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
@openroad-ci openroad-ci force-pushed the remove-button-update-static-GUI branch from 3169df9 to cfb6bd3 Compare May 20, 2026 04:17
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

@maliberty
Copy link
Copy Markdown
Member

This fails for me, in the chrome console:

Uncaught SyntaxError: The requested module './ui-utils.js' does not provide an export named 'isStaticMode' (at timing-widget.js:6:10)Understand this error

Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

The helper was checking app.wsManager.isStaticMode, but the app object
only ever exposes app.websocketManager. As a result isStaticMode() always
returned false, so the per-widget Update buttons (and auto-update calls)
were not actually hidden in saved/static HTML reports.

Signed-off-by: Matt Liberty <mliberty@precisioninno.com>
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

@maliberty maliberty enabled auto-merge May 21, 2026 21:58
@maliberty maliberty merged commit b0ea7ef into The-OpenROAD-Project:master May 21, 2026
15 of 16 checks passed
@maliberty maliberty deleted the remove-button-update-static-GUI branch May 21, 2026 22:23
@oharboe
Copy link
Copy Markdown
Collaborator

oharboe commented May 22, 2026

😌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove "Update" button in static HTML GUI

4 participants