feat(FR-3877): move the system announcement to the domain app config - #9527
feat(FR-3877): move the system announcement to the domain app config#9527ironAiken2 wants to merge 9 commits into
Conversation
Coverage Report for react-coverage (./react)
File Coverage |
78d5257 to
b5e9aaa
Compare
c6d64cf to
88a930d
Compare
agatha197
left a comment
There was a problem hiding this comment.
Requesting changes for the two login-path issues below (loginSessionAuth.ts:119 and :124) — both are unverified assumptions sitting on the app's only entry path, and both are cheap to guard. The remaining comments are minor cleanups.
Everything else checks out: logic and Relay patterns follow the sibling hooks, i18n is in sync across all 21 locales, regenerated Relay artifacts show no drift, and the projects connection uses a single pagination mode (first+after).
|
Addressed the review-body feedback from @agatha197 in
|
7d5d2c7 to
e75bf6e
Compare
|
Dev server on box |
|
Addressed the review from @agatha197 in
Measured on the dev server: icon and Edit sit at the header's vertical centre collapsed and expanded (offset ≤ 0.01px); the quote rule is
|
ccdfd43 to
31836c1
Compare
31836c1 to
5d4ff5b
Compare
5d4ff5b to
ec35b71
Compare
|
Dev server on box |
The announcement lived in the manager's legacy announcement endpoint
(`/manager/announcement`, etcd) as one markdown string whose first line
the banner lifted out as a title. It now lives in the domain app config
as `domainConfig.announcement = { enabled, title, body, updatedAt }`, in
line with the other app-config migrations under FR-1203.
- `useUpdateDomainAppConfig()` joins the app-config hooks: resolves the
domain uuid through `domainV2.entityId` (DOMAIN scope is addressed by
uuid, the client only knows the name), re-reads the raw DOMAIN-scope
`domainConfig` fragment, replaces one sub-key (`undefined` removes it)
through `scopedUpsertAppConfigFragments`, then refetches the merged
`myAppConfigs(['domainConfig'])` view so `useDomainAppConfig` readers
update without a reload.
- `AnnouncementBanner` reads `useDomainAppConfig('announcement')`. The
title shows in every state; the body (markdown) renders behind the
expand toggle. Dismissal is keyed by `updatedAt` instead of the message
text, so a re-published announcement resurfaces the banner.
- `AnnouncementEditModal` gains a Title field and the Enabled checkbox
the legacy endpoint could not persist (it stored by presence only);
Delete removes the sub-key. The content suspends on the Relay read
behind a skeleton-bodied modal fallback.
- `announcementSummary.ts` (first-line extraction, markdown-to-prose) and
`useSuspenseGetAnnouncement` (TanStack) go away with the derived title;
`announcement.ts` keeps the code-point title cutoff and the visibility
and collapsibility predicates. `service.get_announcement` /
`update_announcement` stay in the client library but have no caller.
No manager-version fallback: the app-config reads need 26.9.0+, which the
LTS this ships in guarantees.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B13bqRK3V12jiCvia8fm2w
…p the per-save domainV2 lookup
`useUpdateDomainAppConfig` resolved the DOMAIN scope uuid with a `domainV2`
round-trip on every save because the client only stored the domain *name*
(`_config.domainName`, from the legacy `user { domain_name }` read).
The login GQL connection now reads `myUserV2` once — user, role, domain
(name + `entityId`) and active projects — instead of the `keypair` → `user`
→ `group.list` chain, and stores the uuid as `_config.domainId`. The setter
reads it through the new `useCurrentDomainId` hook; its unused `domainName`
override and the `useAppConfigDomainIdQuery` artifact go away.
`UserV2.projects` caps an unpaginated read at the manager's default page
size (10), so the login walks the cursor with `first: 100`. V2 ids are Relay
global ids, decoded with `toLocalId` for `user_uuid` / `groupIds`.
Dropped what nothing read: the stored `resource_policy` and
`need_password_change`, and the fetched-but-unstored `username` /
`is_active`. `groupIds` now holds only the user's projects (the legacy map
held every group in the domain); its sole reader, `current_group_id()`,
only ever looks up the user's current project.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX2RuEAvsXWdxgi2MdaMsz
Fold `useCurrentDomainId` into `useCurrentDomain`, which returns the domain name and uuid together, instead of a second single-value hook next to `useCurrentDomainValue`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX2RuEAvsXWdxgi2MdaMsz
…se type is generated The login read was a raw `client.query` string with a hand-written response type. `RelayEnvironment`'s fetch function resolves `globalThis.backendaiclient` at request time — the same signed `/admin/gql` request `client.query` makes — and `connectViaGQL` installs the client before its first read, so the query can be a `graphql` tag inside the function: Relay compiles it and generates `loginSessionAuthMyUserQuery`, and the hand-written type goes away. The test mocks the `RelayEnvironment` module with `createMockEnvironment()` and queues one payload per page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX2RuEAvsXWdxgi2MdaMsz
…cement modal - connectViaGQL walks `UserV2.projects` by `limit`/`offset`, bounded by the connection's `count`, so a manager that never ends its cursor cannot hang login; the query is a module constant like the sibling hooks. - The announcement modal shows the required-title error only after the title was edited, with the detached status placement (no bordered block). - The Enabled checkbox is replaced by a Publish split button whose menu offers Save as Draft (enabled: false); Delete stands alone on the left, and a saved draft is marked by a Draft token in the modal title. - `body` is trimmed on save (an empty body is dropped from the document). - i18n: `button.SaveAsDraft` and `summary.AnnouncementDraft` added, `summary.AnnouncementEnabled` removed, in all 21 locales. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCmZSe5Brr6F5CqB4XpHiS
The title field is a `BAIFormItem` inside `Form`, so its required error renders like every other form field (plain text under the input) instead of Astryx `TextInput`'s tinted status box, and the form's validateTrigger keeps an untouched empty title error-free. Publish / Save as Draft call `validateFields()` first; the custom `summary.AnnouncementTitleRequired` key is dropped in favour of the global required template. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCmZSe5Brr6F5CqB4XpHiS
…legible Banner centres its icon and actions only while it has no description, so the expanded body pushed them to the top; the banner's header now keeps `align-items: center` in both states. Markdown blockquotes inside the info band used Blockquote's grey rule and secondary text, which the tint swallowed; the announcement body (banner and editor preview, same class) colours the rule with the band's info colour and the text with the primary text colour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCmZSe5Brr6F5CqB4XpHiS
The committed artifact is a function of the whole tree, so rebasing this stack onto main left it stale on this layer. Regenerated with `pnpm run search-index`; no source change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCmZSe5Brr6F5CqB4XpHiS
main added a second loading phase to this modal — the body mounts hidden so Monaco's lazy chunk loads behind a Skeleton, and nothing is publishable until the editor reports ready — while this branch was rewriting the same markup for the domain app config. Resolving that conflict took the rewrite and lost the gate. It is restored on the new shape: Suspense still covers the Relay read, `isEditorReady` covers Monaco, and the test now stubs the app-config hooks instead of the retired REST query. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCmZSe5Brr6F5CqB4XpHiS
ec35b71 to
3654fa6
Compare





Resolves #9526 (FR-3877)
Summary
Top of the FR-1203 app-config stack (#9361 → #8860 → #9362 → this). The system announcement moves from the manager's legacy announcement endpoint (
/manager/announcement, one markdown string in etcd, first line lifted out as the title) to the domain app config:Hooks (
react/src/hooks/useAppConfig.tsx)useUpdateDomainAppConfig()— the missing DOMAIN-scope setter next touseUpdatePublicDomainAppConfig/useUpdateMyUserAppConfig.(subKey, nextValue): takes the domain uuid from the client (useCurrentDomain().id, stored at login — see below), re-reads the raw DOMAIN-scopedomainConfigfragment, replaces one sub-key (undefinedremoves it) throughscopedUpsertAppConfigFragments, then refetches the mergedmyAppConfigs(['domainConfig'])view so everyuseDomainAppConfigreader updates — nowindow.location.reload(), no per-savedomainV2round-trip.useDomainAppConfig<T>(subKey)(first consumer).Banner / editor
AnnouncementBannerreadsuseDomainAppConfig<DomainAnnouncement>('announcement'). Title in every state; the body renders behind the expand toggle; a title past the 120-code-point cutoff collapses like before. Dismissal is keyed byupdatedAt, so a re-published announcement resurfaces the banner.AnnouncementEditModalgains a Title field (aBAIFormIteminsideForm,required+whitespace, so its error renders like every other form field and an untouched empty title stays error-free; Publish / Save as Draft runvalidateFields()first) and a Publish split button whose menu offers Save as Draft (enabled: false, which the legacy endpoint could not persist — it stored by presence only); a saved draft is marked by a Draft token in the modal title. Delete stands alone on the left and removes the sub-key.bodyis trimmed on save. The content suspends on the Relay read behind a skeleton-bodied modal fallback, so the Maintenance page opens it without a page-level Suspense.announcementSummary.ts(first-line extraction, markdown-to-prose) anduseSuspenseGetAnnouncement.tsx(TanStack).helper/announcement.tskeeps the title cutoff and the visibility / collapsibility predicates, with tests.service.get_announcement/update_announcementstay inbackend.ai-client(library API) but have no caller in the app.summary.AnnouncementTitle,summary.AnnouncementTitleRequired(all 21 locales);summary.AnnouncementMessageRequiredremoved (no consumer).Login reads
myUserV2(react/src/helper/loginSessionAuth.ts)connectViaGQLused to chainkeypair→user→group.listand only ever stored the domain name, so the DOMAIN-scope setter above had to resolve the uuid on every save. It now readsmyUserV2once —id,basicInfo,organization { domainName role },domain { entityId basicInfo { name } },projects(filter: { isActive: true })— and stores the uuid as_config.domainId(newuseCurrentDomain()hook returning{ name, id };LoginConfigState.domain_id). The read is a module-level Relaygraphqltag used throughfetchQuery(RelayEnvironment, …):RelayEnvironment's fetch resolvesglobalThis.backendaiclientat request time, whichconnectViaGQLinstalls first, so the response type is the generatedloginSessionAuthMyUserQueryrather than a hand-written one.UserV2.projectscaps an unpaginated read at the manager's default page size (10), so the login walks the pages withlimit: 100/offset, bounded by the connection'scount(a single pagination mode; no cursor to stall on).ids are Relay global ids;toLocalIddecodes them foruser_uuidandgroupIds.resource_policyandneed_password_change, and the fetched-but-unstoredusername/is_active.groupIdsnow holds only the user's projects (the legacy map held every group in the domain); its sole reader,current_group_id(), only ever looks up the user's current project.useAppConfigDomainIdQueryis gone with its__generated__artifact.myUserV2is 26.2.0+, below the 26.9.0 this PR already requires, so no gate.No version fallback
myAppConfigs/scopedUpsertAppConfigFragmentsneed manager 26.9.0+. This ships in the LTS that guarantees it (decision: FR-3877 discussion), so there is no legacy-endpoint fallback — on an older manager the banner query fails insideErrorBoundaryWithNullFallbackand the Maintenance page's Edit modal errors on load.Verification
bash scripts/verify.sh→ all checks pass at commit time (Lint, Format, TypeScript ×2, Vite warmup, StyleX, Astryx theme build, Astryx integration, z-index, Agent mappings, Terminology). The Relay check flagged only the two new__generated__artifacts as uncommitted before this commit; they are committed.vitest run src/helper/announcement.test.ts— 8 tests.vitest run src/helper/loginSessionAuth.test.ts— 11 tests on acreateMockEnvironment()stand-in forRelayEnvironment(single read, offset walk bounded bycount, role →is_admin/is_superadminmapping, domain fallback, remembered project, unauthenticated logout, endpoint history);STokenLoginBoundary.test.tsxanduseWebUIConfig.test.tsstill pass.pnpm relay—useAppConfigDomainRawQuery,loginSessionAuthMyUserQuerygenerated;useAppConfigDomainIdQueryremoved.Not yet verified against a live manager (the 10.82.0.x test network was unreachable from the authoring box). Assumptions to confirm on the first run:
domainConfigis allow-listed at DOMAIN scope for the read (myAppConfigs);myUserV2.domain.entityIdis the uuidAppConfigScopeRef.scopeIdexpects; andProjectV2.id/UserV2.idcome back as Relay global ids (base64("ProjectV2:<uuid>")), which is what the manager source declares (NodeIDonPydanticNodeMixin). A dev server for this branch is up for that:https://fr-3877.localhost:1355.Review round (
7d5d2c7ff):bash scripts/verify.sh→=== ALL PASS ===;vitest run src/helper/loginSessionAuth.test.ts src/helper/announcement.test.ts→ 2 files / 19 tests passed.Stack rebased onto
main(f8d974dd3) and the title moved to the form engine (826d213b5):bash scripts/verify.sh→=== ALL PASS ===.Reviewer checklist
updatedAt).myUserV2.projects).🤖 Generated with Claude Code
https://claude.ai/code/session_01B13bqRK3V12jiCvia8fm2w