Skip to content

Commit f0b2c10

Browse files
authored
fix(web): disable quota-reached create actions (#2389)
* fix(web): disable quota-reached create actions * fix(web): close quota review gaps --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
1 parent 3101c9b commit f0b2c10

21 files changed

Lines changed: 1037 additions & 293 deletions

File tree

web/src/app/home/add-extension/page.tsx

Lines changed: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import type {
4949
} from '@/app/home/mcp/components/mcp-form/MCPForm';
5050
import SkillZipPreviewPanel from '@/app/home/skills/components/SkillZipPreviewPanel';
5151
import PluginLocalPreviewPanel from '@/app/home/plugins/components/PluginLocalPreviewPanel';
52+
import { useWorkspaceQuotaStatus } from '@/app/home/components/workspace-quota/useWorkspaceQuotaStatus';
53+
import { WorkspaceQuotaTooltip } from '@/app/home/components/workspace-quota/WorkspaceQuotaTooltip';
5254

5355
type PopoverView = 'menu' | 'mcp' | 'github';
5456

@@ -154,6 +156,12 @@ function AddExtensionContent() {
154156
const navigate = useNavigate();
155157
const [searchParams, setSearchParams] = useSearchParams();
156158
const { refreshPlugins, refreshMCPServers, refreshSkills } = useSidebarData();
159+
const { extensions: extensionQuota, extensionsReached } =
160+
useWorkspaceQuotaStatus();
161+
const extensionQuotaTooltip = t('limitation.createDisabledTooltip', {
162+
resource: t('sidebar.extensions'),
163+
max: extensionQuota.max,
164+
});
157165

158166
// Localized label for an extension type, used in the install dialog.
159167
const extensionTypeLabel = (type: string) =>
@@ -344,23 +352,28 @@ function AddExtensionContent() {
344352
t,
345353
]);
346354

347-
const handleInstallPlugin = useCallback(async (plugin: PluginV4) => {
348-
setInstallInfo({
349-
plugin_author: plugin.author,
350-
plugin_name: plugin.name,
351-
plugin_version: plugin.latest_version,
352-
plugin_label: extractI18nObject(plugin.label) || plugin.name,
353-
plugin_description: extractI18nObject(plugin.description) || '',
354-
plugin_icon: plugin.icon || '',
355-
});
356-
setInstallExtensionType(plugin.type || 'plugin');
357-
setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM);
358-
setInstallError(null);
359-
setInstallIconFailed(false);
360-
setModalOpen(true);
361-
}, []);
355+
const handleInstallPlugin = useCallback(
356+
async (plugin: PluginV4) => {
357+
if (extensionsReached) return;
358+
setInstallInfo({
359+
plugin_author: plugin.author,
360+
plugin_name: plugin.name,
361+
plugin_version: plugin.latest_version,
362+
plugin_label: extractI18nObject(plugin.label) || plugin.name,
363+
plugin_description: extractI18nObject(plugin.description) || '',
364+
plugin_icon: plugin.icon || '',
365+
});
366+
setInstallExtensionType(plugin.type || 'plugin');
367+
setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM);
368+
setInstallError(null);
369+
setInstallIconFailed(false);
370+
setModalOpen(true);
371+
},
372+
[extensionsReached],
373+
);
362374

363375
function handleModalConfirm() {
376+
if (extensionsReached) return;
364377
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
365378
const pluginDisplayName = `${installInfo.plugin_author}/${installInfo.plugin_name}`;
366379
httpClient
@@ -402,6 +415,7 @@ function AddExtensionContent() {
402415

403416
const uploadFile = useCallback(
404417
async (file: File) => {
418+
if (extensionsReached) return;
405419
if (!validateFileType(file)) {
406420
toast.error(t('addExtension.unsupportedFileType'));
407421
return;
@@ -421,14 +435,15 @@ function AddExtensionContent() {
421435
setSkillUploadPreviewOpen(true);
422436
}
423437
},
424-
[t, setSelectedTaskId],
438+
[extensionsReached, t, setSelectedTaskId],
425439
);
426440

427441
const handleFileSelect = useCallback(() => {
442+
if (extensionsReached) return;
428443
if (fileInputRef.current) {
429444
fileInputRef.current.click();
430445
}
431-
}, []);
446+
}, [extensionsReached]);
432447

433448
const handleFileChange = useCallback(
434449
(event: React.ChangeEvent<HTMLInputElement>) => {
@@ -455,12 +470,13 @@ function AddExtensionContent() {
455470
(event: React.DragEvent) => {
456471
event.preventDefault();
457472
setIsDragOver(false);
473+
if (extensionsReached) return;
458474
const files = Array.from(event.dataTransfer.files);
459475
if (files.length > 0) {
460476
uploadFile(files[0]);
461477
}
462478
},
463-
[uploadFile],
479+
[extensionsReached, uploadFile],
464480
);
465481

466482
function handleMCPCreated(_serverName: string) {
@@ -490,7 +506,8 @@ function AddExtensionContent() {
490506
return false;
491507
}
492508
} catch {
493-
// If we can't check, let backend handle it
509+
toast.error(t('limitation.quotaCheckFailed'));
510+
return false;
494511
}
495512
return true;
496513
}
@@ -630,9 +647,11 @@ function AddExtensionContent() {
630647

631648
async function handleGithubConfirm() {
632649
if (!selectedAsset || !selectedRelease) return;
633-
if (!(await checkExtensionsLimit())) return;
634-
635650
setGithubInstallStatus(GithubInstallStatus.INSTALLING);
651+
if (!(await checkExtensionsLimit())) {
652+
setGithubInstallStatus(GithubInstallStatus.ASK_CONFIRM);
653+
return;
654+
}
636655
const pluginDisplayName = `${githubOwner}/${githubRepo}`;
637656
httpClient
638657
.installPluginFromGithub(
@@ -664,9 +683,11 @@ function AddExtensionContent() {
664683

665684
async function handleGithubSkillConfirm() {
666685
if (!githubSkillInfo) return;
667-
if (!(await checkExtensionsLimit())) return;
668-
669686
setGithubInstallStatus(GithubInstallStatus.SKILL_INSTALLING);
687+
if (!(await checkExtensionsLimit())) {
688+
setGithubInstallStatus(GithubInstallStatus.SKILL_PREVIEW);
689+
return;
690+
}
670691
try {
671692
await httpClient.installSkillFromGithub(
672693
githubURL.trim(),
@@ -726,17 +747,24 @@ function AddExtensionContent() {
726747
setPopoverOpen(open);
727748
}}
728749
>
729-
<PopoverTrigger asChild>
730-
<Button
731-
variant="default"
732-
className="px-3 sm:px-4 py-2 cursor-pointer flex-shrink-0"
733-
>
734-
<PlusIcon className="w-4 h-4" />
735-
<span className="whitespace-nowrap">
736-
{t('addExtension.manualAdd')}
737-
</span>
738-
</Button>
739-
</PopoverTrigger>
750+
<WorkspaceQuotaTooltip
751+
quota={extensionQuota}
752+
resource={t('sidebar.extensions')}
753+
>
754+
<PopoverTrigger asChild>
755+
<Button
756+
variant="default"
757+
disabled={extensionsReached}
758+
aria-disabled={extensionsReached}
759+
className="px-3 sm:px-4 py-2 cursor-pointer flex-shrink-0 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-100"
760+
>
761+
<PlusIcon className="w-4 h-4" />
762+
<span className="whitespace-nowrap">
763+
{t('addExtension.manualAdd')}
764+
</span>
765+
</Button>
766+
</PopoverTrigger>
767+
</WorkspaceQuotaTooltip>
740768
<PopoverContent
741769
forceMount
742770
className={`${getPopoverWidth()} max-h-[min(720px,80vh)] overflow-hidden p-0`}
@@ -745,9 +773,19 @@ function AddExtensionContent() {
745773
{/* ===== Menu View ===== */}
746774
{popoverView === 'menu' && (
747775
<div className="space-y-4 p-4">
776+
{extensionsReached && (
777+
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-800 dark:text-amber-200">
778+
{extensionQuotaTooltip}
779+
</div>
780+
)}
748781
{/* File upload area */}
749782
<div
750-
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
783+
aria-disabled={extensionsReached}
784+
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
785+
extensionsReached
786+
? 'cursor-not-allowed opacity-50'
787+
: 'cursor-pointer'
788+
} ${
751789
isDragOver
752790
? 'border-primary bg-primary/5'
753791
: 'border-muted-foreground/25 hover:border-primary/50'
@@ -777,7 +815,8 @@ function AddExtensionContent() {
777815
<div className="space-y-2">
778816
<button
779817
type="button"
780-
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
818+
disabled={extensionsReached}
819+
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
781820
onClick={() => setPopoverView('mcp')}
782821
>
783822
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground">
@@ -796,7 +835,8 @@ function AddExtensionContent() {
796835

797836
<button
798837
type="button"
799-
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
838+
disabled={extensionsReached}
839+
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
800840
onClick={() => setPopoverView('github')}
801841
>
802842
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground">
@@ -815,7 +855,8 @@ function AddExtensionContent() {
815855

816856
<button
817857
type="button"
818-
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
858+
disabled={extensionsReached}
859+
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
819860
onClick={async () => {
820861
if (!(await checkExtensionsLimit())) return;
821862
setPopoverOpen(false);
@@ -882,6 +923,7 @@ function AddExtensionContent() {
882923
type="submit"
883924
form="mcp-form"
884925
size="sm"
926+
disabled={extensionsReached}
885927
onClick={async (e) => {
886928
if (!(await checkExtensionsLimit())) {
887929
e.preventDefault();
@@ -946,6 +988,7 @@ function AddExtensionContent() {
946988
className="w-full"
947989
onClick={handleGithubAddressSubmit}
948990
disabled={
991+
extensionsReached ||
949992
!githubURL.trim() ||
950993
fetchingReleases ||
951994
fetchingSkillPreview
@@ -1102,7 +1145,11 @@ function AddExtensionContent() {
11021145
</div>
11031146
</div>
11041147
)}
1105-
<Button className="w-full" onClick={handleGithubConfirm}>
1148+
<Button
1149+
className="w-full"
1150+
onClick={handleGithubConfirm}
1151+
disabled={extensionsReached}
1152+
>
11061153
{t('common.confirm')}
11071154
</Button>
11081155
</div>
@@ -1184,6 +1231,7 @@ function AddExtensionContent() {
11841231
<Button
11851232
className="w-full"
11861233
onClick={handleGithubSkillConfirm}
1234+
disabled={extensionsReached}
11871235
>
11881236
{t('common.confirm')}
11891237
</Button>
@@ -1240,6 +1288,8 @@ function AddExtensionContent() {
12401288
<MarketPage
12411289
installPlugin={handleInstallPlugin}
12421290
headerActions={extensionActions}
1291+
installDisabled={extensionsReached}
1292+
installDisabledTooltip={extensionQuotaTooltip}
12431293
/>
12441294
</div>
12451295
</div>
@@ -1325,9 +1375,17 @@ function AddExtensionContent() {
13251375
<Button variant="outline" onClick={() => setModalOpen(false)}>
13261376
{t('common.cancel')}
13271377
</Button>
1328-
<Button onClick={handleModalConfirm}>
1329-
{t('common.confirm')}
1330-
</Button>
1378+
<WorkspaceQuotaTooltip
1379+
quota={extensionQuota}
1380+
resource={t('sidebar.extensions')}
1381+
>
1382+
<Button
1383+
onClick={handleModalConfirm}
1384+
disabled={extensionsReached}
1385+
>
1386+
{t('common.confirm')}
1387+
</Button>
1388+
</WorkspaceQuotaTooltip>
13311389
</>
13321390
)}
13331391
{pluginInstallStatus === PluginInstallStatus.ERROR && (
@@ -1359,6 +1417,8 @@ function AddExtensionContent() {
13591417
{pluginUploadPreviewFile && (
13601418
<PluginLocalPreviewPanel
13611419
file={pluginUploadPreviewFile}
1420+
quota={extensionQuota}
1421+
quotaResource={t('sidebar.extensions')}
13621422
onCancel={() => {
13631423
setPluginUploadPreviewOpen(false);
13641424
setPluginUploadPreviewFile(null);
@@ -1392,6 +1452,8 @@ function AddExtensionContent() {
13921452
{skillUploadPreviewFile && (
13931453
<SkillZipPreviewPanel
13941454
file={skillUploadPreviewFile}
1455+
quota={extensionQuota}
1456+
quotaResource={t('sidebar.extensions')}
13951457
onCancel={() => {
13961458
setSkillUploadPreviewOpen(false);
13971459
setSkillUploadPreviewFile(null);

0 commit comments

Comments
 (0)