refactor(skills): reorganize imports and improve code formatting

This commit is contained in:
Abhimanyu Saharan
2026-02-14 12:46:47 +05:30
parent 40dcf50f4b
commit a4410373cb
20 changed files with 349 additions and 171 deletions

View File

@@ -50,16 +50,20 @@ export default function SkillsMarketplacePage() {
const searchParams = useSearchParams();
const { isSignedIn } = useAuth();
const { isAdmin } = useOrganizationMembership(isSignedIn);
const [selectedSkill, setSelectedSkill] = useState<MarketplaceSkillCardRead | null>(null);
const [selectedSkill, setSelectedSkill] =
useState<MarketplaceSkillCardRead | null>(null);
const [gatewayInstalledById, setGatewayInstalledById] = useState<
Record<string, boolean>
>({});
const [installedGatewayNamesBySkillId, setInstalledGatewayNamesBySkillId] = useState<
Record<string, string[]>
>({});
const [installedGatewayNamesBySkillId, setInstalledGatewayNamesBySkillId] =
useState<Record<string, { id: string; name: string }[]>>({});
const [isGatewayStatusLoading, setIsGatewayStatusLoading] = useState(false);
const [gatewayStatusError, setGatewayStatusError] = useState<string | null>(null);
const [installingGatewayId, setInstallingGatewayId] = useState<string | null>(null);
const [gatewayStatusError, setGatewayStatusError] = useState<string | null>(
null,
);
const [installingGatewayId, setInstallingGatewayId] = useState<string | null>(
null,
);
const { sorting, onSortingChange } = useUrlSorting({
allowedColumnIds: MARKETPLACE_SKILLS_SORTABLE_COLUMNS,
@@ -161,25 +165,29 @@ export default function SkillsMarketplacePage() {
const updateInstalledGatewayNames = useCallback(
({
skillId,
gatewayId,
gatewayName,
installed,
}: {
skillId: string;
gatewayId: string;
gatewayName: string;
installed: boolean;
}) => {
setInstalledGatewayNamesBySkillId((previous) => {
const installedOn = previous[skillId] ?? [];
if (installed) {
if (installedOn.includes(gatewayName)) return previous;
if (installedOn.some((gateway) => gateway.id === gatewayId)) {
return previous;
}
return {
...previous,
[skillId]: [...installedOn, gatewayName],
[skillId]: [...installedOn, { id: gatewayId, name: gatewayName }],
};
}
return {
...previous,
[skillId]: installedOn.filter((name) => name !== gatewayName),
[skillId]: installedOn.filter((gateway) => gateway.id !== gatewayId),
};
});
},
@@ -190,7 +198,12 @@ export default function SkillsMarketplacePage() {
let cancelled = false;
const loadInstalledGatewaysBySkill = async () => {
if (!isSignedIn || !isAdmin || gateways.length === 0 || skills.length === 0) {
if (
!isSignedIn ||
!isAdmin ||
gateways.length === 0 ||
skills.length === 0
) {
setInstalledGatewayNamesBySkillId({});
return;
}
@@ -198,9 +211,10 @@ export default function SkillsMarketplacePage() {
try {
const gatewaySkills = await Promise.all(
gateways.map(async (gateway) => {
const response = await listMarketplaceSkillsApiV1SkillsMarketplaceGet({
gateway_id: gateway.id,
});
const response =
await listMarketplaceSkillsApiV1SkillsMarketplaceGet({
gateway_id: gateway.id,
});
return {
gatewayId: gateway.id,
gatewayName: gateway.name,
@@ -211,16 +225,26 @@ export default function SkillsMarketplacePage() {
if (cancelled) return;
const nextInstalledGatewayNamesBySkillId: Record<string, string[]> = {};
const nextInstalledGatewayNamesBySkillId: Record<
string,
{ id: string; name: string }[]
> = {};
for (const skill of skills) {
nextInstalledGatewayNamesBySkillId[skill.id] = [];
}
for (const { gatewayName, skills: gatewaySkillRows } of gatewaySkills) {
for (const {
gatewayId,
gatewayName,
skills: gatewaySkillRows,
} of gatewaySkills) {
for (const skill of gatewaySkillRows) {
if (!skill.installed) continue;
if (!nextInstalledGatewayNamesBySkillId[skill.id]) continue;
nextInstalledGatewayNamesBySkillId[skill.id].push(gatewayName);
nextInstalledGatewayNamesBySkillId[skill.id].push({
id: gatewayId,
name: gatewayName,
});
}
}
@@ -250,11 +274,13 @@ export default function SkillsMarketplacePage() {
...previous,
[variables.params.gateway_id]: true,
}));
const gatewayName =
gateways.find((gateway) => gateway.id === variables.params.gateway_id)?.name;
const gatewayName = gateways.find(
(gateway) => gateway.id === variables.params.gateway_id,
)?.name;
if (gatewayName) {
updateInstalledGatewayNames({
skillId: variables.skillId,
gatewayId: variables.params.gateway_id,
gatewayName,
installed: true,
});
@@ -277,11 +303,13 @@ export default function SkillsMarketplacePage() {
...previous,
[variables.params.gateway_id]: false,
}));
const gatewayName =
gateways.find((gateway) => gateway.id === variables.params.gateway_id)?.name;
const gatewayName = gateways.find(
(gateway) => gateway.id === variables.params.gateway_id,
)?.name;
if (gatewayName) {
updateInstalledGatewayNames({
skillId: variables.skillId,
gatewayId: variables.params.gateway_id,
gatewayName,
installed: false,
});
@@ -314,16 +342,22 @@ export default function SkillsMarketplacePage() {
setGatewayStatusError(null);
try {
const gatewaySkills = await loadSkillsByGateway();
const entries = gatewaySkills.map(({ gatewayId, skills: gatewaySkillRows }) => {
const row = gatewaySkillRows.find((skill) => skill.id === selectedSkill.id);
return [gatewayId, Boolean(row?.installed)] as const;
});
const entries = gatewaySkills.map(
({ gatewayId, skills: gatewaySkillRows }) => {
const row = gatewaySkillRows.find(
(skill) => skill.id === selectedSkill.id,
);
return [gatewayId, Boolean(row?.installed)] as const;
},
);
if (cancelled) return;
setGatewayInstalledById(Object.fromEntries(entries));
} catch (error) {
if (cancelled) return;
setGatewayStatusError(
error instanceof Error ? error.message : "Unable to load gateway status.",
error instanceof Error
? error.message
: "Unable to load gateway status.",
);
} finally {
if (!cancelled) {
@@ -391,7 +425,9 @@ export default function SkillsMarketplacePage() {
<div className="space-y-6">
{gateways.length === 0 ? (
<div className="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-600 shadow-sm">
<p className="font-medium text-slate-900">No gateways available yet.</p>
<p className="font-medium text-slate-900">
No gateways available yet.
</p>
<p className="mt-2">
Create a gateway first, then return here to manage installs.
</p>
@@ -407,7 +443,9 @@ export default function SkillsMarketplacePage() {
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<MarketplaceSkillsTable
skills={visibleSkills}
installedGatewayNamesBySkillId={installedGatewayNamesBySkillId}
installedGatewayNamesBySkillId={
installedGatewayNamesBySkillId
}
isLoading={skillsQuery.isLoading}
sorting={sorting}
onSortingChange={onSortingChange}
@@ -416,7 +454,8 @@ export default function SkillsMarketplacePage() {
onSkillClick={setSelectedSkill}
emptyState={{
title: "No marketplace skills yet",
description: "Add packs first, then synced skills will appear here.",
description:
"Add packs first, then synced skills will appear here.",
actionHref: "/skills/packs/new",
actionLabel: "Add your first pack",
}}
@@ -431,7 +470,9 @@ export default function SkillsMarketplacePage() {
{packsQuery.error ? (
<p className="text-sm text-rose-600">{packsQuery.error.message}</p>
) : null}
{mutationError ? <p className="text-sm text-rose-600">{mutationError}</p> : null}
{mutationError ? (
<p className="text-sm text-rose-600">{mutationError}</p>
) : null}
</div>
</DashboardPageLayout>

View File

@@ -36,11 +36,10 @@ export default function EditSkillPackPage() {
},
});
const pack = (
packQuery.data?.status === 200 ? packQuery.data.data : null
);
const pack = packQuery.data?.status === 200 ? packQuery.data.data : null;
const saveMutation = useUpdateSkillPackApiV1SkillsPacksPackIdPatch<ApiError>();
const saveMutation =
useUpdateSkillPackApiV1SkillsPacksPackIdPatch<ApiError>();
return (
<DashboardPageLayout

View File

@@ -80,19 +80,18 @@ export default function SkillsPacksPage() {
},
queryClient,
);
const syncMutation =
useSyncSkillPackApiV1SkillsPacksPackIdSyncPost<ApiError>(
{
mutation: {
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: packsQueryKey,
});
},
const syncMutation = useSyncSkillPackApiV1SkillsPacksPackIdSyncPost<ApiError>(
{
mutation: {
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: packsQueryKey,
});
},
},
queryClient,
);
},
queryClient,
);
const handleDelete = () => {
if (!deleteTarget) return;
@@ -113,7 +112,9 @@ export default function SkillsPacksPage() {
const response = await syncMutation.mutateAsync({
packId: pack.id,
});
setSyncWarnings(response.data.warnings ?? []);
if (response.status === 200) {
setSyncWarnings(response.data.warnings ?? []);
}
} finally {
setSyncingPackIds((previous) => {
const next = new Set(previous);
@@ -124,7 +125,12 @@ export default function SkillsPacksPage() {
};
const handleSyncAllPacks = async () => {
if (!isAdmin || isSyncingAll || syncingPackIds.size > 0 || packs.length === 0) {
if (
!isAdmin ||
isSyncingAll ||
syncingPackIds.size > 0 ||
packs.length === 0
) {
return;
}
@@ -145,10 +151,12 @@ export default function SkillsPacksPage() {
try {
const response = await syncMutation.mutateAsync({ packId: pack.id });
setSyncWarnings((previous) => [
...previous,
...(response.data.warnings ?? []),
]);
if (response.status === 200) {
setSyncWarnings((previous) => [
...previous,
...(response.data.warnings ?? []),
]);
}
} catch {
hasFailure = true;
} finally {
@@ -190,9 +198,7 @@ export default function SkillsPacksPage() {
size: "md",
})}
disabled={
isSyncingAll ||
syncingPackIds.size > 0 ||
packs.length === 0
isSyncingAll || syncingPackIds.size > 0 || packs.length === 0
}
onClick={() => {
void handleSyncAllPacks();
@@ -241,10 +247,14 @@ export default function SkillsPacksPage() {
<p className="text-sm text-rose-600">{packsQuery.error.message}</p>
) : null}
{deleteMutation.error ? (
<p className="text-sm text-rose-600">{deleteMutation.error.message}</p>
<p className="text-sm text-rose-600">
{deleteMutation.error.message}
</p>
) : null}
{syncMutation.error ? (
<p className="text-sm text-rose-600">{syncMutation.error.message}</p>
<p className="text-sm text-rose-600">
{syncMutation.error.message}
</p>
) : null}
{syncAllError ? (
<p className="text-sm text-rose-600">{syncAllError}</p>

View File

@@ -177,7 +177,8 @@ export function DashboardSidebar() {
href="/skills/marketplace"
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname === "/skills" || pathname.startsWith("/skills/marketplace")
pathname === "/skills" ||
pathname.startsWith("/skills/marketplace")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}

View File

@@ -11,9 +11,13 @@ import {
} from "@tanstack/react-table";
import type { MarketplaceSkillCardRead } from "@/api/generated/model";
import { DataTable, type DataTableEmptyState } from "@/components/tables/DataTable";
import {
DataTable,
type DataTableEmptyState,
} from "@/components/tables/DataTable";
import { dateCell } from "@/components/tables/cell-formatters";
import { Button, buttonVariants } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
SKILLS_TABLE_EMPTY_ICON,
useTableSortingState,
@@ -25,9 +29,32 @@ import {
packsHrefFromPackUrl,
} from "@/lib/skills-source";
function riskBadgeVariant(risk: string | null | undefined) {
const normalizedRisk = (risk || "unknown").trim().toLowerCase();
switch (normalizedRisk) {
case "low":
return "success";
case "medium":
case "moderate":
return "warning";
case "high":
case "critical":
return "danger";
case "unknown":
return "outline";
default:
return "accent";
}
}
function riskBadgeLabel(risk: string | null | undefined) {
return (risk || "unknown").trim() || "unknown";
}
type MarketplaceSkillsTableProps = {
skills: MarketplaceSkillCardRead[];
installedGatewayNamesBySkillId?: Record<string, string[]>;
installedGatewayNamesBySkillId?: Record<string, { id: string; name: string }[]>;
isLoading?: boolean;
sorting?: SortingState;
onSortingChange?: OnChangeFn<SortingState>;
@@ -78,7 +105,9 @@ export function MarketplaceSkillsTable({
{row.original.name}
</button>
) : (
<p className="text-sm font-medium text-slate-900">{row.original.name}</p>
<p className="text-sm font-medium text-slate-900">
{row.original.name}
</p>
)}
<p
className="mt-1 line-clamp-2 text-xs text-slate-500"
@@ -117,9 +146,12 @@ export function MarketplaceSkillsTable({
accessorKey: "risk",
header: "Risk",
cell: ({ row }) => (
<span className="text-sm text-slate-700">
{row.original.risk || "unknown"}
</span>
<Badge
variant={riskBadgeVariant(row.original.risk)}
className="px-2 py-0.5"
>
{riskBadgeLabel(row.original.risk)}
</Badge>
),
},
{
@@ -127,6 +159,11 @@ export function MarketplaceSkillsTable({
header: "Source",
cell: ({ row }) => {
const sourceHref = row.original.source || row.original.source_url;
if (!sourceHref) {
return <span className="text-sm text-slate-400">No source</span>;
}
return (
<Link
href={sourceHref}
@@ -145,15 +182,32 @@ export function MarketplaceSkillsTable({
header: "Installed On",
enableSorting: false,
cell: ({ row }) => {
const installedOn = installedGatewayNamesBySkillId?.[row.original.id] ?? [];
const installedOn =
installedGatewayNamesBySkillId?.[row.original.id] ?? [];
if (installedOn.length === 0) {
return <span className="text-sm text-slate-500">-</span>;
}
const installedOnText = installedOn.join(", ");
return (
<span className="text-sm text-slate-700" title={installedOnText}>
{installedOnText}
</span>
<div className="flex flex-wrap gap-1">
{installedOn.map((gateway, index) => {
const isLast = index === installedOn.length - 1;
return (
<span
key={`${gateway.id}-${index}`}
className="inline-flex items-center gap-1 text-sm text-slate-700"
title={gateway.name}
>
<Link
href={`/gateways/${gateway.id}`}
className="text-blue-700 hover:text-blue-600 hover:underline"
>
{gateway.name}
</Link>
{!isLast ? "," : ""}
</span>
);
})}
</div>
);
},
},

View File

@@ -48,7 +48,9 @@ export function SkillInstallDialog({
className="max-w-xl p-6 sm:p-7"
>
<DialogHeader className="pb-1">
<DialogTitle>{selectedSkill ? selectedSkill.name : "Install skill"}</DialogTitle>
<DialogTitle>
{selectedSkill ? selectedSkill.name : "Install skill"}
</DialogTitle>
<DialogDescription>
Choose one or more gateways where this skill should be installed.
</DialogDescription>
@@ -60,14 +62,17 @@ export function SkillInstallDialog({
) : (
gateways.map((gateway) => {
const isInstalled = gatewayInstalledById[gateway.id] === true;
const isUpdatingGateway = installingGatewayId === gateway.id && isMutating;
const isUpdatingGateway =
installingGatewayId === gateway.id && isMutating;
return (
<div
key={gateway.id}
className="flex items-center justify-between rounded-xl border border-slate-200 bg-white p-4"
>
<div>
<p className="text-sm font-medium text-slate-900">{gateway.name}</p>
<p className="text-sm font-medium text-slate-900">
{gateway.name}
</p>
</div>
<Button
type="button"
@@ -91,11 +96,17 @@ export function SkillInstallDialog({
{gatewayStatusError ? (
<p className="text-sm text-rose-600">{gatewayStatusError}</p>
) : null}
{mutationError ? <p className="text-sm text-rose-600">{mutationError}</p> : null}
{mutationError ? (
<p className="text-sm text-rose-600">{mutationError}</p>
) : null}
</div>
<DialogFooter className="mt-6 border-t border-slate-200 pt-4">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isMutating}>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isMutating}
>
Close
</Button>
</DialogFooter>

View File

@@ -11,7 +11,10 @@ import {
} from "@tanstack/react-table";
import type { SkillPackRead } from "@/api/generated/model";
import { DataTable, type DataTableEmptyState } from "@/components/tables/DataTable";
import {
DataTable,
type DataTableEmptyState,
} from "@/components/tables/DataTable";
import { dateCell } from "@/components/tables/cell-formatters";
import { Button } from "@/components/ui/button";
import {
@@ -62,7 +65,9 @@ export function SkillPacksTable({
header: "Pack",
cell: ({ row }) => (
<div>
<p className="text-sm font-medium text-slate-900">{row.original.name}</p>
<p className="text-sm font-medium text-slate-900">
{row.original.name}
</p>
<p className="mt-1 line-clamp-2 text-xs text-slate-500">
{row.original.description || "No description provided."}
</p>
@@ -86,7 +91,11 @@ export function SkillPacksTable({
{
accessorKey: "branch",
header: "Branch",
cell: ({ row }) => <p className="text-sm text-slate-900">{row.original.branch || "main"}</p>,
cell: ({ row }) => (
<p className="text-sm text-slate-900">
{row.original.branch || "main"}
</p>
),
},
{
accessorKey: "skill_count",
@@ -111,7 +120,9 @@ export function SkillPacksTable({
enableSorting: false,
cell: ({ row }) => {
if (!onSync) return null;
const isThisPackSyncing = Boolean(syncingPackIds?.has(row.original.id));
const isThisPackSyncing = Boolean(
syncingPackIds?.has(row.original.id),
);
return (
<div className="flex justify-end">
<Button

View File

@@ -34,7 +34,8 @@ export const useTableSortingState = (
resolvedSorting: SortingState;
handleSortingChange: OnChangeFn<SortingState>;
} => {
const [internalSorting, setInternalSorting] = useState<SortingState>(defaultSorting);
const [internalSorting, setInternalSorting] =
useState<SortingState>(defaultSorting);
const resolvedSorting = sorting ?? internalSorting;
const handleSortingChange: OnChangeFn<SortingState> =
onSortingChange ??

View File

@@ -3,7 +3,9 @@ export const normalizeRepoSourceUrl = (sourceUrl: string): string => {
return trimmed.endsWith(".git") ? trimmed.slice(0, -4) : trimmed;
};
export const repoBaseFromSkillSourceUrl = (skillSourceUrl: string): string | null => {
export const repoBaseFromSkillSourceUrl = (
skillSourceUrl: string,
): string | null => {
try {
const parsed = new URL(skillSourceUrl);
const marker = "/tree/";
@@ -11,7 +13,8 @@ export const repoBaseFromSkillSourceUrl = (skillSourceUrl: string): string | nul
if (markerIndex <= 0) return null;
// Reject unexpected structures (e.g. multiple /tree/ markers).
if (parsed.pathname.indexOf(marker, markerIndex + marker.length) !== -1) return null;
if (parsed.pathname.indexOf(marker, markerIndex + marker.length) !== -1)
return null;
const repoPath = parsed.pathname.slice(0, markerIndex);
if (!repoPath || repoPath === "/") return null;