Merge pull request #687 from PicPeak/chore/in-app-migration-banner

feat(admin): in-app migration banner for the org rename
This commit is contained in:
Paul Nothaft
2026-06-29 22:22:52 +02:00
committed by GitHub
4 changed files with 85 additions and 0 deletions
@@ -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>
);
};
+5
View File
@@ -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",
+5
View File
@@ -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",