diff --git a/weed/admin/handlers/plugin_handlers.go b/weed/admin/handlers/plugin_handlers.go index cb3782d93..b4d0ec74b 100644 --- a/weed/admin/handlers/plugin_handlers.go +++ b/weed/admin/handlers/plugin_handlers.go @@ -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) diff --git a/weed/admin/view/app/plugin.templ b/weed/admin/view/app/plugin.templ index 0aea3a813..e1b897057 100644 --- a/weed/admin/view/app/plugin.templ +++ b/weed/admin/view/app/plugin.templ @@ -1,13 +1,13 @@ package app -templ Plugin(page string) { +templ Plugin(page string, initialJob string) { {{ currentPage := page if currentPage == "" { currentPage = "overview" } }} -
+
@@ -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); diff --git a/weed/admin/view/app/plugin_templ.go b/weed/admin/view/app/plugin_templ.go index 680723a24..6b520413e 100644 --- a/weed/admin/view/app/plugin_templ.go +++ b/weed/admin/view/app/plugin_templ.go @@ -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, "\">

Workers

Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.

Workers

0

Active Jobs

0

Activities (recent)

0

Next Run

-

Per Job Type Summary
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Sequential scheduler with per-job runtime limits
Job TypeEnabledDetectorIn FlightMax RuntimeExec GlobalExec/WorkerExecutor WorkersEffective ExecLast Run
Loading...
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
How often to check for new work.
Next Run
Scheduler
-
Not scheduled
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") + 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, "\">

Workers

Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.

Workers

0

Active Jobs

0

Activities (recent)

0

Next Run

-

Per Job Type Summary
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Sequential scheduler with per-job runtime limits
Job TypeEnabledDetectorIn FlightMax RuntimeExec GlobalExec/WorkerExecutor WorkersEffective ExecLast Run
Loading...
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
How often to check for new work.
Next Run
Scheduler
-
Not scheduled
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }