feat(admin): in-app migration banner for the org rename (#669)
One-time banner shown at the top of the admin layout to surface the org
rename + GHCR registry change for operators who haven't read the release
notes. Sits right below the existing maintenance banner — same pattern.
## What it looks like
Blue, dismissible banner with a short body:
> PicPeak's image registry has moved
> Update your docker-compose.yml to pull from
> ghcr.io/picpeak/picpeak/{backend,frontend} — the old path is no longer
> being updated. [See migration notes]
The link goes to `docs/migration-to-org.md` on the new org repo.
## Design choices
- **No backend feature flag.** A hard-coded `MIGRATION_BANNER_ENABLED`
constant in the component file (1 line) gates global display. After
~1 quarter, flip it to false (or drop the mount in `AdminLayout.tsx`)
in a small follow-up PR. A backend `app_settings` row + Settings UI
toggle would be overkill for a one-time migration event.
- **Per-admin dismissal via localStorage.** Key is `picpeak:migration-banner:v1`
(versioned so a future "we've moved AGAIN" banner can show without
inheriting earlier dismissal). Wrapped in try/catch so private-mode
browsers + storage-quota-exceeded errors don't crash the layout.
- **EN + DE strings** under a new top-level `migrationBanner` namespace.
Other locales (fr, nl, pt, ru) fall through to EN — `migrationBanner.*`
keys aren't translated there yet, deliberate (per #669 the ops
banner is operator-facing and admins reading EN/DE is the majority).
- **Reuses `common.dismiss`** for the close-button aria-label.
## Test plan
- [ ] Frontend `npm run build:check` passes (TS + build)
- [ ] Open admin dashboard in EN → banner shows at top, below header,
above main content
- [ ] Switch to DE → banner shows German strings
- [ ] Click dismiss → banner hides, doesn't re-appear on hard refresh
- [ ] Clear localStorage `picpeak:migration-banner:v1` → banner returns
- [ ] Flip `MIGRATION_BANNER_ENABLED` to false → banner doesn't render
for anyone, regardless of dismissal state
Refs #669.
This commit is contained in:
@@ -7,6 +7,7 @@ import { useSessionTimeout } from '../../hooks/useSessionTimeout';
|
|||||||
import { AdminSidebar } from './AdminSidebar';
|
import { AdminSidebar } from './AdminSidebar';
|
||||||
import { AdminHeader } from './AdminHeader';
|
import { AdminHeader } from './AdminHeader';
|
||||||
import { MaintenanceBanner } from './MaintenanceBanner';
|
import { MaintenanceBanner } from './MaintenanceBanner';
|
||||||
|
import { MigrationBanner } from './MigrationBanner';
|
||||||
import { MandatoryPasswordChangeModal } from './MandatoryPasswordChangeModal';
|
import { MandatoryPasswordChangeModal } from './MandatoryPasswordChangeModal';
|
||||||
|
|
||||||
const SIDEBAR_COLLAPSED_KEY = 'admin-sidebar-collapsed';
|
const SIDEBAR_COLLAPSED_KEY = 'admin-sidebar-collapsed';
|
||||||
@@ -114,6 +115,11 @@ const AdminLayoutInner: React.FC<AdminLayoutInnerProps> = ({ sidebarOpen, setSid
|
|||||||
{/* Maintenance mode banner */}
|
{/* Maintenance mode banner */}
|
||||||
<MaintenanceBanner />
|
<MaintenanceBanner />
|
||||||
|
|
||||||
|
{/* One-time migration banner — flip the constant in MigrationBanner.tsx
|
||||||
|
(or remove this mount) after operators have had time to update their
|
||||||
|
docker-compose.yml. See #669. */}
|
||||||
|
<MigrationBanner />
|
||||||
|
|
||||||
{/* Page content - disabled when password change required.
|
{/* Page content - disabled when password change required.
|
||||||
overflow moved up to the column so the scrollbar gutter is
|
overflow moved up to the column so the scrollbar gutter is
|
||||||
reserved once at the column level (see above). main now
|
reserved once at the column level (see above). main now
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Info, X } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
// Hard-coded "feature flag" — flip to false (or remove this component from
|
||||||
|
// AdminLayout) once the migration window settles, after operators have had
|
||||||
|
// ~1 quarter to update their docker-compose.yml. See #669.
|
||||||
|
const MIGRATION_BANNER_ENABLED = true;
|
||||||
|
|
||||||
|
// localStorage key — versioned (`:v1`) so a future "we've moved again" banner
|
||||||
|
// can show without inheriting the user's earlier dismissal.
|
||||||
|
const DISMISS_KEY = 'picpeak:migration-banner:v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-time migration banner shown at the top of the admin layout (#669).
|
||||||
|
*
|
||||||
|
* Surfaces the org rename + new GHCR registry path so an operator who hasn't
|
||||||
|
* read the release notes sees the change when they next log in. Dismissible
|
||||||
|
* per-admin via localStorage; the toggle above can flip it off globally.
|
||||||
|
*/
|
||||||
|
export const MigrationBanner: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [dismissed, setDismissed] = React.useState<boolean>(() => {
|
||||||
|
try { return localStorage.getItem(DISMISS_KEY) === '1'; } catch { return false; }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!MIGRATION_BANNER_ENABLED || dismissed) return null;
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
try { localStorage.setItem(DISMISS_KEY, '1'); } catch { /* localStorage blocked */ }
|
||||||
|
setDismissed(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-start justify-between gap-3 py-3">
|
||||||
|
<div className="flex items-start gap-3 min-w-0">
|
||||||
|
<Info className="w-5 h-5 text-blue-600 dark:text-blue-300 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-900 dark:text-blue-100 min-w-0">
|
||||||
|
<p className="font-medium">{t('migrationBanner.title', "PicPeak's image registry has moved")}</p>
|
||||||
|
<p className="text-blue-800 dark:text-blue-200 mt-0.5">
|
||||||
|
{t('migrationBanner.body', {
|
||||||
|
defaultValue: 'Update your docker-compose.yml to pull from {{newPath}} — the old path is no longer being updated.',
|
||||||
|
newPath: 'ghcr.io/picpeak/picpeak/{backend,frontend}',
|
||||||
|
})}{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/PicPeak/picpeak/blob/main/docs/migration-to-org.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="underline hover:no-underline"
|
||||||
|
>
|
||||||
|
{t('migrationBanner.link', 'See migration notes')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDismiss}
|
||||||
|
className="text-blue-600 dark:text-blue-300 hover:text-blue-700 dark:hover:text-blue-200 flex-shrink-0"
|
||||||
|
aria-label={t('common.dismiss', 'Dismiss')}
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3065,6 +3065,11 @@
|
|||||||
"message": "Wir führen derzeit geplante Wartungsarbeiten durch, um unseren Service zu verbessern. Wir sind in Kürze wieder online.",
|
"message": "Wir führen derzeit geplante Wartungsarbeiten durch, um unseren Service zu verbessern. Wir sind in Kürze wieder online.",
|
||||||
"urgentMatters": "Bei dringenden Anliegen kontaktieren Sie bitte"
|
"urgentMatters": "Bei dringenden Anliegen kontaktieren Sie bitte"
|
||||||
},
|
},
|
||||||
|
"migrationBanner": {
|
||||||
|
"title": "Die Image-Registry von PicPeak wurde verschoben",
|
||||||
|
"body": "Aktualisiere deine docker-compose.yml, um Images von {{newPath}} zu beziehen — der alte Pfad wird nicht mehr aktualisiert.",
|
||||||
|
"link": "Migrationshinweise ansehen"
|
||||||
|
},
|
||||||
"passwordChange": {
|
"passwordChange": {
|
||||||
"title": "Passwort ändern",
|
"title": "Passwort ändern",
|
||||||
"currentPassword": "Aktuelles Passwort",
|
"currentPassword": "Aktuelles Passwort",
|
||||||
|
|||||||
@@ -3086,6 +3086,11 @@
|
|||||||
"message": "We're currently performing scheduled maintenance to improve our service. We'll be back online shortly.",
|
"message": "We're currently performing scheduled maintenance to improve our service. We'll be back online shortly.",
|
||||||
"urgentMatters": "For urgent matters, please contact"
|
"urgentMatters": "For urgent matters, please contact"
|
||||||
},
|
},
|
||||||
|
"migrationBanner": {
|
||||||
|
"title": "PicPeak's image registry has moved",
|
||||||
|
"body": "Update your docker-compose.yml to pull from {{newPath}} — the old path is no longer being updated.",
|
||||||
|
"link": "See migration notes"
|
||||||
|
},
|
||||||
"passwordChange": {
|
"passwordChange": {
|
||||||
"title": "Change Password",
|
"title": "Change Password",
|
||||||
"currentPassword": "Current Password",
|
"currentPassword": "Current Password",
|
||||||
|
|||||||
Reference in New Issue
Block a user