Skip to content

Commit b5e9aaa

Browse files
ironAiken2claude
andcommitted
feat(FR-3877): move the system announcement to the domain app config
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
1 parent 2cbfdfe commit b5e9aaa

31 files changed

Lines changed: 687 additions & 561 deletions

react/src/__generated__/useAppConfigDomainIdQuery.graphql.ts

Lines changed: 110 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/src/__generated__/useAppConfigDomainRawQuery.graphql.ts

Lines changed: 131 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/src/components/AnnouncementBanner.tsx

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
55
import {
6+
DOMAIN_ANNOUNCEMENT_CONFIG_KEY,
7+
DomainAnnouncement,
68
isAnnouncementCollapsible,
7-
splitAnnouncement,
8-
summarizeAnnouncement,
9-
} from '../helper/announcementSummary';
9+
isAnnouncementVisible,
10+
summarizeAnnouncementTitle,
11+
} from '../helper/announcement';
1012
import { useCurrentUserRole } from '../hooks/backendai';
11-
import { useSuspenseGetAnnouncement } from '../hooks/useSuspenseGetAnnouncement';
13+
import { useDomainAppConfig } from '../hooks/useAppConfig';
1214
import './AnnouncementBanner.css';
1315
import AnnouncementEditModal from './AnnouncementEditModal';
1416
import { Banner } from '@astryxdesign/core/Banner';
@@ -20,17 +22,16 @@ import {
2022
useSessionStorageState,
2123
useToggle,
2224
} from 'backend.ai-ui';
23-
import * as _ from 'lodash-es';
2425
import { ChevronDownIcon, ChevronUpIcon, SquarePenIcon } from 'lucide-react';
2526
import React from 'react';
2627
import { useTranslation } from 'react-i18next';
2728

2829
/**
29-
* The system announcement as AppShell's top banner (FR-3612), replacing the
30-
* StartPage-only `AnnouncementAlert`. A long announcement renders collapsed —
31-
* a one-line summary followed by an explicit labelled expand toggle revealing
32-
* the full markdown. Dismissal is remembered per session and per message, so
33-
* a new announcement resurfaces the banner.
30+
* The domain's system announcement as AppShell's top banner (FR-3612), read
31+
* from the domain app config (FR-3877). The title shows in every state; a
32+
* body (or a title past the cutoff) renders collapsed behind an explicit
33+
* labelled expand toggle. Dismissal is remembered per session and per
34+
* publication, so a re-published announcement resurfaces the banner.
3435
*/
3536
const AnnouncementBanner: React.FC = () => {
3637
'use memo';
@@ -42,31 +43,26 @@ const AnnouncementBanner: React.FC = () => {
4243
const [isEditOpen, { toggle: toggleEditModal }] = useToggle(false);
4344
// Expansion is owned here rather than by Banner's `children` slot: Banner's
4445
// own toggle sits at the far end of the header, away from the text it
45-
// reveals. (0.5.0's `collapsible` config added a controlled mode; the
46-
// placement is what still rules the slot out.)
46+
// reveals.
4747
const [isExpanded, { toggle: toggleExpanded }] = useToggle(false);
48-
const { data: announcement } = useSuspenseGetAnnouncement();
49-
const [dismissedMessage, setDismissedMessage] = useSessionStorageState<
48+
const announcement = useDomainAppConfig<DomainAnnouncement>(
49+
DOMAIN_ANNOUNCEMENT_CONFIG_KEY,
50+
);
51+
const [dismissedKey, setDismissedKey] = useSessionStorageState<
5052
string | undefined
5153
>('backendaiwebui.dismissed_announcement');
5254

53-
const message = announcement.message ?? '';
54-
if (_.isEmpty(message) || dismissedMessage === message) {
55+
if (!isAnnouncementVisible(announcement)) {
56+
return null;
57+
}
58+
const dismissKey = announcement.updatedAt ?? announcement.title;
59+
if (dismissedKey === dismissKey) {
5560
return null;
5661
}
5762

58-
// Must use the same renderer settings as the editor preview (FR-3402); the
59-
// banner sits above the page h1, so markdown `#` starts at h3.
60-
const renderMarkdown = (source: string) => (
61-
<Markdown density="compact" headingLevelStart={3} autolink="gfm">
62-
{source}
63-
</Markdown>
64-
);
65-
const isCollapsible = isAnnouncementCollapsible(message);
66-
// The first line is the banner's title in both states, so the expanded body
67-
// is the source WITHOUT it — expanding adds only what the title does not
68-
// already show.
69-
const { headline, body } = splitAnnouncement(message);
63+
const title = announcement.title.trim();
64+
const body = (announcement.body ?? '').trim();
65+
const isCollapsible = isAnnouncementCollapsible(announcement);
7066

7167
return (
7268
<>
@@ -83,14 +79,13 @@ const AnnouncementBanner: React.FC = () => {
8379
} as React.CSSProperties
8480
}
8581
isDismissable
86-
onDismiss={() => setDismissedMessage(message)}
87-
// Collapsible shape: the first line is the title in BOTH states, with
88-
// the expand toggle right beside it — collapsed it is cut to one row,
89-
// expanded it shows in full (so a cropped long line is never lost) and
90-
// `description` adds the rest of the source below it. Short shape:
91-
// body in `description`, not `title` — Banner centres its header on
82+
onDismiss={() => setDismissedKey(dismissKey)}
83+
// Collapsible shape: the title in BOTH states with the expand toggle
84+
// right beside it — collapsed it is cut to one row, expanded it shows
85+
// in full and `description` adds the body below it. Short shape: the
86+
// title in `description`, not `title` — Banner centres its header on
9287
// `description == null && hasActions`, which misaligns the icon and
93-
// Edit button against a multi-line announcement (FR-3482).
88+
// Edit button against a wrapping line (FR-3482).
9489
title={
9590
isCollapsible ? (
9691
<span className="webui-announcement-title">
@@ -101,7 +96,7 @@ const AnnouncementBanner: React.FC = () => {
10196
: 'webui-announcement-summary'
10297
}
10398
>
104-
{isExpanded ? headline : summarizeAnnouncement(message)}
99+
{isExpanded ? title : summarizeAnnouncementTitle(title)}
105100
</span>
106101
<Button
107102
className="webui-announcement-toggle"
@@ -125,11 +120,18 @@ const AnnouncementBanner: React.FC = () => {
125120
) : null
126121
}
127122
description={
128-
isCollapsible
129-
? isExpanded && body.length > 0
130-
? renderMarkdown(body)
131-
: undefined
132-
: renderMarkdown(message)
123+
isCollapsible ? (
124+
isExpanded && body.length > 0 ? (
125+
// Must use the same renderer settings as the editor preview
126+
// (FR-3402); the banner sits above the page h1, so markdown `#`
127+
// starts at h3.
128+
<Markdown density="compact" headingLevelStart={3} autolink="gfm">
129+
{body}
130+
</Markdown>
131+
) : undefined
132+
) : (
133+
title
134+
)
133135
}
134136
endContent={
135137
isSuperAdmin ? (

0 commit comments

Comments
 (0)