Frontend Developer Guide for Job Portal¶
Purpose¶
This document is intended for frontend developers who need to maintain, extend, or debug the Job Portal implementation in domain-services-ops.
It focuses on:
- how the portal is driven by
portal.json - how page definitions are mapped to React components
- how authentication tokens are resolved and passed into each page
- how
JobListandAutomationListare wired - what configuration fields are actively used
- important implementation caveats in the current codebase
High-Level Architecture¶
The portal is config-driven.
At runtime:
- the app loads
/portal.json AppStateStoreparses the file and extracts page definitions plus auth hosts- the app initializes
AuthServiceusing the discovered auth hosts - the sidebar and routes are built from the
pagesarray - each page wrapper converts config plus session tokens into props for the shared library components
The practical split is:
apps/domain-services-ops/src/*: app-specific composition, routing, and token wiringlibs/shared-lib/*: reusable page implementations and API logic
portal.json as the Source of Truth¶
The application fetches portal.json from the web root through AppStateStore.
Important behavior:
- the file is fetched with a cache-busting query string
- the resulting
pagesarray becomes the runtime route registry - auth hosts are collected from:
page.configuration.authHost- each
dataSource.authHost - each
dataSource.authHostJobLog
This means portal.json does more than define navigation. It also indirectly defines which auth tokens the app must prepare.
Top-level fields¶
Common top-level fields in portal.json:
siteNameauthHostlastDayslogostylepages
What is actively used in the app:
siteName,lastDays,style, andlogoare read inAppStateStorepagesdrives rendering and navigation- top-level
authHostexists in config, but page auth resolution is mainly derived from each page configuration and data source
Page definition shape¶
The page model is defined in the portal types layer.
A typical page object contains:
nametypeconfigurationenabled- optional metadata-like fields used by some page types
Important note:
- the TypeScript interface is looser than the real JSON usage
- actual page behavior depends on what each wrapper component reads from
configuration
Route Generation and Navigation¶
Routes are generated dynamically in the main dashboard router.
How route paths are derived¶
The route path is created by the route helper:
page?.name?.replace(/\s/g, '-').toLowerCase();
Examples:
Jobs-Minion->jobs-minionUser Groups->user-groupsAutomation->automation
This means:
- the sidebar URL is based on
name, nottype - changing
namechanges the route - links between pages that assume a route pattern can break if the page name changes unexpectedly
Page type to component mapping¶
Main.tsx maps page.type to wrapper components:
Accounts->AccountsJobList->JobListLogList->LogListUserGroups->UserGroupsTimeseriesExplorer->TimeseriesExplorerScalarList->ScalarsAutomationList->AutomationList
To introduce a new page type, you must update:
PageTypeenum in the portal types- icon mapping in the dashboard header
- component switch in the main router
portal.jsonpage entry
Enabled pages¶
Both sidebar rendering and route registration filter pages using:
page.enabled === true- or
page.enabled === undefined
So:
- omitted
enabledmeans the page is visible enabled: falsehides it from the UI
Auth and Token Resolution¶
Authentication is orchestrated by AppStateStore.
Auth host discovery¶
When config is loaded, the store collects all unique auth hosts from page configuration.
Sources include:
configuration.authHostconfiguration.dataSources[].authHostconfiguration.dataSources[].authHostJobLog
Those hosts are used to initialize AuthService.
Current page token handling¶
setCurrentPageConfig attempts to set appSession.pageAccessToken from page.configuration.authHost.
This is mainly relevant for pages like:
AccountsUserGroups
Those pages pass:
appSession.pageAccessToken || accessToken
into shared components.
Important limitation:
setCurrentPageConfigonly looks atpage.configuration.authHost- pages driven purely by
dataSources[].authHostdo not usepageAccessToken JobListandAutomationListresolve tokens separately fromauthSession.accessTokenList
fetchUrl token refresh¶
The shared API helper contains fetchUrl.
Behavior:
- if an
authHostargument is passed and the response is401, it attempts token refresh - refresh updates:
refreshTokenaccessTokenaccessTokenListexpirationin local storage
This is relevant because:
- some API wrappers pass
authHost - some do not
- token refresh behavior is therefore not fully consistent across all API calls
JobList Frontend Flow¶
The app-level JobList wrapper prepares configuration and tokens before rendering the shared job list component.
What the wrapper does¶
The wrapper:
- reads
configuration - reads
authServiceandstartTimeUtcfrom the app store - injects
tokenby matchingdataSource.authHost - injects
tokenJobLogby matchingdataSource.authHostJobLog - computes the effective
startTimeUtc - passes all resolved props into the shared
DHIJobList
Config fields used by JobList wrapper¶
Fields actively consumed in the wrapper:
dataSourcesdateTimeFormattimeZoneparameterstaskIdAfterLastPeriodautoRefreshIntervalSecondsnewSignalRDataObjLogAreaDisabledrangeDaysdisabledColumns
Date window behavior¶
The wrapper calculates startTimeUtc as:
now - rangeDaysifrangeDaysis provided- otherwise
observableStates.startTimeUtc
And observableStates.startTimeUtc comes from the top-level lastDays in portal.json.
This creates an override hierarchy:
- page-level
rangeDays - portal-level
lastDays
Shared JobList behavior¶
The main implementation lives in the shared job list component.
It is responsible for:
- querying jobs via
executeJobQuery - displaying the grid
- applying filters and grouping
- opening the right-side job detail panel
- subscribing to SignalR events
- fetching logs for the selected job
JobList data mapping¶
The shared component maps raw API data into UI rows, including:
taskIdstatushostIdrequested,started,finisheddurationdelay- job log source fields such as
tokenJobLog,hostJobLog,connectionJobLog
It also copies dynamic job parameters into row fields so extra columns can be configured via parameters.
SignalR integration¶
JobList connects to:
${source.host}/notificationhub
and subscribes to:
JobUpdatedJobAdded
The connection uses the page data source token.
Config fields that matter here:
newSignalRDataObjconnectionhosttoken
newSignalRDataObj changes how the event payload is parsed:
true: camelCase payload shapefalseor omitted: PascalCase payload shape
Job detail panel¶
The detail panel is implemented in the shared job detail helper.
It supports:
- live log refresh
- configurable refresh interval
Cancel JobforInProgressRemove JobforPending
Related APIs:
cancelJobupdateJobStatusfailJob
JobList config example¶
Typical portal.json entry:
{
"name": "Jobs-Minion",
"type": "JobList",
"enabled": true,
"configuration": {
"dataSources": [
{
"authHost": "http://DS-DEV1:81",
"host": "http://DS-DEV1:82",
"connection": "wf-jobs-Minion",
"authHostJobLog": "http://DS-DEV1:81",
"hostJobLog": "http://DS-DEV1:82",
"connectionJobLog": "wf-logs"
}
],
"dateTimeFormat": "yyyy-MM-dd HH:mm:ss",
"timeZone": "Asia/Jakarta",
"rangeDays": 7,
"LogAreaDisabled": true,
"disabledColumns": ["accountId"]
}
}
JobList caveats¶
There are some implementation details to keep in mind:
JobListhas special fallback routing logic inMain.tsxfor paths containingjob- the shared component stores column widths in local storage under
columnWidths:joblist - SignalR update mapping currently assumes some values from
dataSources[0] - the code itself contains comments indicating that this first-data-source assumption is not ideal
If you plan to support multi-source job pages more heavily, review:
- the shared
JobListimplementation
especially the jobUpdated and jobAdded flows.
AutomationList Frontend Flow¶
The app-level AutomationList wrapper resolves configuration and auth before rendering the shared automation list component.
What the wrapper does¶
The wrapper:
- reads the page configuration
- resolves an access token using each
dataSource.authHost - passes the first resolved data source into the shared
AutomationsList
Important detail:
- although config accepts a
dataSourcesarray, the wrapper effectively uses onlydataSources[0]
Config fields used by the wrapper¶
Actively consumed:
dataSourcesdisabledColumns
Currently hardcoded or not fully passed through:
jobReferringPageis hardcoded to'jobs'in the wrapperdisabledTextFieldis not passed from the wrapper in the current implementationdisabledTriggerNowis not passed from the wrapper in the current implementation
This is important because portal.json contains fields like:
disabledTriggerNowjobRefferingPagedisabledTextField
but the wrapper currently does not forward them all.
Shared AutomationList behavior¶
The main implementation lives in the shared automation list component.
It is responsible for:
- fetching the automation list
- polling every 30 seconds
- rendering the grid
- opening automation detail dialogs
- opening create or update dialogs
- executing
Trigger Now - viewing latest logs
- deleting automations
- toggling automation enabled state
Automation actions and relationship to jobs¶
The automation action menu exposes:
Latest LogLatest Met LogTrigger NowViewEditDeleteEnabled
The jobId cell renders a link constructed from:
pageJobrow.hostGroup.toLowerCase()row.lastJob?.id
Specifically:
const url = `${pageJob}-${row.hostGroup.toLowerCase()}?jobId=${row.lastJob?.id}`;
This means the automation-to-job deep link depends on naming conventions.
If:
pageJobis'jobs'hostGroupis'Minion'
then the target path becomes:
jobs-minion?jobId=<id>
This is why page naming in portal.json matters.
Why portal.json and hostGroup must stay aligned¶
The job page route is derived from page.name, while the automation deep link is derived from hostGroup.
That creates an implicit convention:
- job page name should match
jobs-${hostGroup.toLowerCase()}
For example:
- job page name:
Jobs-Minion - generated route:
jobs-minion - automation host group:
Minion - deep link target:
jobs-minion?jobId=...
If you rename the job page or change host group naming without aligning both sides, the deep link from automation to job will break.
Trigger Now implementation¶
In the shared automation dialog, Trigger Now leads to job submission through:
executeJobin the automation API layer
Important detail:
- this does not call the generic jobs endpoint
- it posts to
/api/temp-automations
So when debugging Trigger Now, inspect:
- automation payload shape
- auth token selection
- backend expectation for
/api/temp-automations
Automation config example¶
Typical config:
{
"name": "Automation",
"type": "AutomationList",
"enabled": true,
"configuration": {
"dataSources": [
{
"host": "http://DS-DEV1:82",
"connection": "wf-scalars",
"connectionJobLog": "wf-logs",
"connectionJobPrefix": "wf-jobs-"
}
],
"disabledTriggerNow": false,
"jobRefferingPage": "jobs",
"disabledTextField": {
"name": true,
"group": true
}
}
}
Automation caveats¶
Current code caveats:
- the config key is spelled
jobRefferingPagein JSON, but the shared prop isjobReferringPage - the wrapper currently hardcodes
jobReferringPage={'jobs'} disabledTriggerNowexists in shared props but is not forwarded by the wrapperdisabledTextFieldexists in shared props but is not forwarded by the wrapperconnectionJobPrefixexists in config and types, but is not actively used in the app wrapper path we traced
If you want full config-driven behavior for automation pages, update:
- the app-level
AutomationListwrapper
portal.json Field Reference for Job and Automation Pages¶
JobList page fields¶
Common fields and current behavior:
name- used for sidebar label
- used to derive the route slug
type- must be
JobList enabled- controls visibility and route registration
configuration.dataSources- required for API and SignalR integration
configuration.dataSources[].authHost- used to resolve the job API token
configuration.dataSources[].host- used as the API base URL and SignalR host
configuration.dataSources[].connection- used as the jobs connection id
configuration.dataSources[].authHostJobLog- used to resolve the job log token
configuration.dataSources[].hostJobLog- used to query job logs
configuration.dataSources[].connectionJobLog- used to query log records
configuration.dateTimeFormat- controls displayed timestamps
configuration.timeZone- controls timezone conversion
configuration.rangeDays- overrides the default portal-level time window
configuration.LogAreaDisabled- controls read-only state in the log area
configuration.disabledColumns- hides columns in the grid
configuration.parameters- adds extra parameter columns to the grid
configuration.taskIdAfterLastPeriod- shortens displayed task IDs
configuration.autoRefreshIntervalSeconds- controls periodic list refresh
configuration.newSignalRDataObj- changes SignalR payload parsing mode
AutomationList page fields¶
Common fields and current behavior:
name- used for sidebar label and route slug
type- must be
AutomationList enabled- controls visibility and route registration
configuration.dataSources- required
configuration.dataSources[].authHost- should exist if token resolution is required from auth session
configuration.dataSources[].host- API base URL
configuration.dataSources[].connection- scalar/log related connection id
configuration.dataSources[].connectionJobLog- used by automation-related log flows
configuration.dataSources[].connectionJobPrefix- present in config/types, but verify actual backend dependency before relying on it
configuration.disabledColumns- forwarded by the wrapper
configuration.disabledTriggerNow- supported by shared props, but not forwarded by current wrapper
configuration.jobRefferingPage- present in config, but effectively bypassed by wrapper hardcoding
configuration.disabledTextField- supported by shared props, but not forwarded by current wrapper
Adding a New Job Portal Page¶
To add a new job page such as Jobs-Alpha:
- add a new page entry to
portal.json - set
typetoJobList - provide the required
dataSources - set
nameto a route-friendly value such asJobs-Alpha - ensure the corresponding automation
hostGroupconvention matches the route if you want deep linking from automation
Example:
{
"name": "Jobs-Alpha",
"type": "JobList",
"enabled": true,
"configuration": {
"dataSources": [
{
"authHost": "http://AUTH-HOST",
"host": "http://API-HOST",
"connection": "wf-jobs-Alpha",
"authHostJobLog": "http://AUTH-HOST",
"hostJobLog": "http://API-HOST",
"connectionJobLog": "wf-logs"
}
],
"dateTimeFormat": "yyyy-MM-dd HH:mm:ss",
"timeZone": "Asia/Jakarta",
"rangeDays": 7
}
}
Adding or Fixing an Automation Page¶
To add a new automation page safely:
- add a page entry with
type: "AutomationList" - define
dataSources - ensure there is a matching job page naming convention if the automation should deep link to jobs
- confirm whether the wrapper needs to forward extra configuration fields
If you need true config-driven behavior, likely wrapper changes are needed in:
- the app-level
AutomationListwrapper
Recommended improvements:
- forward
disabledTriggerNow - forward
disabledTextField - use config-driven
jobRefferingPage - normalize spelling between
jobRefferingPageandjobReferringPage
Known Risks and Implementation Gaps¶
These are worth knowing before extending the portal:
- route slugs depend on
name, so renaming a page can break deep links - automation-to-job linking depends on
hostGroupnaming convention - some
portal.jsonfields exist but are not fully wired through wrappers JobListcurrently has several first-data-source assumptions- token refresh behavior is inconsistent depending on whether API wrappers pass
authHost - config typing in the portal types is less expressive than actual runtime usage
withParamsexists inportal.json, but from the traced frontend path it does not appear to be actively consumed in the pages we reviewed
Recommended Developer Checklist¶
Before changing portal.json or adding a new page, verify:
- whether the page
namechange will alter the route - whether any automation deep link depends on that route
- whether all required auth hosts are discoverable by
AppStateStore - whether the page wrapper actually forwards the config fields you added
- whether SignalR payload shape matches
newSignalRDataObj - whether the page uses one data source or effectively assumes only the first data source
- whether token refresh is expected for the API calls involved
Suggested Refactor Targets¶
If this area is going to evolve, the highest-value cleanup items are:
- make wrapper props fully config-driven
- unify
jobRefferingPageversusjobReferringPage - formalize a typed config schema for each page type
- centralize token injection logic for data sources
- remove hidden route conventions based on page name and host group
- improve multi-data-source support in
JobList
Closing Notes¶
For this portal, portal.json is not just a static navigation file. It drives routing, token host discovery, job page behavior, and the implicit relationship between automation pages and job pages.
When debugging the frontend, always inspect these layers together:
portal.json- app wrapper component
- shared-lib page component
- API helper and token behavior
That sequence will usually reveal whether the issue is:
- configuration-driven
- wrapper-driven
- shared component behavior
- or backend integration related