feat: implement federation management

What is this MR?

It adds a Federation page to the portal. An operator can now:

  1. Register partner operators, the other telecom platforms we want to share edge resources with.
  2. Create or delete a federation with a partner (a trust relationship between two platforms).
  3. Subscribe to or unsubscribe from the partner's zones, the edge locations it offers us.

It also contains supporting work that the page needed: a shared data table, one shared confirm dialog, role-based API catalogue filtering, and a cleanup of dead code.

Terms

Term Meaning
Partner Another operator's platform, registered in our Federation Manager (FM).
Federation An agreement between us and an active partner. It is identified by a federationContextId.
Zone An edge location the partner offers. Subscribing makes the partner reserve resources for us.
FM Federation Manager, the backend service that talks to partners.

How the pieces fit together

flowchart TD
  A["pages/federation/index.tsx<br/>(route entry)"] --> B["features/federation/federation.tsx<br/>(FederationPage: owns dialog state)"]
  B --> C["components/federation/*<br/>(columns, dialogs, badges, zone list)"]
  B --> D["hooks/usePartners, usePartnerFederation"]
  C --> D
  D --> E["services/federation/*<br/>(partners.ts, federations.ts)"]
  E --> F["federation-request.ts<br/>(fetch + error handling)"]
  F --> G["lib/api-fetch (existing)"]

The GUI never talks to the FM directly. Every call goes through the portal backend, which forwards /api/oeg/* to the OEG with the user's token. The only new config is federation.baseUrl in api.ts.

Code layers (top to bottom)

Each layer only calls the one below it. That is why the components contain no fetch calls.

Layer Files Job
Types types/federation.ts Shapes of the API data
Service services/federation/* One function per endpoint
Hooks usePartners, usePartnerFederation, useSingleFlight Loading state, caching, actions
Validation features/federation/*-schema.ts Zod schemas for the forms
UI components/federation/*, features/federation/federation.tsx What the user sees

Service layer: errors you can react to

federation-request.ts wraps apiFetch and does three things:

  • It sets the JSON headers and parses the body safely.
  • On a non-2xx response it throws a FederationRequestError. The error message is built from the API's ProblemDetails: detail, then title, plus any invalidParams.
  • It exposes isProblem(err, 'some-code'). The code is the last :-segment of problem.type.

isProblem turns "expected" errors into normal answers. In federations.ts:

  • findFederationContextId returns null on federation-context-unknown. That means "not federated", not a failure.
  • findZoneSubscription returns null on zone-not-subscribed. That means "not subscribed".

URL builders (federations(), federation(), zones(), zone()) call encodeURIComponent on every id, so a weird id can't break the path.

partners.ts has listPartners, getPartner, registerPartner (POST) and updatePartner (PATCH).

usePartners: the table's data, loaded in two stages

Listing partners is one call. Finding out whether each is federated costs one call per partner, so it is lazy:

sequenceDiagram
  participant T as DataTable
  participant H as usePartners
  participant FM as API

  H->>FM: GET /partners
  FM-->>H: partners
  H-->>T: rows (federation = "loading")

  T->>H: onVisibleRows(ids on this page)

  loop each id not requested yet
    H->>FM: GET /partners/{id}/federations
    FM-->>H: contextId or "federation-context-unknown"
    H-->>T: badge becomes federated / not-federated / unknown
  end

Details:

  • Only the rows visible on the current page are checked. A requested Set avoids asking twice.
  • A generation counter is bumped on every refresh. A late answer from an older generation is ignored, so stale data can't overwrite fresh data.
  • A failed check shows unknown. It does not break the page.
  • refresh() re-runs the load without blanking the table (refreshing is separate from loading).

usePartnerFederation(partnerId, { zones, partner }): one partner's federation

It loads the federation context and its details. With zones: true it also checks the subscription status of each offered zone. It returns create, remove, subscribe and unsubscribe, and each action reloads afterwards, so the UI always shows server truth rather than guesses.

The cancelled flag in each useEffect stops a closed dialog from setting state.

useSingleFlight: no double-click bugs

useSingleFlight.ts wraps an async action. If it is already running, a second call is ignored. It is used on every submit and confirm button, so a double-click can't create two federations.

The UI

FederationPage is the only stateful orchestrator. It holds which partner is selected for each dialog and renders:

flowchart TD
  PAGE[FederationPage] --> TABLE[DataTable of partners]
  TABLE -->|row menu: Details| D1[PartnerDetailsDialog]
  TABLE -->|Federation| D2[PartnerFederationDialog]
  TABLE -->|Zones<br/>only if federated| D3[PartnerZonesDialog]
  TABLE -->|Edit| D4[PartnerFormDialog edit]
  PAGE -->|Register partner button| D5[PartnerFormDialog create]
  D2 --> D2a[CreateFederationDialog]
  D2 --> D2b[ConfirmActionDialog: delete]
  D3 --> ZL[ZoneList]
  ZL --> D3a[ConfirmActionDialog: unsubscribe]
Component What it does
partner-columns.tsx Table columns: Name (click opens details, searchable by name or MCC-MNC), Registration badge, Federation badge, EWBI endpoint, actions menu. The Zones action only appears when federation === 'federated'.
PartnerFormDialog Register and edit form. On edit it sends only changed fields (PATCH).
PartnerFederationDialog Shows the federation details. It offers Create federation only when the partner is active, otherwise it explains why not. It also offers Delete federation.
CreateFederationDialog Asks for initialDate (defaults to now) and partnerStatusLink.
PartnerZonesDialog / ZoneList Lists the offered zones as Offered or Subscribed, with the reserved CPU, memory and disk, and Subscribe/Unsubscribe buttons.
*-badge.tsx, detail-fields.tsx Small display helpers.

The Federation page reloads the table (refresh) when the federation dialog closes after a change.

Access control: operators only

  • The sidebar item (Handshake icon, in navigation.ts) has roles: ['operator']. The new canSeeNavItem() hides it from other users.
  • The route in router.tsx is wrapped in the existing RoleGuard, so typing the URL by hand sends non-operators to Forbidden.
  • API catalogue: the federation endpoints are in the OEG OpenAPI spec. getApis(includeFederation) and getApi(id, includeFederation) in apis.ts now hide them from non-operators. useAPIs passes isOperator from useAuth(). getApi now returns null instead of throwing when an API is not found.

The OEG base URL also changed from vwip to v0.1alpha1, in api.ts and in tests/unit/test_oeg_proxy.py. This matches the docs rename in the architecture repo.

Shared components added or refactored

ConfirmActionDialog (confirm-action-dialog.tsx)

  • It replaces the three near-identical dialogs confirm-delete-app/deployment/instance-dialog, which are deleted.
  • Its props are title, description, confirmLabel, pendingLabel, failureMessage and onConfirm.
  • It shows a pending state, shows the error inside the dialog, and can't be closed while pending.
  • It is used for delete federation, unsubscribe zone, and the existing app/deployment/instance deletes.

DataTable (data-table.tsx)

  • It replaces the per-feature table components (app-*-table.tsx and app-*-toolbar.tsx), about 1,000 lines of near-copies.
  • It is built on TanStack Table and gives you:
    • name search
    • sorting
    • pagination (10 per page)
    • column show/hide
    • drag-and-drop row reordering (@dnd-kit)
    • a refresh button
    • an optional filters slot, which Federation uses for the Registration filter
    • an onVisibleRows callback, which usePartners uses to load federation states lazily
  • Applications (profiles, instances, deployments) and Federation all use it now, so they look and behave alike.
  • actionsColumn and RowActionsMenu give every table the same "⋯" menu. Items can be false, which hides them, and can add separatorBefore.

return-focus.ts: when a dialog closes, keyboard focus returns to the element that opened it (or to a fallback). It also works with nested dialogs.

Cleanup

  • Deleted dead code: api-columns.tsx, api-search.tsx, api-details.css, catalogue/column.tsx, table-columns.tsx, useAppInstances.ts, unused types and utils, and debug logs.
  • Removed the unused deps next-themes, eslint-config-prettier and eslint-plugin-react. Renamed the package to oop-portal.
  • Normalised file names: app-profile-columns .tsx (it had a space) became app-profile-columns.tsx, dragable-row became draggable-row, and use-mobile and use-sidebar became useIsMobile and useSidebar.
  • useApplications and useDeployments were reworked to match the loading/error/refresh pattern used by usePartners.

Suggested review order

  1. types/federation.ts to learn the data.
  2. services/federation/* to see what the API offers.
  3. hooks/usePartners.ts and usePartnerFederation.ts, the core logic.
  4. features/federation/federation.tsx, then the dialogs.
  5. data-table/* and confirm-action-dialog.tsx, the shared refactors.
  6. Skim the cleanup.

How to try it

  1. Log in as a user with the operator role. The Federation item appears in the sidebar. A non-operator doesn't see it, and /federation shows Forbidden.
  2. Click Register partner. For active status, fill in all three outbound credentials.
  3. Open Federation from the row menu and click Create federation.
  4. When the badge turns Federated, open Zones and subscribe or unsubscribe.
  5. Check Applications, Instances and Deployments still list, filter and delete as before.
Edited by Dimitrios Gogos

Merge request reports

Loading
Loading