feat: add OS logo component for templates and instances
- Add OsLogo component with SVG icons for major distros (Ubuntu, Debian, etc.) - Integrate OsLogo in TemplatesPage, CreateInstanceWizard, InstanceDetailPage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -98,7 +98,8 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
const { data: templates = [] } = useTemplates(undefined, selectedServer?.id);
|
||||
const { data: networks = [] } = useIPAMNetworks();
|
||||
const { data: lxcTemplates = [], isLoading: lxcLoading } = useLXCTemplates(selectedServer?.id);
|
||||
const { data: lxcStorages = [] } = useLXCStorages(selectedServer?.id);
|
||||
const { data: lxcStorages = [] } = useLXCStorages(selectedServer?.id, undefined, 'rootdir');
|
||||
const { data: vmStorages = [] } = useLXCStorages(selectedServer?.id, undefined, 'images');
|
||||
|
||||
// ── SSH Keys & ownership ──────────────────────────────────────────────────
|
||||
const { data: profile } = useProfile();
|
||||
@@ -223,6 +224,21 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
}
|
||||
};
|
||||
|
||||
// ── Filtered IPAM networks ────────────────────────────────────────────────
|
||||
// Show only networks matching the selected server (or global networks with no server assigned)
|
||||
// Further filter by node if a node is set
|
||||
const vmFilteredNetworks = networks.filter(n =>
|
||||
n.is_active &&
|
||||
(!n.proxmox_server_id || !selectedServer || n.proxmox_server_id === selectedServer.id) &&
|
||||
(!n.proxmox_node || !selectedNode || n.proxmox_node === selectedNode)
|
||||
);
|
||||
|
||||
const lxcFilteredNetworks = networks.filter(n =>
|
||||
n.is_active &&
|
||||
(!n.proxmox_server_id || !selectedServer || n.proxmox_server_id === selectedServer.id) &&
|
||||
(!n.proxmox_node || !selectedLXCTemplate?.node || n.proxmox_node === selectedLXCTemplate.node)
|
||||
);
|
||||
|
||||
// Apply template defaults when template selected
|
||||
const selectTemplate = (tpl: OSTemplate) => {
|
||||
setSelectedTemplate(tpl);
|
||||
@@ -460,6 +476,18 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
onChange={e => setConfig(p => ({ ...p, disk: Number(e.target.value) }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{t('wizard.storage')}</Label>
|
||||
<Select value={config.storage} onValueChange={v => setConfig(p => ({ ...p, storage: v }))}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{vmStorages.length > 0
|
||||
? vmStorages.map(s => <SelectItem key={s.storage} value={s.storage}>{s.storage} ({s.type})</SelectItem>)
|
||||
: <SelectItem value="local-lvm">local-lvm</SelectItem>
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -476,7 +504,7 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
<Select value={String(config.ipam_network_id || '')} onValueChange={v => setConfig(p => ({ ...p, ipam_network_id: v ? Number(v) : null }))}>
|
||||
<SelectTrigger><SelectValue placeholder={t('wizard.manual_ip')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{networks.map(n => (
|
||||
{vmFilteredNetworks.map(n => (
|
||||
<SelectItem key={n.id} value={String(n.id)}>{n.name} ({n.network})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -517,15 +545,17 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
/>
|
||||
)}
|
||||
<SSHKeyPicker keys={availableKeys} selected={selectedKeyIds} onToggle={toggleKey} />
|
||||
<div>
|
||||
<Label>SSH Keys ({t('common.optional')})</Label>
|
||||
<textarea
|
||||
className="w-full rounded-md border bg-transparent px-3 py-2 text-sm font-mono min-h-[60px]"
|
||||
value={config.ssh_keys}
|
||||
onChange={e => setConfig(p => ({ ...p, ssh_keys: e.target.value }))}
|
||||
placeholder={t('common.placeholder_ssh_key')}
|
||||
/>
|
||||
</div>
|
||||
{availableKeys.length === 0 && (
|
||||
<div>
|
||||
<Label>SSH Keys <span className="text-muted-foreground text-xs font-normal">(безопаснее)</span></Label>
|
||||
<textarea
|
||||
className="w-full rounded-md border bg-transparent px-3 py-2 text-sm font-mono min-h-[60px]"
|
||||
value={config.ssh_keys}
|
||||
onChange={e => setConfig(p => ({ ...p, ssh_keys: e.target.value }))}
|
||||
placeholder={t('common.placeholder_ssh_key')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -620,7 +650,7 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
<Select value={String(lxcConfig.ipam_network_id || '')} onValueChange={v => setLxcConfig(p => ({ ...p, ipam_network_id: v ? Number(v) : null }))}>
|
||||
<SelectTrigger><SelectValue placeholder={t('wizard.manual_ip')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{networks.map(n => (
|
||||
{lxcFilteredNetworks.map(n => (
|
||||
<SelectItem key={n.id} value={String(n.id)}>{n.name} ({n.network})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -666,15 +696,17 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
/>
|
||||
)}
|
||||
<SSHKeyPicker keys={availableKeys} selected={selectedKeyIds} onToggle={toggleKey} />
|
||||
<div>
|
||||
<Label>SSH Keys ({t('common.optional')})</Label>
|
||||
<textarea
|
||||
className="w-full rounded-md border bg-transparent px-3 py-2 text-sm font-mono min-h-[60px]"
|
||||
value={lxcConfig.ssh_keys}
|
||||
onChange={e => setLxcConfig(p => ({ ...p, ssh_keys: e.target.value }))}
|
||||
placeholder={t('common.placeholder_ssh_key')}
|
||||
/>
|
||||
</div>
|
||||
{availableKeys.length === 0 && (
|
||||
<div>
|
||||
<Label>SSH Keys <span className="text-muted-foreground text-xs font-normal">(безопаснее)</span></Label>
|
||||
<textarea
|
||||
className="w-full rounded-md border bg-transparent px-3 py-2 text-sm font-mono min-h-[60px]"
|
||||
value={lxcConfig.ssh_keys}
|
||||
onChange={e => setLxcConfig(p => ({ ...p, ssh_keys: e.target.value }))}
|
||||
placeholder={t('common.placeholder_ssh_key')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -694,6 +726,7 @@ export default function CreateInstanceWizard({ onClose }: { onClose?: () => void
|
||||
<Row label={t('common.vcpu')} value={String(config.cores)} />
|
||||
<Row label={t('wizard.memory_mb')} value={`${config.memory} MB`} />
|
||||
<Row label={t('wizard.disk_gb')} value={`${config.disk} GB`} />
|
||||
<Row label={t('wizard.storage')} value={config.storage} />
|
||||
<Row label={t('wizard.bridge')} value={config.bridge} />
|
||||
{config.ipam_network_id && <Row label={t('common.ipam')} value={networks.find(n => n.id === config.ipam_network_id)?.name || 'Auto'} />}
|
||||
{config.ip_address && <Row label={t('common.primary_ip')} value={config.ip_address} />}
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { StatusDot } from '@/components/shared/status-dot';
|
||||
import { useVirtualMachines, usePowerAction } from '@/hooks/use-instances';
|
||||
import { useVirtualMachines, usePowerAction, useVMInterfaces } from '@/hooks/use-instances';
|
||||
import InstanceActionDialogs, { PowerConfirmDialog, type InstanceAction, type PowerAction } from './InstanceActionDialogs';
|
||||
import { vmTypeLabel } from '@/lib/format';
|
||||
import { toast } from 'sonner';
|
||||
@@ -59,6 +59,13 @@ export default function InstanceDetailPage() {
|
||||
|
||||
const power = usePowerAction(sid, vid, type, node);
|
||||
const isRunning = vm?.status === 'running';
|
||||
|
||||
const { data: ifaces } = useVMInterfaces(sid, vid, type, node, isRunning);
|
||||
const primaryLiveIP = ifaces?.interfaces
|
||||
?.flatMap((i) => i.ips || [])
|
||||
.find((ip) => ip.type === 'ipv4' && !ip.address.startsWith('127.'))
|
||||
?.address;
|
||||
const displayIP = primaryLiveIP || vm?.ip_address;
|
||||
const isQemu = type === 'qemu';
|
||||
const TypeIcon = type === 'qemu' ? Monitor : Container;
|
||||
const [dialog, setDialog] = useState<InstanceAction>(null);
|
||||
@@ -125,7 +132,7 @@ export default function InstanceDetailPage() {
|
||||
{vm?.status || 'unknown'}
|
||||
</span>
|
||||
<span>Node: {node}</span>
|
||||
{vm?.ip_address && <span className="font-mono">{vm.ip_address}</span>}
|
||||
{displayIP && <span className="font-mono">{displayIP}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
237
frontend/src/features/templates/OsLogo.tsx
Normal file
237
frontend/src/features/templates/OsLogo.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Monitor } from 'lucide-react';
|
||||
|
||||
type OsInfo = {
|
||||
label: string;
|
||||
bg: string;
|
||||
fg: string;
|
||||
svg?: JSX.Element;
|
||||
};
|
||||
|
||||
const OS_MAP: Array<{ keys: string[]; info: OsInfo }> = [
|
||||
{
|
||||
keys: ['ubuntu'],
|
||||
info: {
|
||||
label: 'U',
|
||||
bg: '#f47421',
|
||||
fg: '#fff',
|
||||
svg: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100">
|
||||
<circle fill="#f47421" cy="50" cx="50" r="45"/>
|
||||
<circle fill="none" stroke="#ffffff" strokeWidth="8.55" cx="50" cy="50" r="21.825"/>
|
||||
<g id="ubuntu-friend">
|
||||
<circle fill="#f47421" cx="19.4" cy="50" r="8.4376"/>
|
||||
<path stroke="#f47421" strokeWidth="3.2378" d="M67,50H77"/>
|
||||
<circle fill="#ffffff" cx="19.4" cy="50" r="6.00745"/>
|
||||
</g>
|
||||
<use xlinkHref="#ubuntu-friend" transform="rotate(120,50,50)"/>
|
||||
<use xlinkHref="#ubuntu-friend" transform="rotate(240,50,50)"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['debian'],
|
||||
info: {
|
||||
label: 'D',
|
||||
bg: '#ffffff',
|
||||
fg: '#D70751',
|
||||
svg: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-60 -75 210 260">
|
||||
<g fill="#D70751">
|
||||
<path d="M64.525 62.053c-4.125.058.78 2.125 6.165 2.954 1.488-1.161 2.838-2.336 4.04-3.479-3.354.821-6.765.838-10.205.525m22.14-5.52c2.457-3.389 4.246-7.102 4.878-10.939-.551 2.736-2.035 5.099-3.435 7.592-7.711 4.854-.726-2.883-.004-5.824-8.29 10.436-1.138 6.257-1.439 9.171m8.174-21.265c.497-7.428-1.462-5.08-2.121-2.245.766.4 1.377 5.237 2.121 2.245M48.883-66.264c2.201.395 4.757.698 4.398 1.224 2.407-.528 2.954-1.015-4.398-1.224"/>
|
||||
<path d="m53.281-65.04-1.556.32 1.448-.127.108-.193"/>
|
||||
<path d="M121.93 38.085c.247 6.671-1.95 9.907-3.932 15.637l-3.564 1.781c-2.919 5.666.282 3.598-1.807 8.105-4.556 4.049-13.823 12.67-16.789 13.457-2.163-.047 1.469-2.554 1.943-3.537-6.097 4.188-4.894 6.285-14.217 8.83l-.273-.607C60.29 92.569 28.344 71.129 28.765 41.875c-.246 1.857-.698 1.393-1.208 2.144-1.186-15.052 6.952-30.17 20.675-36.343 13.427-6.646 29.163-3.918 38.78 5.044C81.73 5.8 71.217-1.534 58.757-.848c-12.208.193-23.625 7.95-27.436 16.369-6.253 3.938-6.979 15.177-9.704 17.233-3.665 26.943 6.896 38.583 24.762 52.275 2.812 1.896.792 2.184 1.173 3.627-5.936-2.779-11.372-6.976-15.841-12.114 2.372 3.473 4.931 6.847 8.239 9.499-5.596-1.897-13.074-13.563-15.256-14.038 9.647 17.274 39.142 30.295 54.587 23.836-7.146.263-16.226.146-24.256-2.822-3.371-1.734-7.958-5.331-7.14-6.003 21.079 7.875 42.854 5.965 61.09-8.655 4.641-3.614 9.709-9.761 11.173-9.846-2.206 3.317.377 1.596-1.318 4.523 4.625-7.456-2.008-3.035 4.779-12.877l2.507 3.453c-.931-6.188 7.687-13.704 6.813-23.492 1.975-2.994 2.206 3.22.107 10.107 2.912-7.64.767-8.867 1.516-15.171.81 2.118 1.867 4.37 2.412 6.606-1.895-7.382 1.948-12.433 2.898-16.724-.937-.415-2.928 3.264-3.383-5.457.065-3.788 1.054-1.985 1.435-2.917-.744-.427-2.694-3.33-3.88-8.9.86-1.308 2.3 3.393 3.47 3.586-.753-4.429-2.049-7.805-2.103-11.202-3.421-7.149-1.211.953-3.985-3.069-3.641-11.357 3.021-2.637 3.47-7.796 5.52 7.995 8.667 20.387 10.11 25.519-1.103-6.258-2.883-12.32-5.058-18.185 1.677.705-2.699-12.875 2.18-3.882-5.21-19.172-22.302-37.087-38.025-45.493 1.924 1.76 4.354 3.971 3.481 4.317-7.819-4.656-6.444-5.018-7.565-6.985-6.369-2.591-6.788.208-11.007.004-12.005-6.368-14.318-5.69-25.368-9.681l.502 2.349c-7.953-2.649-9.265 1.005-17.862.009-.523-.409 2.753-1.479 5.452-1.871-7.69 1.015-7.329-1.515-14.854.279 1.855-1.301 3.815-2.162 5.793-3.269-6.271.381-14.971 3.649-12.286.677C20.144-62.46 1.976-56.053-8.218-46.494l-.321-2.142c-4.672 5.608-20.371 16.748-21.622 24.011l-1.249.291c-2.431 4.116-4.004 8.781-5.932 13.016-3.18 5.417-4.661 2.085-4.208 2.934-6.253 12.679-9.359 23.332-12.043 32.069 1.912 2.858.046 17.206.769 28.688-3.141 56.709 39.8 111.77 86.737 124.48 6.88 2.459 17.11 2.364 25.813 2.618-10.268-2.937-11.595-1.556-21.595-5.044-7.215-3.398-8.797-7.277-13.907-11.711l2.022 3.573c-10.021-3.547-5.829-4.39-13.982-6.972l2.16-2.82c-3.249-.246-8.604-5.475-10.069-8.371l-3.553.14c-4.27-5.269-6.545-9.063-6.379-12.005l-1.148 2.047c-1.301-2.235-15.709-19.759-8.234-15.679-1.389-1.271-3.235-2.067-5.237-5.703l1.522-1.739c-3.597-4.627-6.621-10.562-6.391-12.536 1.919 2.592 3.25 3.075 4.568 3.52-9.083-22.539-9.593-1.242-16.474-22.942l1.456-.116c-1.116-1.682-1.793-3.506-2.69-5.298l.633-6.313c-6.541-7.562-1.829-32.151-.887-45.637.655-5.485 5.459-11.322 9.114-20.477l-2.227-.384C-27.316-2.419-7.271-24.81 2.011-23.658c4.499-5.649-.892-.02-1.772-1.443 9.878-10.223 12.984-7.222 19.65-9.061 7.19-4.268-6.17 1.664-2.761-1.628 12.427-3.174 8.808-7.216 25.021-8.828 1.71.973-3.969 1.503-5.395 2.766 10.354-5.066 32.769-3.914 47.326 2.811 16.895 7.896 35.873 31.232 36.622 53.189l.852.229c-.431 8.729 1.336 18.822-1.727 28.094l2.1-4.385"/>
|
||||
<path d="m19.5 67.715-.578 2.893c2.71 3.683 4.861 7.673 8.323 10.552-2.49-4.863-4.341-6.872-7.745-13.445m6.409-.251c-1.435-1.587-2.284-3.497-3.235-5.4.909 3.345 2.771 6.219 4.504 9.143l-1.269-3.743m113.411-24.65-.605 1.52c-1.111 7.892-3.511 15.701-7.189 22.941 4.06-7.639 6.69-15.995 7.79-24.461M49.698-68.243c2.789-1.022 6.855-.56 9.814-1.233-3.855.324-7.693.517-11.484 1.005l1.67.228m-97.917 52.067c.642 5.951-4.477 8.26 1.134 4.337 3.007-6.773-1.175-1.87-1.134-4.337m-6.593 27.538c1.292-3.967 1.526-6.349 2.02-8.645-3.571 4.566-1.643 5.539-2.02 8.645"/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['windows', 'win2', 'win10', 'win11', 'winserver', 'win7', 'win8'],
|
||||
info: {
|
||||
label: 'W',
|
||||
bg: '#0078D4',
|
||||
fg: '#fff',
|
||||
svg: (
|
||||
<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 5.6L10.3 4.6V11.5H3V5.6ZM11.2 4.5L21 3V11.5H11.2V4.5ZM3 12.5H10.3V19.4L3 18.4V12.5ZM11.2 12.5H21V21L11.2 19.5V12.5Z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['centos'],
|
||||
info: {
|
||||
label: 'C',
|
||||
bg: '#ffffff',
|
||||
fg: '#932279',
|
||||
svg: (
|
||||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Green top-left */}
|
||||
<polygon points="50,50 3,3 3,47 47,47" fill="#9ccd2a"/>
|
||||
{/* Purple top-right */}
|
||||
<polygon points="50,50 97,3 53,3 53,47" fill="#932279"/>
|
||||
{/* Yellow bottom-right */}
|
||||
<polygon points="50,50 97,97 97,53 53,53" fill="#efa724"/>
|
||||
{/* Blue bottom-left */}
|
||||
<polygon points="50,50 3,97 47,97 47,53" fill="#262577"/>
|
||||
{/* White center cross */}
|
||||
<rect x="47" y="3" width="6" height="94" fill="white"/>
|
||||
<rect x="3" y="47" width="94" height="6" fill="white"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['rocky', 'rockylinux'],
|
||||
info: {
|
||||
label: 'R',
|
||||
bg: '#10B981',
|
||||
fg: '#fff',
|
||||
svg: (
|
||||
<svg viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M186.658 127.658C190.119 117.746 192 107.093 192 96C192 42.9807 149.019 0 96 0C42.9807 0 0 42.9807 0 96C0 122.234 10.523 146.011 27.5783 163.338L124.958 65.9584L149 90L186.658 127.658ZM169.122 158.205L124.958 114.042L55.7978 183.202C68.0269 188.849 81.6455 192 96 192C125.288 192 151.514 178.884 169.122 158.205Z" fill="white"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['alma', 'almalinux'],
|
||||
info: {
|
||||
label: 'A',
|
||||
bg: '#ffffff',
|
||||
fg: '#000',
|
||||
svg: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61.028259 59.731277">
|
||||
<path d="M56.11382 33.731278c2.6-.2 4.7 1.5 4.9 4.1.2 2.7-1.7 4.9-4.3 5.1-2.5.2-4.7-1.7-4.9-4.2-.2-2.7 1.6-4.7 4.3-5z" fill="#86da2f"/>
|
||||
<path d="M24.51382 55.031278c0-2.6 2-4.6 4.4-4.6 2.4 0 4.7 2.2 4.7 4.7 0 2.4-2 4.5-4.3 4.6-2.9 0-4.8-1.8-4.8-4.7z" fill="#24c2ff"/>
|
||||
<path d="M31.61382 25.831278c-.4.2-.6-.1-.7-.4-3.7-6.9-2.6-15.6000004 3.9-20.8000004 1.7-1.4 4.9-1.7 6.3-.3.6.5.7 1.1.8 1.8.2 1.5.5 3 1.5 4.2000004 1.1 1.3 2.5 1.8 4.1 1.7 1.4 0 2.8-.2 3.7 1.4.5.9.3 4.4-.5 5.1-.4.3-.7.1-1 0-2.3-.9-4.7-.9-7.1-.5-.8.1-1.2-.1-1.2-1-.1-1.5-.4-2.9-1.2-4.2-1.5-2.7-4.3-2.8-6.1-.3-1.5 2-1.9 4.4-2.3 6.8-.4 2.1-.3 4.3-.2 6.5 0 0-.1 0 0 0z" fill="#ffcb12"/>
|
||||
<path d="M34.11382 27.331278c-.2-.3-.1-.6.2-.8 5.7-5.2 14.2-6.2 20.8-1.1 1.7 1.4 2.8 4.3 1.9 6-.4.7-.9 1-1.5 1.2-1.4.6-2.7 1.2-3.6 2.5-.9 1.3-1.1 2.8-.7 4.4.3 1.3.8 2.7-.5 3.9-.7.7-4.1 1.3-5 .7-.4-.3-.3-.6-.2-1 .3-2.5-.3-4.8-1.2-7-.3-.8-.2-1.2.6-1.4 1.4-.4 2.7-1.1 3.7-2.1 2.2-2.1 1.7-4.8-1.2-6-2.3-1-4.7-.8-7-.6-2.2.1-4.3.7-6.3 1.3z" fill="#86da2f"/>
|
||||
<path d="M32.81382 29.931278c.3-.3.5-.2.8 0 6.6 4 10 11.9 7 19.6-.8 2-3.4 4-5.3 3.5-.8-.2-1.2-.6-1.6-1.1-.9-1.2-1.9-2.3-3.4-2.8-1.6-.5-3-.2-4.4.6-1.2.7-2.4 1.6-3.9.7-.9-.5-2.4-3.6-2.1-4.6.2-.4.6-.4 1-.4 2.5-.4 4.5-1.6 6.4-3.2.6-.5 1.1-.5 1.6.2.8 1.2 1.8 2.2 3.1 2.9 2.6 1.5 5.1.2 5.4-2.8.3-2.5-.6-4.7-1.4-6.9-.9-2-2-3.9-3.2-5.7z" fill="#24c2ff"/>
|
||||
<path d="M29.61382 30.531278c-.4 2-1.3 3.9-2.5 5.6-3.6 5.4-8.8 7.6-15.2 7-2.2999997-.2-4.1999997-2.1-4.3999997-4-.1-.8.1-1.4.6-2 .7-.9 1.3-1.7 1.6-2.8.5999997-2.2-.2-4-1.8-5.6-2.2-2.2-1.9-4.2.7-5.8.3-.2.7-.4 1.1-.6.5999997-.3 1.0999997-.3 1.2999997.4.9 2.3 2.7 4 4.7 5.4.7.6.7 1 .1 1.7-1.2 1.3-1.9 2.9-2 4.7-.2 2.2 1.1 3.6 3.3 3.6 1.4 0 2.7-.5 3.9-1.1 3.1-1.6 5.5-3.9 7.8-6.3.3-.1.4-.3.8-.2z" fill="#0069da"/>
|
||||
<path d="M13.21382 9.5312776c.2 0 .7.1 1.2.2 3.7.7000004 6-.6 7.2-4.1.8-2.3 2.5-3 4.7-1.8.1 0 .1.1.2.1 2.3 1.3 2.3 1.5.9 3.5-1.2 1.6-1.8 3.4000004-2.1 5.3000004-.2 1.1-.6 1.3-1.6.9-1.6-.6-3.3-.6-5 0-1.9.6-2.7 2.3-2.1 4.2.8 2.5 3 3.6 4.9 4.9 1.9 1.3 4.1 2 6.2 2.9.3.1.8.1.7.6-.1.3-.5.3-.9.3-4.5.2-8.8-.5-12.3-3.5-3.3-2.7-5.6999997-6-5.2999997-10.6.2999997-1.5 1.3999997-2.6000004 3.2999997-2.9000004z" fill="#ff4649"/>
|
||||
<path d="M5.0138203 37.631278c-2.4.3-4.80000003-1.7-5.00000003-4.2-.2-2.4 1.80000003-4.8 4.10000003-5 2.6-.3 5 1.5 5.2 3.9.1 2.3-1.4 5.1-4.3 5.3z" fill="#0069da"/>
|
||||
<path d="M47.01382 2.0312776c2.5-.2 4.9 1.8 5.1 4.3.2 2.4-1.8 4.7000004-4.2 4.9000004-2.6.2-4.9-1.7000004-5.1-4.2000004-.2-2.5 1.6-4.8 4.2-5z" fill="#ffcb12"/>
|
||||
<path d="M20.91382 3.9312776c.3 2.6-1.5 4.8-4.2 5.2-2.3.3-4.7-1.6-5-3.8-.3-2.9 1.3-4.99999996 4-5.29999996 2.5-.3 4.9 1.59999996 5.2 3.89999996z" fill="#ff4649"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['fedora'],
|
||||
info: {
|
||||
label: 'F',
|
||||
bg: '#294172',
|
||||
fg: '#fff',
|
||||
svg: (
|
||||
<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm4 11h-3v3a1 1 0 0 1-2 0v-3H8a1 1 0 0 1 0-2h3V8a1 1 0 0 1 2 0v3h3a1 1 0 0 1 0 2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['opensuse', 'suse', 'leap', 'tumbleweed'],
|
||||
info: {
|
||||
label: 'S',
|
||||
bg: '#73BA25',
|
||||
fg: '#fff',
|
||||
svg: (
|
||||
<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm3.5 14.5c-1 .6-2.2 1-3.5 1-3.9 0-7-3.1-7-7 0-1 .2-2 .6-2.8l1.7 1.7C7.1 10 7 10.5 7 11c0 2.8 2.2 5 5 5 .8 0 1.5-.2 2.1-.5l1.4 1zM18 16.2l-1.7-1.7c.2-.5.2-1 .2-1.5 0-2.8-2.2-5-5-5-.7 0-1.4.1-2 .4L8.1 7C9 6.4 10.5 6 12 6c3.9 0 7 3.1 7 7 0 1.2-.4 2.4-1 3.2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['arch', 'archlinux'],
|
||||
info: {
|
||||
label: 'A',
|
||||
bg: '#1793D1',
|
||||
fg: '#fff',
|
||||
svg: (
|
||||
<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L3 20h3.5l1.8-4.5h7.4L17.5 20H21L12 2zm0 5.3l2.8 6.7H9.2L12 7.3z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['kali'],
|
||||
info: {
|
||||
label: 'K',
|
||||
bg: '#268BEE',
|
||||
fg: '#fff',
|
||||
svg: (
|
||||
<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm-1 14H9V8h2v8zm4-4l-3-4h2.5l3 4-3 4H13l3-4z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['mint', 'linuxmint'],
|
||||
info: {
|
||||
label: 'M',
|
||||
bg: '#87CF3E',
|
||||
fg: '#fff',
|
||||
svg: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['freebsd', 'bsd'],
|
||||
info: {
|
||||
label: 'B',
|
||||
bg: '#AB2B28',
|
||||
fg: '#fff',
|
||||
svg: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
keys: ['rhel', 'redhat', 'red hat'],
|
||||
info: {
|
||||
label: 'R',
|
||||
bg: '#EE0000',
|
||||
fg: '#fff',
|
||||
svg: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function detectOs(name: string): OsInfo | null {
|
||||
const lower = name.toLowerCase();
|
||||
for (const { keys, info } of OS_MAP) {
|
||||
if (keys.some(k => lower.includes(k))) return info;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface OsLogoProps {
|
||||
name: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function OsLogo({ name, className = 'h-8 w-8' }: OsLogoProps) {
|
||||
const os = detectOs(name);
|
||||
|
||||
if (!os) {
|
||||
return <Monitor className={`${className} text-muted-foreground`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center justify-center rounded-md ${className}`}
|
||||
style={{ backgroundColor: os.bg, color: os.fg, flexShrink: 0 }}
|
||||
title={name}
|
||||
>
|
||||
{os.svg ? (
|
||||
<span className="h-5 w-5 flex items-center justify-center">
|
||||
{os.svg}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs font-bold">{os.label}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Monitor, Search, Download, Trash2, Loader2 } from 'lucide-react';
|
||||
import { OsLogo } from './OsLogo';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -99,7 +100,9 @@ export default function TemplatesPage() {
|
||||
<CardContent className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{tpl.group_icon ? (
|
||||
{tpl.vm_type === 'qemu' ? (
|
||||
<OsLogo name={tpl.name} className="h-7 w-7" />
|
||||
) : tpl.group_icon ? (
|
||||
<span className="text-xl" dangerouslySetInnerHTML={{ __html: tpl.group_icon }} />
|
||||
) : (
|
||||
<Monitor className="h-5 w-5 text-muted-foreground" />
|
||||
|
||||
Reference in New Issue
Block a user