feat(instances): disable Create Instance when the quota is used up
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
The wizard already refuses to open once a limit has no headroom left, but the button leading to it still looked available, so the only way to find out was to click it. Reuse exhaustedMetrics() from the wizard's quota module, so the button and the wizard cannot disagree about what counts as exhausted, and skip the check for admins for the same reason the wizard does — their own quota says nothing about an instance they create for someone else. The button is rendered as a Link, and disabled does nothing to an <a>: it would still navigate. So the exhausted case renders a plain button with no Link, and a tooltip naming the limits that are full. The trigger wraps it in a span because a disabled button receives no pointer events of its own. The wizard keeps its own block, which is what still covers navigating straight to /instances/create.
This commit is contained in:
@@ -83,7 +83,8 @@ import { StatusDot } from '@/components/shared/status-dot';
|
|||||||
import { ColumnFilter, multiSelectFilter } from '@/components/shared/column-filter';
|
import { ColumnFilter, multiSelectFilter } from '@/components/shared/column-filter';
|
||||||
import { useVirtualMachines, useBulkOperation, usePowerAction, useVMStatusSync, useInstancesMetricsSync, useBulkTasksSync, vmKeys } from '@/hooks/use-instances';
|
import { useVirtualMachines, useBulkOperation, usePowerAction, useVMStatusSync, useInstancesMetricsSync, useBulkTasksSync, vmKeys } from '@/hooks/use-instances';
|
||||||
import { useServers } from '@/hooks/use-nodes';
|
import { useServers } from '@/hooks/use-nodes';
|
||||||
import { useProfile } from '@/hooks/use-settings';
|
import { useProfile, useMyQuota } from '@/hooks/use-settings';
|
||||||
|
import { buildQuotaMetrics, exhaustedMetrics } from './quota';
|
||||||
import { formatBytes, vmTypeLabel, formatUptime, formatVmConfig } from '@/lib/format';
|
import { formatBytes, vmTypeLabel, formatUptime, formatVmConfig } from '@/lib/format';
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
import { useDeployTasksStore } from '@/stores/deploy-tasks-store';
|
import { useDeployTasksStore } from '@/stores/deploy-tasks-store';
|
||||||
@@ -339,6 +340,16 @@ export default function InstancesPage() {
|
|||||||
const { data: servers = [] } = useServers();
|
const { data: servers = [] } = useServers();
|
||||||
const { data: profile } = useProfile();
|
const { data: profile } = useProfile();
|
||||||
|
|
||||||
|
// Квота исчерпана — мастер создания всё равно откажется открываться, так что
|
||||||
|
// не ведём туда пользователя вхолостую. Условие то же, что в мастере:
|
||||||
|
// размер будущего инстанса не важен (сравниваем использовано/лимит), а на
|
||||||
|
// админов не распространяется — их лимит не про инстанс, который они создают
|
||||||
|
// другому пользователю.
|
||||||
|
const { data: myQuota } = useMyQuota();
|
||||||
|
const quotaBlockers = profile?.is_admin
|
||||||
|
? []
|
||||||
|
: exhaustedMetrics(buildQuotaMetrics(myQuota, { cores: 0, memoryMb: 0, diskGb: 0 }));
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
const [serverFilter, setServerFilter] = useState<string>(() => {
|
const [serverFilter, setServerFilter] = useState<string>(() => {
|
||||||
@@ -1070,10 +1081,34 @@ export default function InstancesPage() {
|
|||||||
{t('snap_archive.title')}
|
{t('snap_archive.title')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button render={<Link to="/instances/create" />} size="sm">
|
{quotaBlockers.length > 0 ? (
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
// Кнопка-ссылка с disabled всё равно навигирует (<a> не знает про
|
||||||
{t('instances.create', 'Create Instance')}
|
// disabled), поэтому при исчерпанной квоте рендерим обычную кнопку
|
||||||
</Button>
|
// без Link, а причину показываем в подсказке.
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger render={<span />}>
|
||||||
|
<Button size="sm" disabled>
|
||||||
|
<Plus className="mr-1.5 h-4 w-4" />
|
||||||
|
{t('instances.create', 'Create Instance')}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('wizard.quota_blocked_title')}</p>
|
||||||
|
<ul className="mt-1">
|
||||||
|
{quotaBlockers.map(m => (
|
||||||
|
<li key={m.key}>
|
||||||
|
{t(m.labelKey)}: {t('wizard.quota_used_of', { used: m.used, limit: m.limit })}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Button render={<Link to="/instances/create" />} size="sm">
|
||||||
|
<Plus className="mr-1.5 h-4 w-4" />
|
||||||
|
{t('instances.create', 'Create Instance')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user