Skip to content

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 JobList and AutomationList are wired
  • what configuration fields are actively used
  • important implementation caveats in the current codebase

High-Level Architecture

The portal is config-driven.

At runtime:

  1. the app loads /portal.json
  2. AppStateStore parses the file and extracts page definitions plus auth hosts
  3. the app initializes AuthService using the discovered auth hosts
  4. the sidebar and routes are built from the pages array
  5. 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 wiring
  • libs/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 pages array 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:

  • siteName
  • authHost
  • lastDays
  • logo
  • style
  • pages

What is actively used in the app:

  • siteName, lastDays, style, and logo are read in AppStateStore
  • pages drives rendering and navigation
  • top-level authHost exists 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:

  • name
  • type
  • configuration
  • enabled
  • 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-minion
  • User Groups -> user-groups
  • Automation -> automation

This means:

  • the sidebar URL is based on name, not type
  • changing name changes 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 -> Accounts
  • JobList -> JobList
  • LogList -> LogList
  • UserGroups -> UserGroups
  • TimeseriesExplorer -> TimeseriesExplorer
  • ScalarList -> Scalars
  • AutomationList -> AutomationList

To introduce a new page type, you must update:

  1. PageType enum in the portal types
  2. icon mapping in the dashboard header
  3. component switch in the main router
  4. portal.json page entry

Enabled pages

Both sidebar rendering and route registration filter pages using:

  • page.enabled === true
  • or page.enabled === undefined

So:

  • omitted enabled means the page is visible
  • enabled: false hides 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.authHost
  • configuration.dataSources[].authHost
  • configuration.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:

  • Accounts
  • UserGroups

Those pages pass:

  • appSession.pageAccessToken || accessToken

into shared components.

Important limitation:

  • setCurrentPageConfig only looks at page.configuration.authHost
  • pages driven purely by dataSources[].authHost do not use pageAccessToken
  • JobList and AutomationList resolve tokens separately from authSession.accessTokenList

fetchUrl token refresh

The shared API helper contains fetchUrl.

Behavior:

  • if an authHost argument is passed and the response is 401, it attempts token refresh
  • refresh updates:
  • refreshToken
  • accessToken
  • accessTokenList
  • expiration in 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 authService and startTimeUtc from the app store
  • injects token by matching dataSource.authHost
  • injects tokenJobLog by matching dataSource.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:

  • dataSources
  • dateTimeFormat
  • timeZone
  • parameters
  • taskIdAfterLastPeriod
  • autoRefreshIntervalSeconds
  • newSignalRDataObj
  • LogAreaDisabled
  • rangeDays
  • disabledColumns

Date window behavior

The wrapper calculates startTimeUtc as:

  • now - rangeDays if rangeDays is provided
  • otherwise observableStates.startTimeUtc

And observableStates.startTimeUtc comes from the top-level lastDays in portal.json.

This creates an override hierarchy:

  1. page-level rangeDays
  2. 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:

  • taskId
  • status
  • hostId
  • requested, started, finished
  • duration
  • delay
  • 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:

  • JobUpdated
  • JobAdded

The connection uses the page data source token.

Config fields that matter here:

  • newSignalRDataObj
  • connection
  • host
  • token

newSignalRDataObj changes how the event payload is parsed:

  • true: camelCase payload shape
  • false or 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 Job for InProgress
  • Remove Job for Pending

Related APIs:

  • cancelJob
  • updateJobStatus
  • failJob

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:

  • JobList has special fallback routing logic in Main.tsx for paths containing job
  • 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 JobList implementation

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 dataSources array, the wrapper effectively uses only dataSources[0]

Config fields used by the wrapper

Actively consumed:

  • dataSources
  • disabledColumns

Currently hardcoded or not fully passed through:

  • jobReferringPage is hardcoded to 'jobs' in the wrapper
  • disabledTextField is not passed from the wrapper in the current implementation
  • disabledTriggerNow is not passed from the wrapper in the current implementation

This is important because portal.json contains fields like:

  • disabledTriggerNow
  • jobRefferingPage
  • disabledTextField

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 Log
  • Latest Met Log
  • Trigger Now
  • View
  • Edit
  • Delete
  • Enabled

The jobId cell renders a link constructed from:

  • pageJob
  • row.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:

  • pageJob is 'jobs'
  • hostGroup is '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:

  • executeJob in 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 jobRefferingPage in JSON, but the shared prop is jobReferringPage
  • the wrapper currently hardcodes jobReferringPage={'jobs'}
  • disabledTriggerNow exists in shared props but is not forwarded by the wrapper
  • disabledTextField exists in shared props but is not forwarded by the wrapper
  • connectionJobPrefix exists 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 AutomationList wrapper

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:

  1. add a new page entry to portal.json
  2. set type to JobList
  3. provide the required dataSources
  4. set name to a route-friendly value such as Jobs-Alpha
  5. ensure the corresponding automation hostGroup convention 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:

  1. add a page entry with type: "AutomationList"
  2. define dataSources
  3. ensure there is a matching job page naming convention if the automation should deep link to jobs
  4. 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 AutomationList wrapper

Recommended improvements:

  • forward disabledTriggerNow
  • forward disabledTextField
  • use config-driven jobRefferingPage
  • normalize spelling between jobRefferingPage and jobReferringPage

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 hostGroup naming convention
  • some portal.json fields exist but are not fully wired through wrappers
  • JobList currently 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
  • withParams exists in portal.json, but from the traced frontend path it does not appear to be actively consumed in the pages we reviewed

Before changing portal.json or adding a new page, verify:

  1. whether the page name change will alter the route
  2. whether any automation deep link depends on that route
  3. whether all required auth hosts are discoverable by AppStateStore
  4. whether the page wrapper actually forwards the config fields you added
  5. whether SignalR payload shape matches newSignalRDataObj
  6. whether the page uses one data source or effectively assumes only the first data source
  7. 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 jobRefferingPage versus jobReferringPage
  • 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:

  1. portal.json
  2. app wrapper component
  3. shared-lib page component
  4. 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