diff --git a/frontend/src/app/skills/marketplace/page.tsx b/frontend/src/app/skills/marketplace/page.tsx index 934f078e..e2e8e31e 100644 --- a/frontend/src/app/skills/marketplace/page.tsx +++ b/frontend/src/app/skills/marketplace/page.tsx @@ -76,6 +76,9 @@ export default function SkillsMarketplacePage() { const [gatewayInstalledById, setGatewayInstalledById] = useState< Record >({}); + const [installedGatewayNamesBySkillId, setInstalledGatewayNamesBySkillId] = useState< + Record + >({}); const [isGatewayStatusLoading, setIsGatewayStatusLoading] = useState(false); const [gatewayStatusError, setGatewayStatusError] = useState(null); const [installingGatewayId, setInstallingGatewayId] = useState(null); @@ -156,6 +159,55 @@ export default function SkillsMarketplacePage() { }); }, [selectedPack, skills]); + useEffect(() => { + let cancelled = false; + + const loadInstalledGatewaysBySkill = async () => { + if (!isSignedIn || !isAdmin || gateways.length === 0 || skills.length === 0) { + setInstalledGatewayNamesBySkillId({}); + return; + } + + try { + const responses = await Promise.all( + gateways.map(async (gateway) => { + const response = await listMarketplaceSkillsApiV1SkillsMarketplaceGet({ + gateway_id: gateway.id, + }); + return { gatewayName: gateway.name, response }; + }), + ); + + if (cancelled) return; + + const nextInstalledGatewayNamesBySkillId: Record = {}; + for (const skill of skills) { + nextInstalledGatewayNamesBySkillId[skill.id] = []; + } + + for (const { gatewayName, response } of responses) { + if (response.status !== 200) continue; + for (const skill of response.data) { + if (!skill.installed) continue; + if (!nextInstalledGatewayNamesBySkillId[skill.id]) continue; + nextInstalledGatewayNamesBySkillId[skill.id].push(gatewayName); + } + } + + setInstalledGatewayNamesBySkillId(nextInstalledGatewayNamesBySkillId); + } catch { + if (cancelled) return; + setInstalledGatewayNamesBySkillId({}); + } + }; + + void loadInstalledGatewaysBySkill(); + + return () => { + cancelled = true; + }; + }, [gateways, isAdmin, isSignedIn, skills]); + const installMutation = useInstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdInstallPost( { @@ -168,6 +220,18 @@ export default function SkillsMarketplacePage() { ...previous, [variables.params.gateway_id]: true, })); + const gatewayName = + gateways.find((gateway) => gateway.id === variables.params.gateway_id)?.name; + if (gatewayName) { + setInstalledGatewayNamesBySkillId((previous) => { + const installedOn = previous[variables.skillId] ?? []; + if (installedOn.includes(gatewayName)) return previous; + return { + ...previous, + [variables.skillId]: [...installedOn, gatewayName], + }; + }); + } }, }, }, @@ -178,10 +242,25 @@ export default function SkillsMarketplacePage() { useUninstallMarketplaceSkillApiV1SkillsMarketplaceSkillIdUninstallPost( { mutation: { - onSuccess: async () => { + onSuccess: async (_, variables) => { await queryClient.invalidateQueries({ queryKey: ["/api/v1/skills/marketplace"], }); + setGatewayInstalledById((previous) => ({ + ...previous, + [variables.params.gateway_id]: false, + })); + const gatewayName = + gateways.find((gateway) => gateway.id === variables.params.gateway_id)?.name; + if (gatewayName) { + setInstalledGatewayNamesBySkillId((previous) => { + const installedOn = previous[variables.skillId] ?? []; + return { + ...previous, + [variables.skillId]: installedOn.filter((name) => name !== gatewayName), + }; + }); + } }, }, }, @@ -247,14 +326,24 @@ export default function SkillsMarketplacePage() { const isMutating = installMutation.isPending || uninstallMutation.isPending; - const handleInstallToGateway = async (gatewayId: string) => { + const handleGatewayInstallAction = async ( + gatewayId: string, + isInstalled: boolean, + ) => { if (!selectedSkill) return; setInstallingGatewayId(gatewayId); try { - await installMutation.mutateAsync({ - skillId: selectedSkill.id, - params: { gateway_id: gatewayId }, - }); + if (isInstalled) { + await uninstallMutation.mutateAsync({ + skillId: selectedSkill.id, + params: { gateway_id: gatewayId }, + }); + } else { + await installMutation.mutateAsync({ + skillId: selectedSkill.id, + params: { gateway_id: gatewayId }, + }); + } } finally { setInstallingGatewayId(null); } @@ -300,19 +389,13 @@ export default function SkillsMarketplacePage() {
- uninstallMutation.mutate({ - skillId: skill.id, - params: { gateway_id: resolvedGatewayId }, - }) - } emptyState={{ title: "No marketplace skills yet", description: "Add packs first, then synced skills will appear here.", @@ -359,8 +442,9 @@ export default function SkillsMarketplacePage() { ) : ( gateways.map((gateway) => { const isInstalled = gatewayInstalledById[gateway.id] === true; - const isInstalling = - installMutation.isPending && installingGatewayId === gateway.id; + const isUpdatingGateway = + installingGatewayId === gateway.id && + (installMutation.isPending || uninstallMutation.isPending); return (

{gateway.name}

-

- {isInstalled ? "Installed" : "Not installed"} -

); @@ -387,8 +477,8 @@ export default function SkillsMarketplacePage() { {gatewayStatusError ? (

{gatewayStatusError}

) : null} - {installMutation.error ? ( -

{installMutation.error.message}

+ {mutationError ? ( +

{mutationError}

) : null}
diff --git a/frontend/src/components/skills/MarketplaceSkillsTable.tsx b/frontend/src/components/skills/MarketplaceSkillsTable.tsx index 30b49261..65259b8e 100644 --- a/frontend/src/components/skills/MarketplaceSkillsTable.tsx +++ b/frontend/src/components/skills/MarketplaceSkillsTable.tsx @@ -19,15 +19,14 @@ import { truncateText as truncate } from "@/lib/formatters"; type MarketplaceSkillsTableProps = { skills: MarketplaceSkillCardRead[]; + installedGatewayNamesBySkillId?: Record; isLoading?: boolean; sorting?: SortingState; onSortingChange?: OnChangeFn; stickyHeader?: boolean; disableSorting?: boolean; - canInstallActions: boolean; isMutating?: boolean; onSkillClick?: (skill: MarketplaceSkillCardRead) => void; - onUninstall: (skill: MarketplaceSkillCardRead) => void; onDelete?: (skill: MarketplaceSkillCardRead) => void; getEditHref?: (skill: MarketplaceSkillCardRead) => string; emptyState?: Omit & { @@ -88,15 +87,14 @@ const toPackDetailHref = (packUrl: string): string => { export function MarketplaceSkillsTable({ skills, + installedGatewayNamesBySkillId, isLoading = false, sorting, onSortingChange, stickyHeader = false, disableSorting = false, - canInstallActions, isMutating = false, onSkillClick, - onUninstall, onDelete, getEditHref, emptyState, @@ -129,7 +127,10 @@ export function MarketplaceSkillsTable({ ) : (

{row.original.name}

)} -

+

{row.original.description || "No description provided."}

@@ -171,11 +172,37 @@ export function MarketplaceSkillsTable({ { accessorKey: "source", header: "Source", - cell: ({ row }) => ( - - {truncate(row.original.source || "unknown", 36)} - - ), + cell: ({ row }) => { + const sourceHref = row.original.source || row.original.source_url; + return ( + + {truncate(sourceHref, 36)} + + ); + }, + }, + { + id: "installed_on", + header: "Installed On", + enableSorting: false, + cell: ({ row }) => { + const installedOn = installedGatewayNamesBySkillId?.[row.original.id] ?? []; + if (installedOn.length === 0) { + return -; + } + const installedOnText = installedOn.join(", "); + return ( + + {installedOnText} + + ); + }, }, { accessorKey: "updated_at", @@ -188,17 +215,6 @@ export function MarketplaceSkillsTable({ enableSorting: false, cell: ({ row }) => (
- {row.original.installed ? ( - - ) : null} {getEditHref ? (