fix(images): default arch to node platform; fix node selector placeholder
- 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:
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user