Install plugins from Yaak plugin registry (#230)

This commit is contained in:
Gregory Schier
2025-06-23 08:55:38 -07:00
committed by GitHub
parent b5620fcdf3
commit cb7c44cc65
27 changed files with 421 additions and 218 deletions

View File

@@ -1,8 +1,13 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { openUrl } from '@tauri-apps/plugin-opener';
import type { Plugin } from '@yaakapp-internal/models';
import { pluginsAtom } from '@yaakapp-internal/models';
import { installPlugin, PluginVersion, searchPlugins } from '@yaakapp-internal/plugins';
import { Plugin, pluginsAtom } from '@yaakapp-internal/models';
import {
checkPluginUpdates,
installPlugin,
PluginVersion,
searchPlugins,
} from '@yaakapp-internal/plugins';
import { PluginUpdatesResponse } from '@yaakapp-internal/plugins/bindings/gen_api';
import { useAtomValue } from 'jotai';
import React, { useState } from 'react';
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
@@ -43,15 +48,8 @@ export function SettingsPlugins() {
<PluginSearch />
</TabContent>
<TabContent value="installed">
<InstalledPlugins />
<form
onSubmit={(e) => {
e.preventDefault();
if (directory == null) return;
createPlugin.mutate(directory);
setDirectory(null);
}}
>
<div className="h-full grid grid-rows-[minmax(0,1fr)_auto]">
<InstalledPlugins />
<footer className="grid grid-cols-[minmax(0,1fr)_auto] -mx-4 py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0">
<SelectFile
size="xs"
@@ -62,7 +60,16 @@ export function SettingsPlugins() {
/>
<HStack>
{directory && (
<Button size="xs" type="submit" color="primary" className="ml-auto">
<Button
size="xs"
color="primary"
className="ml-auto"
onClick={() => {
if (directory == null) return;
createPlugin.mutate(directory);
setDirectory(null);
}}
>
Add Plugin
</Button>
)}
@@ -83,31 +90,61 @@ export function SettingsPlugins() {
/>
</HStack>
</footer>
</form>
</div>
</TabContent>
</Tabs>
</div>
);
}
function PluginInfo({ plugin }: { plugin: Plugin }) {
function PluginTableRow({
plugin,
updates,
}: {
plugin: Plugin;
updates: PluginUpdatesResponse | null;
}) {
const pluginInfo = usePluginInfo(plugin.id);
const deletePlugin = useUninstallPlugin();
const uninstallPlugin = useUninstallPlugin();
const latestVersion = updates?.plugins.find((u) => u.name === pluginInfo.data?.name)?.version;
const installPluginMutation = useMutation({
mutationKey: ['install_plugin', plugin.id],
mutationFn: (name: string) => installPlugin(name, null),
});
if (pluginInfo.data == null) return null;
return (
<tr className="group">
<td className="py-2 select-text cursor-text w-full">{pluginInfo.data?.name}</td>
<td className="py-2 select-text cursor-text text-right">
<TableRow>
<TableCell className="font-semibold">{pluginInfo.data.displayName}</TableCell>
<TableCell>
<InlineCode>{pluginInfo.data?.version}</InlineCode>
</td>
<td className="py-2 select-text cursor-text pl-2">
<IconButton
size="sm"
icon="trash"
title="Uninstall plugin"
onClick={() => deletePlugin.mutate(plugin.id)}
/>
</td>
</tr>
</TableCell>
<TableCell className="w-full text-text-subtle">{pluginInfo.data.description}</TableCell>
<TableCell>
<HStack>
{latestVersion != null && (
<Button
variant="border"
color="success"
title={`Update to ${latestVersion}`}
size="xs"
isLoading={installPluginMutation.isPending}
onClick={() => installPluginMutation.mutate(pluginInfo.data.name)}
>
Update
</Button>
)}
<IconButton
size="sm"
icon="trash"
title="Uninstall plugin"
onClick={async () => {
uninstallPlugin.mutate({ pluginId: plugin.id, name: pluginInfo.data.displayName });
}}
/>
</HStack>
</TableCell>
</TableRow>
);
}
@@ -135,7 +172,7 @@ function PluginSearch() {
<EmptyStateText>
<LoadingIcon size="xl" className="text-text-subtlest" />
</EmptyStateText>
) : (results.data.results ?? []).length === 0 ? (
) : (results.data.plugins ?? []).length === 0 ? (
<EmptyStateText>No plugins found</EmptyStateText>
) : (
<Table>
@@ -148,22 +185,20 @@ function PluginSearch() {
</TableRow>
</TableHead>
<TableBody>
{results.data.results.map((plugin) => {
return (
<TableRow key={plugin.id}>
<TableCell className="font-semibold">{plugin.displayName}</TableCell>
<TableCell className="text-text-subtle">
<InlineCode>{plugin.version}</InlineCode>
</TableCell>
<TableCell className="w-full text-text-subtle">
{plugin.description ?? 'n/a'}
</TableCell>
<TableCell>
<InstallPluginButton plugin={plugin} />
</TableCell>
</TableRow>
);
})}
{results.data.plugins.map((plugin) => (
<TableRow key={plugin.id}>
<TableCell className="font-semibold">{plugin.displayName}</TableCell>
<TableCell>
<InlineCode>{plugin.version}</InlineCode>
</TableCell>
<TableCell className="w-full text-text-subtle">
{plugin.description ?? 'n/a'}
</TableCell>
<TableCell>
<InstallPluginButton plugin={plugin} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
@@ -174,23 +209,23 @@ function PluginSearch() {
function InstallPluginButton({ plugin }: { plugin: PluginVersion }) {
const plugins = useAtomValue(pluginsAtom);
const deletePlugin = useUninstallPlugin();
const uninstallPlugin = useUninstallPlugin();
const installed = plugins?.some((p) => p.id === plugin.id);
const installPluginMutation = useMutation({
mutationKey: ['install_plugin', plugin.id],
mutationFn: installPlugin,
mutationFn: (pv: PluginVersion) => installPlugin(pv.name, null),
});
return (
<Button
size="xs"
variant={installed ? 'solid' : 'border'}
color={installed ? 'primary' : 'secondary'}
variant="border"
color={installed ? 'secondary' : 'primary'}
className="ml-auto"
isLoading={installPluginMutation.isPending}
onClick={async () => {
if (installed) {
deletePlugin.mutate(plugin.id);
uninstallPlugin.mutate({ pluginId: plugin.id, name: plugin.displayName });
} else {
installPluginMutation.mutate(plugin);
}
@@ -203,6 +238,11 @@ function InstallPluginButton({ plugin }: { plugin: PluginVersion }) {
function InstalledPlugins() {
const plugins = useAtomValue(pluginsAtom);
const updates = useQuery({
queryKey: ['plugin_updates'],
queryFn: () => checkPluginUpdates(),
});
return plugins.length === 0 ? (
<div className="pb-4">
<EmptyStateText className="text-center">
@@ -212,19 +252,20 @@ function InstalledPlugins() {
</EmptyStateText>
</div>
) : (
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th className="py-2 text-left">Plugin</th>
<th className="py-2 text-right">Version</th>
<th></th>
</tr>
</thead>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell>Description</TableHeaderCell>
<TableHeaderCell />
</TableRow>
</TableHead>
<tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => (
<PluginInfo key={p.id} plugin={p} />
))}
{plugins.map((p) => {
return <PluginTableRow key={p.id} plugin={p} updates={updates.data ?? null} />;
})}
</tbody>
</table>
</Table>
);
}

View File

@@ -33,6 +33,7 @@ const icons = {
chevron_down: lucide.ChevronDownIcon,
chevron_right: lucide.ChevronRightIcon,
circle_alert: lucide.CircleAlertIcon,
circle_fading_arrow_up: lucide.CircleFadingArrowUpIcon,
clock: lucide.ClockIcon,
code: lucide.CodeIcon,
columns_2: lucide.Columns2Icon,
@@ -133,7 +134,7 @@ export const Icon = memo(function Icon({
title={title}
className={classNames(
className,
'flex-shrink-0 transform-cpu',
'flex-shrink-0',
size === 'xl' && 'h-6 w-6',
size === 'lg' && 'h-5 w-5',
size === 'md' && 'h-4 w-4',

View File

@@ -6,6 +6,7 @@ import type { ButtonProps } from './Button';
import { Button } from './Button';
import type { IconProps } from './Icon';
import { Icon } from './Icon';
import { LoadingIcon } from './LoadingIcon';
export type IconButtonProps = IconProps &
ButtonProps & {
@@ -31,6 +32,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
iconSize,
showBadge,
iconColor,
isLoading,
...props
}: IconButtonProps,
ref,
@@ -70,18 +72,22 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
<div className="w-2.5 h-2.5 bg-pink-500 rounded-full" />
</div>
)}
<Icon
size={iconSize}
icon={confirmed ? 'check' : icon}
spin={spin}
color={iconColor}
className={classNames(
iconClassName,
'group-hover/button:text-text',
confirmed && '!text-success', // Don't use Icon.color here because it won't override the hover color
props.disabled && 'opacity-70',
)}
/>
{isLoading ? (
<LoadingIcon size={iconSize} className={iconClassName} />
) : (
<Icon
size={iconSize}
icon={confirmed ? 'check' : icon}
spin={spin}
color={iconColor}
className={classNames(
iconClassName,
'group-hover/button:text-text',
confirmed && '!text-success', // Don't use Icon.color here because it won't override the hover color
props.disabled && 'opacity-70',
)}
/>
)}
</Button>
);
});

View File

@@ -53,7 +53,7 @@ export function TableHeaderCell({
children,
className,
}: {
children: ReactNode;
children?: ReactNode;
className?: string;
}) {
return (