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:
@@ -26,8 +26,10 @@ import {
|
|||||||
} from './constants';
|
} from './constants';
|
||||||
import { useNewDashboardDraft } from './useNewDashboardDraft';
|
import { useNewDashboardDraft } from './useNewDashboardDraft';
|
||||||
import { cwdToPath } from '../../components/Workspace';
|
import { cwdToPath } from '../../components/Workspace';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
export const DashboardListApp = () => {
|
export const DashboardListApp = () => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const queryClient = useQueryClient();
|
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"
|
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" />
|
<LayoutGrid className="h-4 w-4" />
|
||||||
Dashboards
|
{t('dashboards.title')}
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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" />
|
<Plus className="h-4 w-4 shrink-0" />
|
||||||
New Dashboard
|
{t('dashboards.new')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative px-3 pt-2">
|
<div className="relative px-3 pt-2">
|
||||||
@@ -122,7 +124,7 @@ export const DashboardListApp = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(ev) => setSearch(ev.target.value)}
|
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"
|
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>
|
</div>
|
||||||
@@ -182,15 +184,15 @@ export const DashboardListApp = () => {
|
|||||||
>
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete dashboard</AlertDialogTitle>
|
<AlertDialogTitle>{t('dashboards.delete')}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<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>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>{tUi('button.cancel')}</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
|
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
|
||||||
Delete
|
{t('common.delete')}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -19,21 +19,23 @@ import {
|
|||||||
NEW_DASH_DESC_KEY,
|
NEW_DASH_DESC_KEY,
|
||||||
NEW_DASH_TEMPLATE_KEY,
|
NEW_DASH_TEMPLATE_KEY,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import type { PanelTranslationKey } from '../../locales';
|
||||||
|
import { panelName, useTranslation } from '../../locales';
|
||||||
|
|
||||||
// --- Layout Templates ---
|
// --- Layout Templates ---
|
||||||
|
|
||||||
type LayoutTemplate = {
|
type LayoutTemplate = {
|
||||||
name: string;
|
nameKey: PanelTranslationKey;
|
||||||
layout: () => LayoutNode;
|
layout: () => LayoutNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const templates: LayoutTemplate[] = [
|
const templates: LayoutTemplate[] = [
|
||||||
{
|
{
|
||||||
name: 'Single',
|
nameKey: 'dashboards.templates.single',
|
||||||
layout: () => ({ type: 'panel', id: uid(), appType: null }),
|
layout: () => ({ type: 'panel', id: uid(), appType: null }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '2 Columns',
|
nameKey: 'dashboards.templates.twoColumns',
|
||||||
layout: () => ({
|
layout: () => ({
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: uid(),
|
id: uid(),
|
||||||
@@ -45,7 +47,7 @@ const templates: LayoutTemplate[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Main + Side',
|
nameKey: 'dashboards.templates.mainSide',
|
||||||
layout: () => ({
|
layout: () => ({
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: uid(),
|
id: uid(),
|
||||||
@@ -68,7 +70,7 @@ const templates: LayoutTemplate[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sidebar',
|
nameKey: 'dashboards.templates.sidebar',
|
||||||
layout: () => ({
|
layout: () => ({
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: uid(),
|
id: uid(),
|
||||||
@@ -80,7 +82,7 @@ const templates: LayoutTemplate[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '2x2 Grid',
|
nameKey: 'dashboards.templates.grid',
|
||||||
layout: () => ({
|
layout: () => ({
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: uid(),
|
id: uid(),
|
||||||
@@ -114,7 +116,7 @@ const templates: LayoutTemplate[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Cols + Bottom',
|
nameKey: 'dashboards.templates.colsBottom',
|
||||||
layout: () => ({
|
layout: () => ({
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: uid(),
|
id: uid(),
|
||||||
@@ -157,6 +159,7 @@ const ThumbnailNode = ({ node }: { node: LayoutNode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TemplateCell = ({ index }: { index: number }) => {
|
const TemplateCell = ({ index }: { index: number }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const tpl = templates[index]!;
|
const tpl = templates[index]!;
|
||||||
const [selected, setSelected] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
const [selected, setSelected] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
||||||
const node = tpl.layout();
|
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">
|
<div className="w-full flex-1 min-h-0 rounded border border-duck-dark/20 overflow-hidden">
|
||||||
<ThumbnailNode node={node} />
|
<ThumbnailNode node={node} />
|
||||||
</div>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -219,7 +224,7 @@ const tplPanelLayout: LayoutNode = {
|
|||||||
const tplRegistry = Object.fromEntries(
|
const tplRegistry = Object.fromEntries(
|
||||||
templates.map((tpl, i) => [
|
templates.map((tpl, i) => [
|
||||||
`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 ---
|
// --- Panel: Name ---
|
||||||
|
|
||||||
const NamePanel = () => {
|
const NamePanel = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [name, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
const [name, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
||||||
const [description, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
const [description, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
||||||
|
|
||||||
@@ -239,13 +245,13 @@ const NamePanel = () => {
|
|||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs font-medium text-duck-dark/50 flex items-center 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" />
|
<Type className="h-3 w-3" />
|
||||||
Name
|
{t('dashboards.name')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(ev) => setName(ev.target.value)}
|
onChange={(ev) => setName(ev.target.value)}
|
||||||
placeholder="My Dashboard"
|
placeholder={t('dashboards.namePlaceholder')}
|
||||||
autoFocus
|
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"
|
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">
|
<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">
|
<label className="text-xs font-medium text-duck-dark/50 flex items-center gap-1.5">
|
||||||
<FileText className="h-3 w-3" />
|
<FileText className="h-3 w-3" />
|
||||||
Description
|
{t('dashboards.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(ev) => setDescription(ev.target.value)}
|
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"
|
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>
|
</div>
|
||||||
@@ -270,6 +276,7 @@ const NamePanel = () => {
|
|||||||
// --- Panel: Create ---
|
// --- Panel: Create ---
|
||||||
|
|
||||||
const CreatePanel = () => {
|
const CreatePanel = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
@@ -401,7 +408,7 @@ const CreatePanel = () => {
|
|||||||
<Button asChild variant="outline" className="cursor-pointer">
|
<Button asChild variant="outline" className="cursor-pointer">
|
||||||
<Link to={`/dashboards/${editingId}`}>
|
<Link to={`/dashboards/${editingId}`}>
|
||||||
<ArrowRight className="h-4 w-4 mr-1" />
|
<ArrowRight className="h-4 w-4 mr-1" />
|
||||||
Open Dashboard
|
{t('dashboards.open')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -453,13 +460,14 @@ const newDashLayout: LayoutNode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NewDashboardForm = () => {
|
const NewDashboardForm = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { registry } = useAppRegistry();
|
const { registry } = useAppRegistry();
|
||||||
const newDashRegistry = {
|
const newDashRegistry = {
|
||||||
...registry,
|
...registry,
|
||||||
'file-browser-cwd': { name: 'File Browser', icon: FolderOpen, component: CwdFileBrowser },
|
'file-browser-cwd': { name: panelName('panelName.fileBrowser'), icon: FolderOpen, component: CwdFileBrowser },
|
||||||
'new-ws-name': { name: 'Name', icon: Type, component: NamePanel },
|
'new-ws-name': { name: t('dashboards.name'), icon: Type, component: NamePanel },
|
||||||
'new-ws-template': { name: 'Template', icon: Layout, component: TemplatePanel },
|
'new-ws-template': { name: t('dashboards.template'), icon: Layout, component: TemplatePanel },
|
||||||
'new-ws-create': { name: 'Create', icon: Rocket, component: CreatePanel },
|
'new-ws-create': { name: t('dashboards.create'), icon: Rocket, component: CreatePanel },
|
||||||
};
|
};
|
||||||
|
|
||||||
return <WorkspaceLayout layout={newDashLayout} onLayoutChange={() => {}} registry={newDashRegistry} noHeader />;
|
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
|
// 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.
|
// those first now, so this component no longer needs its own creating/editing branch.
|
||||||
const DashboardPreviewEmpty = () => {
|
const DashboardPreviewEmpty = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
|
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
|
||||||
const [, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
const [, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
||||||
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
||||||
@@ -488,10 +497,10 @@ const DashboardPreviewEmpty = () => {
|
|||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
|
||||||
<LayoutGrid className="h-10 w-10 text-white/80" />
|
<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">
|
<Button onClick={handleCreate} className="bg-duck-teal hover:bg-duck-teal/90 cursor-pointer">
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
New Dashboard
|
{t('dashboards.new')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"save": "Save"
|
"save": "Save",
|
||||||
|
"delete": "Delete"
|
||||||
},
|
},
|
||||||
"minesweeper": {
|
"minesweeper": {
|
||||||
"notInstalled": "Minesweeper is not installed on this server.",
|
"notInstalled": "Minesweeper is not installed on this server.",
|
||||||
@@ -136,5 +137,28 @@
|
|||||||
"hostTerminal": "Host Terminal",
|
"hostTerminal": "Host Terminal",
|
||||||
"runningShells": "Running Shells",
|
"runningShells": "Running Shells",
|
||||||
"pm2Processes": "pm2 processes"
|
"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": {
|
"common": {
|
||||||
"save": "Guardar"
|
"save": "Guardar",
|
||||||
|
"delete": "Eliminar"
|
||||||
},
|
},
|
||||||
"minesweeper": {
|
"minesweeper": {
|
||||||
"notInstalled": "O Minesweeper não está instalado neste servidor.",
|
"notInstalled": "O Minesweeper não está instalado neste servidor.",
|
||||||
@@ -136,5 +137,28 @@
|
|||||||
"hostTerminal": "Terminal do anfitrião",
|
"hostTerminal": "Terminal do anfitrião",
|
||||||
"runningShells": "Shells em execução",
|
"runningShells": "Shells em execução",
|
||||||
"pm2Processes": "processos pm2"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user