fix(images): default arch to node platform; fix node selector placeholder
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Successful in 55s
Deploy to Production / Trigger production update (push) Failing after 10s

- arch filter defaults to amd64 and auto-syncs to the selected node's arch
  (useNodeArch), so amd64 nodes no longer show arm64 images by default
- node Select no longer renders raw '__none__'; shows 'Нода' placeholder
  and a disabled hint when no nodes are reachable

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:28:04 +05:00
parent 2dff9f8c5c
commit 03b574777c
2 changed files with 23 additions and 3 deletions

View File

@@ -13,7 +13,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { useServers, useNodes } from '@/hooks/use-nodes'; import { useServers, useNodes } from '@/hooks/use-nodes';
import { useImageCatalog, useImageLXCTemplates } from '@/hooks/use-image-catalog'; import { useImageCatalog, useImageLXCTemplates, useNodeArch } from '@/hooks/use-image-catalog';
import { useProfile } from '@/hooks/use-settings'; import { useProfile } from '@/hooks/use-settings';
import DownloadImageDialog, { type SelectedImage } from './DownloadImageDialog'; import DownloadImageDialog, { type SelectedImage } from './DownloadImageDialog';
import MirrorsManager from './MirrorsManager'; import MirrorsManager from './MirrorsManager';
@@ -23,7 +23,8 @@ export default function ImagesPage() {
const [serverId, setServerId] = useState<number | null>(null); const [serverId, setServerId] = useState<number | null>(null);
const [node, setNode] = useState<string>(''); const [node, setNode] = useState<string>('');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [archFilter, setArchFilter] = useState<string>(''); // По умолчанию показываем amd64 (доминирующая платформа PVE); уточняется по арх ноды
const [archFilter, setArchFilter] = useState<string>('amd64');
const [selected, setSelected] = useState<SelectedImage | null>(null); const [selected, setSelected] = useState<SelectedImage | null>(null);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
@@ -35,6 +36,7 @@ export default function ImagesPage() {
const { data: catalog } = useImageCatalog(); const { data: catalog } = useImageCatalog();
const { data: lxcRepo = [] } = useImageLXCTemplates(serverId ?? undefined, node || undefined); const { data: lxcRepo = [] } = useImageLXCTemplates(serverId ?? undefined, node || undefined);
const { data: nodeArch } = useNodeArch(serverId ?? undefined, node || undefined);
// Авто-выбор первого сервера и ноды // Авто-выбор первого сервера и ноды
useEffect(() => { useEffect(() => {
@@ -43,6 +45,10 @@ export default function ImagesPage() {
useEffect(() => { useEffect(() => {
if (!node && nodes.length > 0) setNode(nodes[0].node); if (!node && nodes.length > 0) setNode(nodes[0].node);
}, [nodes, node]); }, [nodes, node]);
// Подстроить фильтр архитектуры под выбранную ноду (если арх определилась)
useEffect(() => {
if (nodeArch?.arch) setArchFilter(nodeArch.arch);
}, [nodeArch?.arch, node]);
const cloudImages = useMemo<CatalogImage[]>(() => { const cloudImages = useMemo<CatalogImage[]>(() => {
const all = [...(catalog?.builtin ?? []), ...(catalog?.mirrors ?? [])]; const all = [...(catalog?.builtin ?? []), ...(catalog?.mirrors ?? [])];
@@ -83,9 +89,10 @@ export default function ImagesPage() {
{servers.map((s) => <SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>)} {servers.map((s) => <SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>)}
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={node || '__none__'} onValueChange={(v) => { if (v !== null) setNode(v === '__none__' ? '' : v); }}> <Select value={node} onValueChange={(v) => { if (v) setNode(v); }}>
<SelectTrigger className="w-[180px]"><SelectValue placeholder="Нода" /></SelectTrigger> <SelectTrigger className="w-[180px]"><SelectValue placeholder="Нода" /></SelectTrigger>
<SelectContent> <SelectContent>
{nodes.length === 0 && <SelectItem value="__empty__" disabled>Нет доступных нод</SelectItem>}
{nodes.map((n) => <SelectItem key={n.node} value={n.node}>{n.node}</SelectItem>)} {nodes.map((n) => <SelectItem key={n.node} value={n.node}>{n.node}</SelectItem>)}
</SelectContent> </SelectContent>
</Select> </Select>

View File

@@ -37,6 +37,19 @@ export function useImageLXCTemplates(serverId?: number, node?: string) {
}); });
} }
// Архитектура ноды (amd64|arm64) — для дефолтного фильтра образов.
export function useNodeArch(serverId?: number, node?: string) {
return useQuery({
queryKey: ['image-node-arch', serverId, node],
queryFn: () =>
apiClient.get<{ arch: string | null; machine: string | null }>(
`/proxmox/api/${serverId}/images/node-arch?node=${encodeURIComponent(node!)}`,
),
enabled: !!serverId && !!node,
staleTime: 10 * 60 * 1000,
});
}
// Целевые хранилища ноды по типу контента (import|vztmpl|iso). // Целевые хранилища ноды по типу контента (import|vztmpl|iso).
export function useImageStorages(serverId?: number, node?: string, content?: string) { export function useImageStorages(serverId?: number, node?: string, content?: string) {
return useQuery({ return useQuery({