Skip to content

Frontend Architecture

The Lens frontend is a React 18 Single Page Application running inside an Atlassian Forge Custom UI iframe. It renders a hierarchical project portfolio grid powered by AG Grid, with drag-and-drop via dnd-kit, inline editing, Gantt timeline visualization, and a full inspector panel.

LayerTechnologyVersion
UI FrameworkReact18.x
GridAG Grid Community35.x
Drag & Drop@dnd-kit6.x
RoutingReact Router6.x
i18nreact-intl (FormatJS)8.1.3
Design System@atlaskit/* (Atlassian Design System)Various
Bridge@forge/bridgeLatest

The app ships three independent frontend packages, each built with Create React App:

PackagePathPurpose
mainfoundation/static/main/Primary app — grid, Gantt, inspector, inline editing, all features
adminfoundation/static/admin/Admin panel — feature flags, monitoring dashboard, CSS overrides
issue-contextfoundation/static/issue-context/Jira issue context panel (displayed on individual issue pages)

All three are registered as Forge Custom UI resources in manifest.yml and deployed together via forge deploy.

The React entry point mounts the app and enables the Forge theme:

foundation/static/main/src/index.tsx
import '@atlaskit/css-reset';
import './foundation.css';
import { view } from '@forge/bridge';
view.theme.enable(); // Sync ADS dark/light theme with Jira host
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<React.StrictMode><App /></React.StrictMode>);

App.tsx is the application shell. It:

  1. Creates a Forge bridge history object (view.createHistory()) for SPA routing inside the iframe
  2. Wraps the history object as a React Router v6 Navigator
  3. Nests providers: ErrorBoundary > IntlProvider > FeatureFlagProvider > RovoProvider
  4. Mounts global components: LicenseBanner, AnnouncementPopup, FeedbackButton
  5. Registers routes
ErrorBoundary
└── IntlProvider (i18n, lazy locale loading)
└── FeatureFlagProvider (KVS-backed feature flags)
└── RovoProvider (Rovo AI availability check)
├── LicenseBanner (license enforcement)
├── AnnouncementPopup (queued notices)
├── Router
│ ├── / → LensListOrRedirect
│ ├── /import → ImportHub (lazy)
│ ├── /lens/:id → LensView
│ ├── /structure/:id → StructureRedirect (legacy)
│ └── /admin → AdminPage (lazy)
└── FeedbackButton

Boot-time effects:

  • Triggers api.listLenses() early to surface OAuth consent dialog (QA-BUG-04)
  • Loads admin CSS overrides from KVS and injects them into <head>
  • LensListOrRedirect checks for a last-viewed lens in KVS and auto-navigates

Routes use Forge bridge history, not browser window.history. The bridge synchronizes the iframe URL with the Jira host URL bar.

RouteComponentDescription
/LensListOrRedirectAuto-redirects to last-viewed lens, or shows LensMenu
/lens/:idLensViewMain portfolio view (grid + Gantt + inspector)
/importImportHubImport hub (CSV, BigPicture, Structure, file)
/adminAdminPageAdmin dashboard (lazy-loaded)
/structure/:idStructureRedirectLegacy bookmark redirect to /lens/:id

The :id parameter supports both numeric IDs (e.g., 7) and friendly URL slugs (e.g., 7-q1-roadmap). LensView parses the numeric prefix via parseInt(rawId.split('-')[0], 10).

Loads feature flags from the backend on mount, merges with compile-time defaults. Exposes:

  • useFeatureFlags() hook — full flags object + setFlag() for admin panel
  • useFeatureFlag(name) hook — single boolean flag
  • isFeatureEnabled(name) static function — for non-hook code (e.g., callbacks)

Performs a single async check for Rovo AI availability via bridge.rovo?.isEnabled?.(). Fail-open: defaults to true if the bridge call is inconclusive. Provides:

  • useRovoEnabled() hook — null (loading), true, or false

All backend communication goes through api/client.ts, which wraps @forge/bridge’s invoke():

foundation/static/main/src/api/client.ts
import { invoke as rawInvoke } from '@forge/bridge';
function invoke<T>(functionKey: string, payload?: Record<string, any>): Promise<T> {
return rawInvoke<T>(functionKey, payload).then((result: any) => {
if (result?.error === 'license_inactive') {
setLicenseInactive(); // Triggers license banner globally
}
return result;
});
}
export const api = {
listLenses: () => invoke('listLenses', {}),
getLensView: (id: string) => invoke('getLensView', { lensId: id }),
updateField: (lensId, issueKey, field, value) => invoke('updateField', { ... }),
// ... 60+ methods
};

The api object is the single point of contact between frontend and backend. Every method maps 1:1 to a Forge resolver function registered in src/index.ts.

Key interfaces exported from client.ts:

  • Lens — lens metadata (id, name, owner, mode, item_limit)
  • HierarchyNode — tree node (id, parent_id, depth, position, node_type, issueData)
  • CachedIssue — cached Jira issue fields (summary, status, assignee, priority, dates, custom_fields JSON)
  • ViewConfig — saved view configuration (columns, formatting, density, Gantt settings, pipeline)
  • FieldMeta — field metadata (id, name, type, isCustom)

The app uses no state management library — all state lives in React’s built-in primitives:

  • useState for data and UI state
  • useCallback for stable callback references
  • useMemo for derived data
  • useRef for values that should not trigger re-renders (editing state, timers, previous values)

LensView is the central state owner. See State Management for the complete data flow architecture.

The backend assembles complete views — the frontend never joins data from multiple API calls. getLensView() returns rows with enriched issueData already attached.

HierarchyGrid (the AG Grid wrapper) is a fully controlled component. It receives nodes, columns, and callbacks as props. It never fetches its own data or owns persistent state.

Rather than Redux or Zustand, state flows top-down from LensView through props and callbacks. This was a deliberate choice to keep the mental model simple and avoid synchronization bugs between a store and AG Grid’s internal state.

The app runs in a cross-origin Forge iframe, which affects:

  • Focus trackingstopEditingWhenCellsLoseFocus must be false
  • Popup positioning — AG Grid popups clip inside the iframe; editors use inline rendering
  • Console.log — invisible in the parent page; debugging uses visible DOM elements
  • Navigationview.createHistory() provides SPA routing within the iframe

Inline edits update the local state immediately, then fire the API call. If the API call fails, the cell reverts and a toast notification appears.

src/
├── index.tsx # Entry point
├── App.tsx # Provider tree + routing
├── api/client.ts # Bridge API client (60+ methods)
├── config/ # Feature flags, announcements config
├── contexts/ # FeatureFlagContext, RovoContext
├── pages/
│ ├── LensList.tsx # Lens card grid (legacy)
│ ├── LensMenu.tsx # Sidebar + preview lens list
│ ├── LensView.tsx # Main view orchestrator
│ └── lens-view/ # LensView sub-modules (12 custom hooks + sub-components)
├── components/ # 26 feature directories (see components.md)
├── hooks/ # 15 shared hooks
├── utils/ # 38 utility modules
├── types/ # Type definitions (density, pipeline, views, etc.)
├── i18n/ # 11 locale files + IntlProvider
├── foundation.css # All CSS (9000+ lines)
└── __mocks__/ # Test mocks (@forge/bridge)