Skip to content

feat(directory): Phase 1 frontend — Settings tab + PrincipalPicker#407

Open
larsgeorge-db wants to merge 1 commit into
feat/directory-backendfrom
feat/directory-frontend
Open

feat(directory): Phase 1 frontend — Settings tab + PrincipalPicker#407
larsgeorge-db wants to merge 1 commit into
feat/directory-backendfrom
feat/directory-frontend

Conversation

@larsgeorge-db
Copy link
Copy Markdown
Collaborator

Second of two PRs implementing Phase 1 of the directory-lookup-and-principal-picker plan (#375). Stacked on #406 — base branch is feat/directory-backend, so the diff here is frontend-only. Will rebase onto main once #406 merges.

Source PRD: #335
Plan: #375
Backend PR: #406

Summary

  • Settings → Integrations → Directory tab: provider Select (Entra ID enabled, Okta/Ping rendered disabled so the abstraction is visible to operators), UC HTTP Connection dropdown fed from /api/directory/uc-http-connections, Save / Test / Clear buttons with success/destructive toast feedback, and a provider-specific help block for Entra with the four UC-connection fields plus required Graph application scopes.
  • PrincipalPicker — single component, both modes (configured / unconfigured) and both UI variants (inline 2-char-debounced type-ahead + popup "Browse directory" dialog with type-filter chips) selected at runtime from the directory store. Two-line result rows (display name + sub_label), tooltip on selected badges exposing the underlying email/UPN/GUID, X-to-remove. Props are discriminated on multiple so single-pick callers get string | null typing and multi-pick callers get string[] typing without casts.
  • directory-store (Zustand) — singleton cache for /api/directory/status so every picker on a page shares one network call, plus a session-sticky degraded flag flipped on the first search failure so subsequent pickers drop straight into manual mode without re-probing.
  • Assign Owner dialog migrated to PrincipalPicker(accepts=['user']). API payload (user_email + user_name) is unchanged — the change is purely UI.
  • Sidebar entry under Settings → Integrations and /settings/directory route wired up.

Architectural notes

  • The picker emits and consumes only the persisted identifier string (UPN/email for users, displayName for groups). Existing storage shapes (str / List[str]) are preserved; no DB migration in v1.
  • Pre-existing values render as plain badges with no error decoration regardless of provider availability. The picker does not re-resolve persisted ids against the directory.
  • Graceful degradation: if directory.status says configured but a search call fails, the picker logs once (console.warn), flips into manual-entry mode for the rest of the session, and directory-store.degraded becomes sticky so newly-mounted pickers behave the same way.
  • Future Phase-2+ migrations (Roles, Entitlements, Comments, etc.) reuse this same component with different multiple / accepts combinations — no per-call-site bespoke control.

Test plan

  • Type-check clean: yarn type-check (Done in 17.93s)
  • Lint clean on new files (npx eslint ...): 0 errors, only the repo-wide err: any catch-block warning pattern remains (matches existing files).
  • Frontend suite: npx vitest run550 passed, 6 skipped, 0 failed.
  • New tests (19):
    • principal-picker.test.tsx (13): pre-existing values render as plain badges without re-resolving; X-remove emits remaining ids; disabled hides remove buttons; Enter/Tab/comma/blur commit in unconfigured mode; empty / duplicate inputs are no-ops; configured mode calls /api/directory/search with q= and types= honouring accepts; queries < 2 chars do not fire a request; failing search flips into manual-entry mode for the rest of the session.
    • directory-store.test.ts (6): fetchStatus populates state, dedupes concurrent calls into one in-flight request, no-ops once loaded, falls back to not-configured on network failure; refresh re-fetches and clears degraded; markDegraded is sticky.
  • Radix Popover dropdown internals are deferred to Playwright E2E — the existing tag-selector tests in this repo follow the same split for the same reason (Radix Popover hangs in jsdom).
  • Manual smoke test of the Settings tab + Assign Owner against a real workspace with an Entra UC connection (left for reviewer / follow-up).

What's intentionally not here

  • Migration of other principal-bearing fields (Roles, Entitlements, Comments, Teams, Reviews, MDM, etc.) — Phases 2–4 in the plan.
  • A production second provider — abstraction is exercised by backend unit-test stub.

Front-end half of the Directory layer (see #406 for the backend).

UI pieces:
- views/settings-directory.tsx — Settings → Integrations → Directory.
  Provider Select (Entra ID enabled, Okta/Ping rendered disabled so
  the abstraction is visible), UC HTTP Connection Select fed from
  GET /api/directory/uc-http-connections, Save / Test / Clear buttons
  with toast feedback, and a provider-specific help block for Entra
  (token URL, base URL, scope, grant type, required Graph scopes).
- components/common/principal-picker.tsx — single component that
  switches between configured-mode type-ahead and unconfigured-mode
  manual entry from directory-store status. Two-line result rows
  (display_name + sub_label), tooltip on selected badges exposing
  sub_label, X-to-remove, optional popup-dialog "Browse directory"
  variant with type-filter chips. Discriminated `multiple` prop so
  single-pick gives `string | null`, multi-pick gives `string[]`.
- stores/directory-store.ts — Zustand store caching /api/directory/status
  for the page lifetime and tracking a session-sticky `degraded`
  flag flipped on the first search failure so subsequent pickers
  drop straight into manual mode.
- types/directory.ts — TS mirrors of the backend Pydantic models.
- Assign Owner dialog migrated to PrincipalPicker(accepts=['user']);
  API payload (user_email + user_name) unchanged.
- Sidebar/route wiring in app.tsx and settings-layout.tsx.

Tests (19 new, 550 passing total, 0 regressions):
- principal-picker.test.tsx (13):
  - Pre-existing values render as plain badges without re-resolving
    against the directory.
  - X-remove emits the remaining ids.
  - Disabled hides remove buttons.
  - Unconfigured: Enter / Tab / comma / blur commit the typed value
    as a badge; empty input and duplicates are no-ops.
  - Configured: /api/directory/search is called with q= and types=
    parameters honouring the accepts filter; queries < 2 chars do
    not fire a request.
  - Graceful degradation: a failing search flips into manual-entry
    mode for the rest of the session.
- directory-store.test.ts (6):
  - fetchStatus populates state, dedupes concurrent calls into one
    in-flight request, and is a no-op once loaded.
  - Network failure falls back to a not-configured status.
  - refresh re-fetches and clears the degraded flag; markDegraded
    is sticky across re-renders.

Notes:
- The Radix Popover dropdown itself is exercised by Playwright E2E;
  the existing tag-selector tests in this repo follow the same split
  for the same reason (Radix Popover hangs in jsdom).
- New code lints clean (no errors); only the existing repo-wide
  `err: any` catch-block warning pattern remains.
larsgeorge-db added a commit that referenced this pull request May 21, 2026
…r scope

Documents what shipped under PRs #406 / #407 / #412 / #413 / #416 / #417:

- Renames the integration's manager / routes / settings keys in the
  PRD to match the implementation (Directory layer, /api/directory/*,
  DIRECTORY_* settings, Settings → Directory tab).
- Documents the DirectoryProvider interface and the
  (DirectoryProviderContext, DirectoryProviderConfig) factory
  signature so future provider plug-ins know what to implement.
- Documents the v1 provider set, which expanded during planning
  from Entra-only to entra + lakebase + file. The Lakebase table
  schema and CSV format are included so operators have a single
  reference.
- Preserves story content, the disambiguation rule, both picker
  modes, storage-compatibility guarantees, and graceful-degradation
  rules from the PRD body unchanged.
- Re-confirms the out-of-scope list (Okta/Ping, service principals,
  OBO, profile photos, manager hierarchy, role/team Select replacement,
  CSV bulk import) which the abstraction makes cheap to revisit.
larsgeorge-db added a commit that referenced this pull request May 21, 2026
…r scope

Documents what shipped under PRs #406 / #407 / #412 / #413 / #416 / #417:

- Renames the integration's manager / routes / settings keys in the
  PRD to match the implementation (Directory layer, /api/directory/*,
  DIRECTORY_* settings, Settings → Directory tab).
- Documents the DirectoryProvider interface and the
  (DirectoryProviderContext, DirectoryProviderConfig) factory
  signature so future provider plug-ins know what to implement.
- Documents the v1 provider set, which expanded during planning
  from Entra-only to entra + lakebase + file. The Lakebase table
  schema and CSV format are included so operators have a single
  reference.
- Preserves story content, the disambiguation rule, both picker
  modes, storage-compatibility guarantees, and graceful-degradation
  rules from the PRD body unchanged.
- Re-confirms the out-of-scope list (Okta/Ping, service principals,
  OBO, profile photos, manager hierarchy, role/team Select replacement,
  CSV bulk import) which the abstraction makes cheap to revisit.
@larsgeorge-db larsgeorge-db force-pushed the feat/directory-backend branch from b2ee317 to d2f09fa Compare May 21, 2026 21:35
@larsgeorge-db larsgeorge-db force-pushed the feat/directory-frontend branch from 398400c to 23fd4f7 Compare May 21, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant