mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-04 11:07:35 +01:00
feat(plugin): make page tabs and sub-tabs addressable by URLs (#8626)
* feat(plugin): make page tabs and sub-tabs addressable by URLs Update the plugin page so that clicking tabs and sub-tabs pushes browser history via history.pushState(), enabling bookmarkable URLs, browser back/forward navigation, and shareable links. URL mapping: - /plugin → Overview tab - /plugin/configuration → Configuration sub-tab - /plugin/detection → Job Detection sub-tab - /plugin/queue → Job Queue sub-tab - /plugin/execution → Job Execution sub-tab Job-type-specific URLs use the ?job= query parameter (e.g., /plugin/configuration?job=vacuum) so that a specific job type tab is pre-selected on page load. Changes: - Add initialJob parameter to Plugin() template and handler - Extract ?job= query param in renderPluginPage handler - Add buildPluginURL/updateURL helpers in JavaScript - Push history state on top-tab, sub-tab, and job-type clicks - Listen for popstate to restore tab state on back/forward - Replace initial history entry on page load via replaceState * make popstate handler async with proper error handling Await loadDescriptorAndConfig so data loading completes before rendering dependent views. Log errors instead of silently swallowing them.
This commit is contained in:
1 parent
baae672b6f
commit
6fc0489dd8
3 files changed
+86
-8
No files matched your search
@@ -53,7 +53,8 @@ func (h *PluginHandlers) ShowPluginMonitoring(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
func (h *PluginHandlers) renderPluginPage(w http.ResponseWriter, r *http.Request, page string) {
|
||||
component := app.Plugin(page)
|
||||
initialJob := r.URL.Query().Get("job")
|
||||
component := app.Plugin(page, initialJob)
|
||||
viewCtx := layout.NewViewContext(r, dash.UsernameFromContext(r.Context()), dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, component)
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package app
|
||||
|
||||
templ Plugin(page string) {
|
||||
templ Plugin(page string, initialJob string) {
|
||||
{{
|
||||
currentPage := page
|
||||
if currentPage == "" {
|
||||
currentPage = "overview"
|
||||
}
|
||||
}}
|
||||
<div class="container-fluid" id="plugin-page" data-plugin-page={ currentPage }>
|
||||
<div class="container-fluid" id="plugin-page" data-plugin-page={ currentPage } data-plugin-job={ initialJob }>
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
@@ -589,6 +589,7 @@ templ Plugin(page string) {
|
||||
jobTypes: [],
|
||||
lastDetectionByJobType: {},
|
||||
initialPage: String(page.getAttribute('data-plugin-page') || 'overview').trim().toLowerCase(),
|
||||
initialJob: String(page.getAttribute('data-plugin-job') || '').trim(),
|
||||
activeTopTab: 'overview',
|
||||
activeSubTab: 'configuration',
|
||||
initialNavigationApplied: false,
|
||||
@@ -717,6 +718,28 @@ templ Plugin(page string) {
|
||||
return 'configuration';
|
||||
}
|
||||
|
||||
function buildPluginURL(topTab, subTab, jobType) {
|
||||
if (topTab === 'overview') {
|
||||
return '/plugin';
|
||||
}
|
||||
var effectiveSubTab = normalizeSubTab(subTab);
|
||||
var path = '/plugin/' + encodeURIComponent(effectiveSubTab);
|
||||
var jt = String(jobType || '').trim();
|
||||
if (jt) {
|
||||
path += '?job=' + encodeURIComponent(jt);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function updateURL(replace) {
|
||||
var url = buildPluginURL(state.activeTopTab, state.activeSubTab, state.selectedJobType);
|
||||
if (replace) {
|
||||
history.replaceState({ pluginTopTab: state.activeTopTab, pluginSubTab: state.activeSubTab, pluginJob: state.selectedJobType }, '', url);
|
||||
} else {
|
||||
history.pushState({ pluginTopTab: state.activeTopTab, pluginSubTab: state.activeSubTab, pluginJob: state.selectedJobType }, '', url);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a job type item (string or object) into a safe object shape
|
||||
function parseJobTypeItem(item) {
|
||||
var jobType = '';
|
||||
@@ -782,8 +805,13 @@ templ Plugin(page string) {
|
||||
state.activeTopTab = 'overview';
|
||||
return;
|
||||
}
|
||||
var firstItem = parseJobTypeItem(state.jobTypes[0]);
|
||||
state.selectedJobType = firstItem.jobType;
|
||||
// If a specific job type was provided in the URL, use it
|
||||
if (state.initialJob && hasJobType(state.initialJob)) {
|
||||
state.selectedJobType = state.initialJob;
|
||||
} else {
|
||||
var firstItem = parseJobTypeItem(state.jobTypes[0]);
|
||||
state.selectedJobType = firstItem.jobType;
|
||||
}
|
||||
state.activeTopTab = topTabKeyForJobType(state.selectedJobType);
|
||||
}
|
||||
|
||||
@@ -3015,6 +3043,7 @@ templ Plugin(page string) {
|
||||
if (topTabKey === 'overview') {
|
||||
state.activeTopTab = 'overview';
|
||||
renderNavigationState();
|
||||
updateURL(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3026,6 +3055,7 @@ templ Plugin(page string) {
|
||||
state.selectedJobType = jobType;
|
||||
ensureActiveNavigation();
|
||||
renderNavigationState();
|
||||
updateURL(false);
|
||||
if (state.loadedJobType !== jobType) {
|
||||
try {
|
||||
await loadDescriptorAndConfig(jobType, false);
|
||||
@@ -3053,6 +3083,7 @@ templ Plugin(page string) {
|
||||
var subTabKey = normalizeSubTab(subTab.getAttribute('data-plugin-subtab'));
|
||||
state.activeSubTab = subTabKey;
|
||||
renderNavigationState();
|
||||
updateURL(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3076,11 +3107,44 @@ templ Plugin(page string) {
|
||||
renderNavigationState();
|
||||
await refreshAll();
|
||||
|
||||
// Set the initial browser history state to match the current view
|
||||
updateURL(true);
|
||||
|
||||
state.refreshTimer = setInterval(function() {
|
||||
refreshAll();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', async function(event) {
|
||||
var s = event.state;
|
||||
if (!s || !s.pluginTopTab) {
|
||||
// No plugin state — treat as overview
|
||||
state.activeTopTab = 'overview';
|
||||
renderNavigationState();
|
||||
return;
|
||||
}
|
||||
state.activeTopTab = String(s.pluginTopTab || 'overview');
|
||||
state.activeSubTab = normalizeSubTab(s.pluginSubTab);
|
||||
var jobType = String(s.pluginJob || '').trim();
|
||||
if (jobType) {
|
||||
state.selectedJobType = jobType;
|
||||
}
|
||||
ensureActiveNavigation();
|
||||
renderNavigationState();
|
||||
// If a job type is selected and not loaded yet, load it
|
||||
if (state.selectedJobType && state.loadedJobType !== state.selectedJobType) {
|
||||
try {
|
||||
await loadDescriptorAndConfig(state.selectedJobType, false);
|
||||
} catch (e) {
|
||||
console.error('Failed to load data on popstate navigation:', e);
|
||||
}
|
||||
}
|
||||
renderQueueJobs();
|
||||
renderDetectionJobs();
|
||||
renderExecutionJobs();
|
||||
renderExecutionActivities();
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (state.refreshTimer) {
|
||||
clearInterval(state.refreshTimer);
|
||||
|
||||
@@ -8,7 +8,7 @@ package app
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Plugin(page string) templ.Component {
|
||||
func Plugin(page string, initialJob string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
@@ -40,13 +40,26 @@ func Plugin(page string) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(currentPage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin.templ`, Line: 10, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 10, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"d-flex justify-content-between align-items-center flex-wrap gap-2\"><div><h2 class=\"mb-0\"><i class=\"fas fa-plug me-2\"></i>Workers</h2><p class=\"text-muted mb-0\">Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.</p></div><div class=\"btn-group\"><button type=\"button\" class=\"btn btn-outline-secondary\" id=\"plugin-refresh-all-btn\"><i class=\"fas fa-sync-alt me-1\"></i>Refresh</button></div></div></div></div><div class=\"row mb-3\"><div class=\"col-12\"><ul class=\"nav nav-tabs\" id=\"plugin-top-tabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-top-tab=\"overview\"><i class=\"fas fa-chart-line me-1\"></i>Overview</button></li></ul></div></div><div class=\"row mb-3 d-none\" id=\"plugin-subtab-row\"><div class=\"col-12\"><ul class=\"nav nav-pills gap-2\" id=\"plugin-subtabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-subtab=\"configuration\"><i class=\"fas fa-sliders-h me-1\"></i>Configuration</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"detection\"><i class=\"fas fa-search me-1\"></i>Job Detection</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"queue\"><i class=\"fas fa-list me-1\"></i>Job Queue</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"execution\"><i class=\"fas fa-tasks me-1\"></i>Job Execution</button></li></ul></div></div><div class=\"plugin-section plugin-section-overview\"><div class=\"row mb-4\"><div class=\"col-md-3 mb-3\"><div class=\"card border-info h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Workers</div><h4 class=\"mb-0\" id=\"plugin-status-workers\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-warning h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Active Jobs</div><h4 class=\"mb-0\" id=\"plugin-status-active-jobs\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-success h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Activities (recent)</div><h4 class=\"mb-0\" id=\"plugin-status-activities\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-secondary h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Next Run</div><h4 class=\"mb-0 plugin-scheduler-next-run\">-</h4><div class=\"text-muted small plugin-scheduler-next-run-meta\"></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-chart-bar me-2\"></i>Per Job Type Summary</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Active Jobs</th><th>Recent Activities</th></tr></thead> <tbody id=\"plugin-jobtype-summary-body\"><tr><td colspan=\"3\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header d-flex justify-content-between align-items-center flex-wrap gap-2\"><h5 class=\"mb-0\"><i class=\"fas fa-clock me-2\"></i>Scheduler State</h5><small class=\"text-muted\">Sequential scheduler with per-job runtime limits</small></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Enabled</th><th>Detector</th><th>In Flight</th><th>Max Runtime</th><th>Exec Global</th><th>Exec/Worker</th><th>Executor Workers</th><th>Effective Exec</th><th>Last Run</th></tr></thead> <tbody id=\"plugin-scheduler-table-body\"><tr><td colspan=\"10\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-users me-2\"></i>Workers</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\" style=\"max-height: 560px; overflow-y: auto;\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light sticky-top\"><tr><th>Worker</th><th>Address</th><th>Capabilities</th><th>Load</th></tr></thead> <tbody id=\"plugin-workers-table-body\"><tr><td colspan=\"4\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div></div><div class=\"plugin-section plugin-section-configuration\"><div class=\"row mb-4\"><div class=\"col-lg-8 mb-Line truncated
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" data-plugin-job=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(initialJob)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 10, Col: 111}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"d-flex justify-content-between align-items-center flex-wrap gap-2\"><div><h2 class=\"mb-0\"><i class=\"fas fa-plug me-2\"></i>Workers</h2><p class=\"text-muted mb-0\">Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.</p></div><div class=\"btn-group\"><button type=\"button\" class=\"btn btn-outline-secondary\" id=\"plugin-refresh-all-btn\"><i class=\"fas fa-sync-alt me-1\"></i>Refresh</button></div></div></div></div><div class=\"row mb-3\"><div class=\"col-12\"><ul class=\"nav nav-tabs\" id=\"plugin-top-tabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-top-tab=\"overview\"><i class=\"fas fa-chart-line me-1\"></i>Overview</button></li></ul></div></div><div class=\"row mb-3 d-none\" id=\"plugin-subtab-row\"><div class=\"col-12\"><ul class=\"nav nav-pills gap-2\" id=\"plugin-subtabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-subtab=\"configuration\"><i class=\"fas fa-sliders-h me-1\"></i>Configuration</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"detection\"><i class=\"fas fa-search me-1\"></i>Job Detection</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"queue\"><i class=\"fas fa-list me-1\"></i>Job Queue</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"execution\"><i class=\"fas fa-tasks me-1\"></i>Job Execution</button></li></ul></div></div><div class=\"plugin-section plugin-section-overview\"><div class=\"row mb-4\"><div class=\"col-md-3 mb-3\"><div class=\"card border-info h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Workers</div><h4 class=\"mb-0\" id=\"plugin-status-workers\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-warning h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Active Jobs</div><h4 class=\"mb-0\" id=\"plugin-status-active-jobs\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-success h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Activities (recent)</div><h4 class=\"mb-0\" id=\"plugin-status-activities\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-secondary h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Next Run</div><h4 class=\"mb-0 plugin-scheduler-next-run\">-</h4><div class=\"text-muted small plugin-scheduler-next-run-meta\"></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-chart-bar me-2\"></i>Per Job Type Summary</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Active Jobs</th><th>Recent Activities</th></tr></thead> <tbody id=\"plugin-jobtype-summary-body\"><tr><td colspan=\"3\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header d-flex justify-content-between align-items-center flex-wrap gap-2\"><h5 class=\"mb-0\"><i class=\"fas fa-clock me-2\"></i>Scheduler State</h5><small class=\"text-muted\">Sequential scheduler with per-job runtime limits</small></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Enabled</th><th>Detector</th><th>In Flight</th><th>Max Runtime</th><th>Exec Global</th><th>Exec/Worker</th><th>Executor Workers</th><th>Effective Exec</th><th>Last Run</th></tr></thead> <tbody id=\"plugin-scheduler-table-body\"><tr><td colspan=\"10\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-users me-2\"></i>Workers</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\" style=\"max-height: 560px; overflow-y: auto;\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light sticky-top\"><tr><th>Worker</th><th>Address</th><th>Capabilities</th><th>Load</th></tr></thead> <tbody id=\"plugin-workers-table-body\"><tr><td colspan=\"4\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div></div><div class=\"plugin-section plugin-section-configuration\"><div class=\"row mb-4\"><div class=\"col-lg-8 mb-Line truncated
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user