dashboards panel keyed (469 -> 445)

The layout templates are module-level data, so they carry nameKey and both consumers
resolve it — the visible label with t(), and the preview's own mini-registry with
panelName(), which is the same late-bound-thunk problem the app registry has.

Cancel comes from ui.button.cancel; Delete is panel.common.delete since nothing in
the shared components renders one.
This commit is contained in:
2026-08-23 14:45:23 +00:00
parent 1ae986bb31
commit 06d5c64de3
4 changed files with 88 additions and 29 deletions
@@ -26,8 +26,10 @@ import {
} from './constants';
import { useNewDashboardDraft } from './useNewDashboardDraft';
import { cwdToPath } from '../../components/Workspace';
import { useTranslation } from '../../locales';
export const DashboardListApp = () => {
const { t, tUi } = useTranslation();
const location = useLocation();
const client = useClient();
const queryClient = useQueryClient();
@@ -105,7 +107,7 @@ export const DashboardListApp = () => {
className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal"
>
<LayoutGrid className="h-4 w-4" />
Dashboards
{t('dashboards.title')}
</Link>
<button
type="button"
@@ -113,7 +115,7 @@ export const DashboardListApp = () => {
className="flex items-center justify-center gap-1 py-2 px-3 rounded-lg text-sm font-medium bg-duck-teal hover:bg-duck-teal/90 text-white transition-all cursor-pointer"
>
<Plus className="h-4 w-4 shrink-0" />
New Dashboard
{t('dashboards.new')}
</button>
</div>
<div className="relative px-3 pt-2">
@@ -122,7 +124,7 @@ export const DashboardListApp = () => {
type="text"
value={search}
onChange={(ev) => setSearch(ev.target.value)}
placeholder="Search dashboards"
placeholder={t('dashboards.search')}
className="w-full rounded-lg border border-duck-dark/15 bg-transparent py-1.5 pl-8 pr-3 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-duck-teal/30 focus:border-duck-teal/40"
/>
</div>
@@ -182,15 +184,15 @@ export const DashboardListApp = () => {
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete dashboard</AlertDialogTitle>
<AlertDialogTitle>{t('dashboards.delete')}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete <strong>{deleting?.name}</strong>? This action cannot be undone.
{t('dashboards.confirmDelete')} <strong>{deleting?.name}</strong>? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{tUi('button.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
Delete
{t('common.delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -19,21 +19,23 @@ import {
NEW_DASH_DESC_KEY,
NEW_DASH_TEMPLATE_KEY,
} from './constants';
import type { PanelTranslationKey } from '../../locales';
import { panelName, useTranslation } from '../../locales';
// --- Layout Templates ---
type LayoutTemplate = {
name: string;
nameKey: PanelTranslationKey;
layout: () => LayoutNode;
};
const templates: LayoutTemplate[] = [
{
name: 'Single',
nameKey: 'dashboards.templates.single',
layout: () => ({ type: 'panel', id: uid(), appType: null }),
},
{
name: '2 Columns',
nameKey: 'dashboards.templates.twoColumns',
layout: () => ({
type: 'group',
id: uid(),
@@ -45,7 +47,7 @@ const templates: LayoutTemplate[] = [
}),
},
{
name: 'Main + Side',
nameKey: 'dashboards.templates.mainSide',
layout: () => ({
type: 'group',
id: uid(),
@@ -68,7 +70,7 @@ const templates: LayoutTemplate[] = [
}),
},
{
name: 'Sidebar',
nameKey: 'dashboards.templates.sidebar',
layout: () => ({
type: 'group',
id: uid(),
@@ -80,7 +82,7 @@ const templates: LayoutTemplate[] = [
}),
},
{
name: '2x2 Grid',
nameKey: 'dashboards.templates.grid',
layout: () => ({
type: 'group',
id: uid(),
@@ -114,7 +116,7 @@ const templates: LayoutTemplate[] = [
}),
},
{
name: 'Cols + Bottom',
nameKey: 'dashboards.templates.colsBottom',
layout: () => ({
type: 'group',
id: uid(),
@@ -157,6 +159,7 @@ const ThumbnailNode = ({ node }: { node: LayoutNode }) => {
};
const TemplateCell = ({ index }: { index: number }) => {
const { t } = useTranslation();
const tpl = templates[index]!;
const [selected, setSelected] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
const node = tpl.layout();
@@ -174,7 +177,9 @@ const TemplateCell = ({ index }: { index: number }) => {
<div className="w-full flex-1 min-h-0 rounded border border-duck-dark/20 overflow-hidden">
<ThumbnailNode node={node} />
</div>
<span className={`text-xs font-medium ${isSelected ? 'text-duck-teal' : 'text-duck-dark/60'}`}>{tpl.name}</span>
<span className={`text-xs font-medium ${isSelected ? 'text-duck-teal' : 'text-duck-dark/60'}`}>
{t(tpl.nameKey)}
</span>
</button>
</div>
);
@@ -219,7 +224,7 @@ const tplPanelLayout: LayoutNode = {
const tplRegistry = Object.fromEntries(
templates.map((tpl, i) => [
`tpl-${i}`,
{ name: tpl.name, icon: Layout, component: () => <TemplateCell index={i} /> },
{ name: panelName(tpl.nameKey), icon: Layout, component: () => <TemplateCell index={i} /> },
]),
);
@@ -230,6 +235,7 @@ const TemplatePanel = () => (
// --- Panel: Name ---
const NamePanel = () => {
const { t } = useTranslation();
const [name, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
const [description, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
@@ -239,13 +245,13 @@ const NamePanel = () => {
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-duck-dark/50 flex items-center gap-1.5">
<Type className="h-3 w-3" />
Name
{t('dashboards.name')}
</label>
<input
type="text"
value={name}
onChange={(ev) => setName(ev.target.value)}
placeholder="My Dashboard"
placeholder={t('dashboards.namePlaceholder')}
autoFocus
className="rounded-lg border border-duck-dark/20 bg-background px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
/>
@@ -253,12 +259,12 @@ const NamePanel = () => {
<div className="flex flex-1 min-h-0 flex-col gap-1.5">
<label className="text-xs font-medium text-duck-dark/50 flex items-center gap-1.5">
<FileText className="h-3 w-3" />
Description
{t('dashboards.description')}
</label>
<textarea
value={description}
onChange={(ev) => setDescription(ev.target.value)}
placeholder="What is this dashboard for?"
placeholder={t('dashboards.descriptionPlaceholder')}
className="flex-1 min-h-0 resize-none rounded-lg border border-duck-dark/20 bg-background px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
/>
</div>
@@ -270,6 +276,7 @@ const NamePanel = () => {
// --- Panel: Create ---
const CreatePanel = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const client = useClient();
@@ -401,7 +408,7 @@ const CreatePanel = () => {
<Button asChild variant="outline" className="cursor-pointer">
<Link to={`/dashboards/${editingId}`}>
<ArrowRight className="h-4 w-4 mr-1" />
Open Dashboard
{t('dashboards.open')}
</Link>
</Button>
)}
@@ -453,13 +460,14 @@ const newDashLayout: LayoutNode = {
};
const NewDashboardForm = () => {
const { t } = useTranslation();
const { registry } = useAppRegistry();
const newDashRegistry = {
...registry,
'file-browser-cwd': { name: 'File Browser', icon: FolderOpen, component: CwdFileBrowser },
'new-ws-name': { name: 'Name', icon: Type, component: NamePanel },
'new-ws-template': { name: 'Template', icon: Layout, component: TemplatePanel },
'new-ws-create': { name: 'Create', icon: Rocket, component: CreatePanel },
'file-browser-cwd': { name: panelName('panelName.fileBrowser'), icon: FolderOpen, component: CwdFileBrowser },
'new-ws-name': { name: t('dashboards.name'), icon: Type, component: NamePanel },
'new-ws-template': { name: t('dashboards.template'), icon: Layout, component: TemplatePanel },
'new-ws-create': { name: t('dashboards.create'), icon: Rocket, component: CreatePanel },
};
return <WorkspaceLayout layout={newDashLayout} onLayoutChange={() => {}} registry={newDashRegistry} noHeader />;
@@ -470,6 +478,7 @@ const NewDashboardForm = () => {
// Only reached when nothing is selected AND nothing is being created or edited — DashboardPreview checks
// those first now, so this component no longer needs its own creating/editing branch.
const DashboardPreviewEmpty = () => {
const { t } = useTranslation();
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
const [, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
@@ -488,10 +497,10 @@ const DashboardPreviewEmpty = () => {
<div className="h-full w-full">
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
<LayoutGrid className="h-10 w-10 text-white/80" />
<p className="text-sm text-white/80">Select a dashboard or create a new one</p>
<p className="text-sm text-white/80">{t('dashboards.selectOne')}</p>
<Button onClick={handleCreate} className="bg-duck-teal hover:bg-duck-teal/90 cursor-pointer">
<Plus className="h-4 w-4 mr-1" />
New Dashboard
{t('dashboards.new')}
</Button>
</div>
</div>
@@ -1,6 +1,7 @@
{
"common": {
"save": "Save"
"save": "Save",
"delete": "Delete"
},
"minesweeper": {
"notInstalled": "Minesweeper is not installed on this server.",
@@ -136,5 +137,28 @@
"hostTerminal": "Host Terminal",
"runningShells": "Running Shells",
"pm2Processes": "pm2 processes"
},
"dashboards": {
"title": "Dashboards",
"new": "New Dashboard",
"delete": "Delete dashboard",
"confirmDelete": "Are you sure you want to delete",
"search": "Search dashboards",
"name": "Name",
"description": "Description",
"open": "Open Dashboard",
"selectOne": "Select a dashboard or create a new one",
"namePlaceholder": "My Dashboard",
"descriptionPlaceholder": "What is this dashboard for?",
"template": "Template",
"create": "Create",
"templates": {
"single": "Single",
"twoColumns": "2 Columns",
"mainSide": "Main + Side",
"sidebar": "Sidebar",
"grid": "2x2 Grid",
"colsBottom": "Cols + Bottom"
}
}
}
@@ -1,6 +1,7 @@
{
"common": {
"save": "Guardar"
"save": "Guardar",
"delete": "Eliminar"
},
"minesweeper": {
"notInstalled": "O Minesweeper não está instalado neste servidor.",
@@ -136,5 +137,28 @@
"hostTerminal": "Terminal do anfitrião",
"runningShells": "Shells em execução",
"pm2Processes": "processos pm2"
},
"dashboards": {
"title": "Painéis",
"new": "Novo painel",
"delete": "Eliminar painel",
"confirmDelete": "Tem a certeza de que quer eliminar",
"search": "Pesquisar painéis",
"name": "Nome",
"description": "Descrição",
"open": "Abrir painel",
"selectOne": "Selecione um painel ou crie um novo",
"namePlaceholder": "O meu painel",
"descriptionPlaceholder": "Para que serve este painel?",
"template": "Modelo",
"create": "Criar",
"templates": {
"single": "Único",
"twoColumns": "2 colunas",
"mainSide": "Principal + lateral",
"sidebar": "Barra lateral",
"grid": "Grelha 2x2",
"colsBottom": "Colunas + inferior"
}
}
}