Files
hristudio/src/components/plugins/plugins-columns.tsx
Sean O'Connor 18f709f879 feat: implement complete plugin store repository synchronization system
• Fix repository sync implementation in admin API (was TODO placeholder)
- Add full fetch/parse logic for repository.json and plugin index -
Implement robot matching by name/manufacturer patterns - Handle plugin
creation/updates with proper error handling - Add comprehensive
TypeScript typing throughout

• Fix plugin store installation state detection - Add getStudyPlugins
API integration to check installed plugins - Update PluginCard component
with isInstalled prop and correct button states - Fix repository name
display using metadata.repositoryId mapping - Show "Installed"
(disabled) vs "Install" (enabled) based on actual state

• Resolve admin access and authentication issues - Add missing
administrator role to user system roles table - Fix admin route access
for repository management - Enable repository sync functionality in
admin dashboard

• Add repository metadata integration - Update plugin records with
proper repositoryId references - Add metadata field to
robots.plugins.list API response - Enable repository name display for
all plugins from metadata

• Fix TypeScript compliance across plugin system - Replace unsafe 'any'
types with proper interfaces - Add type definitions for repository and
plugin data structures - Use nullish coalescing operators for safer null
handling - Remove unnecessary type assertions

• Integrate live repository at https://repo.hristudio.com - Successfully
loads 3 robot plugins (TurtleBot3 Burger/Waffle, NAO) - Complete ROS2
action definitions with parameter schemas - Trust level categorization
(official, verified, community) - Platform and documentation metadata
preservation

• Update documentation and development workflow - Document plugin
repository system in work_in_progress.md - Update quick-reference.md
with repository sync examples - Add plugin installation and management
guidance - Remove problematic test script with TypeScript errors

BREAKING CHANGE: Plugin store now requires repository sync for robot
plugins. Run repository sync in admin dashboard after deployment to
populate plugin store.

Closes: Plugin store repository integration Resolves: Installation state
detection and repository name display Fixes: Admin authentication and
TypeScript compliance issues
2025-08-07 10:47:29 -04:00

325 lines
8.5 KiB
TypeScript

"use client";
import { type ColumnDef } from "@tanstack/react-table";
import { formatDistanceToNow } from "date-fns";
import {
Copy,
ExternalLink,
MoreHorizontal,
Puzzle,
Settings,
Trash2,
User,
} from "lucide-react";
import { toast } from "sonner";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Checkbox } from "~/components/ui/checkbox";
import { DataTableColumnHeader } from "~/components/ui/data-table-column-header";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
export type Plugin = {
plugin: {
id: string;
robotId: string | null;
name: string;
version: string;
description: string | null;
author: string | null;
repositoryUrl: string | null;
trustLevel: "official" | "verified" | "community" | null;
status: "active" | "deprecated" | "disabled";
createdAt: Date;
updatedAt: Date;
};
installation: {
id: string;
configuration: Record<string, unknown>;
installedAt: Date;
installedBy: string;
};
};
const trustLevelConfig = {
official: {
label: "Official",
className: "bg-blue-100 text-blue-800 hover:bg-blue-200",
description: "Official HRIStudio plugin",
},
verified: {
label: "Verified",
className: "bg-green-100 text-green-800 hover:bg-green-200",
description: "Verified by the community",
},
community: {
label: "Community",
className: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
description: "Community contributed",
},
};
const statusConfig = {
active: {
label: "Active",
className: "bg-green-100 text-green-800 hover:bg-green-200",
description: "Plugin is active and working",
},
deprecated: {
label: "Deprecated",
className: "bg-orange-100 text-orange-800 hover:bg-orange-200",
description: "Plugin is deprecated",
},
disabled: {
label: "Disabled",
className: "bg-red-100 text-red-800 hover:bg-red-200",
description: "Plugin is disabled",
},
};
function PluginActionsCell({ plugin }: { plugin: Plugin }) {
const handleUninstall = async () => {
if (
window.confirm(
`Are you sure you want to uninstall "${plugin.plugin.name}"?`,
)
) {
try {
// TODO: Implement uninstall mutation
toast.success("Plugin uninstalled successfully");
} catch {
toast.error("Failed to uninstall plugin");
}
}
};
const handleCopyId = () => {
void navigator.clipboard.writeText(plugin.plugin.id);
toast.success("Plugin ID copied to clipboard");
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
Configure
</DropdownMenuItem>
{plugin.plugin.repositoryUrl && (
<DropdownMenuItem asChild>
<a
href={plugin.plugin.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="mr-2 h-4 w-4" />
View Repository
</a>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleCopyId}>
<Copy className="mr-2 h-4 w-4" />
Copy Plugin ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleUninstall}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
Uninstall
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
export const pluginsColumns: ColumnDef<Plugin>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
id: "name",
accessorFn: (row) => row.plugin.name,
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Plugin Name" />
),
cell: ({ row }) => {
const plugin = row.original;
return (
<div className="max-w-[200px] min-w-0 space-y-1">
<div className="flex items-center space-x-2">
<Puzzle className="text-muted-foreground h-4 w-4 flex-shrink-0" />
<span className="truncate font-medium" title={plugin.plugin.name}>
{plugin.plugin.name}
</span>
</div>
{plugin.plugin.description && (
<p
className="text-muted-foreground line-clamp-1 truncate text-sm"
title={plugin.plugin.description}
>
{plugin.plugin.description}
</p>
)}
</div>
);
},
},
{
accessorKey: "plugin.version",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Version" />
),
cell: ({ row }) => {
const version = row.original.plugin.version;
return (
<Badge variant="outline" className="font-mono text-xs">
v{version}
</Badge>
);
},
},
{
accessorKey: "plugin.author",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Author" />
),
cell: ({ row }) => {
const author = row.original.plugin.author;
return (
<div className="flex items-center space-x-1 text-sm">
<User className="text-muted-foreground h-3 w-3" />
<span className="max-w-[120px] truncate" title={author ?? undefined}>
{author ?? "Unknown"}
</span>
</div>
);
},
},
{
accessorKey: "plugin.trustLevel",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Trust Level" />
),
cell: ({ row }) => {
const trustLevel = row.original.plugin.trustLevel;
if (!trustLevel) return "-";
const config = trustLevelConfig[trustLevel];
return (
<Badge
variant="secondary"
className={config.className}
title={config.description}
>
{config.label}
</Badge>
);
},
filterFn: (row, id, value: string[]) => {
const trustLevel = row.original.plugin.trustLevel;
return trustLevel ? value.includes(trustLevel) : false;
},
},
{
accessorKey: "plugin.status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const status = row.original.plugin.status;
const config = statusConfig[status];
return (
<Badge
variant="secondary"
className={config.className}
title={config.description}
>
{config.label}
</Badge>
);
},
filterFn: (row, id, value: string[]) => {
return value.includes(row.original.plugin.status);
},
},
{
accessorKey: "installation.installedAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Installed" />
),
cell: ({ row }) => {
const date = row.original.installation.installedAt;
return (
<div className="text-sm whitespace-nowrap">
{formatDistanceToNow(date, { addSuffix: true })}
</div>
);
},
},
{
accessorKey: "plugin.updatedAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Updated" />
),
cell: ({ row }) => {
const date = row.original.plugin.updatedAt;
return (
<div className="text-sm whitespace-nowrap">
{formatDistanceToNow(date, { addSuffix: true })}
</div>
);
},
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => <PluginActionsCell plugin={row.original} />,
enableSorting: false,
enableHiding: false,
},
];