Dynamic plugins (#68)

This commit is contained in:
Gregory Schier
2024-09-06 10:43:25 -07:00
committed by GitHub
parent f7949c9909
commit b3adbc1860
37 changed files with 533 additions and 184 deletions

View File

@@ -18,6 +18,7 @@ import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { pluginsAtom } from '../hooks/usePlugins';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
@@ -67,6 +68,7 @@ export function GlobalHooks() {
const setSettings = useSetAtom(settingsAtom);
const setWorkspaces = useSetAtom(workspacesAtom);
const setPlugins = useSetAtom(pluginsAtom);
const setHttpRequests = useSetAtom(httpRequestsAtom);
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
const setEnvironments = useSetAtom(environmentsAtom);
@@ -100,6 +102,8 @@ export function GlobalHooks() {
if (model.model === 'workspace') {
setWorkspaces(updateModelList(model, pushToFront));
} else if (model.model === 'plugin') {
setPlugins(updateModelList(model, pushToFront));
} else if (model.model === 'http_request') {
setHttpRequests(updateModelList(model, pushToFront));
} else if (model.model === 'grpc_request') {
@@ -129,6 +133,8 @@ export function GlobalHooks() {
if (model.model === 'workspace') {
setWorkspaces(removeById(model));
} else if (model.model === 'plugin') {
setPlugins(removeById(model));
} else if (model.model === 'http_request') {
setHttpRequests(removeById(model));
} else if (model.model === 'http_response') {

View File

@@ -9,20 +9,31 @@ import { HStack } from './core/Stacks';
type Props = ButtonProps & {
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
filePath: string | null;
directory?: boolean;
inline?: boolean;
noun?: string;
};
// Special character to insert ltr text in rtl element
const rtlEscapeChar = <>&#x200E;</>;
export function SelectFile({ onChange, filePath, inline, className }: Props) {
export function SelectFile({
onChange,
filePath,
inline,
className,
directory,
noun,
size = 'sm',
...props
}: Props) {
const handleClick = async () => {
const filePath = await open({
title: 'Select File',
multiple: false,
directory,
});
if (filePath == null) return;
const contentType = filePath ? mime.getType(filePath) : null;
onChange({ filePath, contentType });
};
@@ -31,31 +42,41 @@ export function SelectFile({ onChange, filePath, inline, className }: Props) {
onChange({ filePath: null, contentType: null });
};
const itemLabel = noun ?? (directory ? 'Folder' : 'File');
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel;
return (
<HStack space={1.5} className="group relative justify-stretch">
<HStack space={1.5} className="group relative justify-stretch overflow-hidden">
<Button
className={classNames(className, 'font-mono text-xs rtl', inline && 'w-full')}
color="secondary"
size="sm"
onClick={handleClick}
size={size}
{...props}
>
{rtlEscapeChar}
{inline ? <>{filePath || 'Select File'}</> : <>Select File</>}
{inline ? filePath || selectOrChange : selectOrChange}
</Button>
{!inline && (
<>
{filePath && (
<IconButton
size="sm"
size={size}
variant="border"
icon="x"
title="Unset File"
title={'Unset ' + itemLabel}
onClick={handleClear}
/>
)}
<div className="text-sm font-mono truncate rtl pr-3 text">
<div
className={classNames(
'font-mono truncate rtl pl-1.5 pr-3 text-text',
size === 'xs' && 'text-xs',
size === 'sm' && 'text-sm',
)}
>
{rtlEscapeChar}
{filePath ?? 'No file selected'}
{filePath ?? `No ${itemLabel.toLowerCase()} selected`}
</div>
</>
)}

View File

@@ -10,13 +10,15 @@ import { HeaderSize } from '../HeaderSize';
import { WindowControls } from '../WindowControls';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsPlugins } from './SettingsPlugins';
enum Tab {
General = 'general',
Appearance = 'appearance',
Plugins = 'plugins',
}
const tabs = [Tab.General, Tab.Appearance];
const tabs = [Tab.General, Tab.Appearance, Tab.Plugins];
export const Settings = () => {
const osInfo = useOsInfo();
@@ -58,6 +60,9 @@ export const Settings = () => {
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
</TabContent>
<TabContent value={Tab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
<SettingsPlugins />
</TabContent>
</Tabs>
</div>
);

View File

@@ -0,0 +1,95 @@
import type { Plugin } from '@yaakapp/api';
import React from 'react';
import { useCreatePlugin } from '../../hooks/useCreatePlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo';
import { usePlugins, useRefreshPlugins } from '../../hooks/usePlugins';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode';
import { HStack } from '../core/Stacks';
import { SelectFile } from '../SelectFile';
export function SettingsPlugins() {
const [directory, setDirectory] = React.useState<string | null>(null);
const plugins = usePlugins();
const createPlugin = useCreatePlugin();
const refreshPlugins = useRefreshPlugins();
return (
<div className="grid grid-rows-[minmax(0,1fr)_auto] h-full">
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th></th>
<th className="py-2 text-left">Plugin</th>
<th className="py-2 text-right">Version</th>
<th></th>
</tr>
</thead>
<tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => (
<PluginInfo key={p.id} plugin={p} />
))}
</tbody>
</table>
<form
onSubmit={(e) => {
e.preventDefault();
if (directory == null) return;
createPlugin.mutate(directory);
setDirectory(null);
}}
>
<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"
noun="Plugin"
directory
onChange={({ filePath }) => setDirectory(filePath)}
filePath={directory}
/>
<HStack>
{directory && (
<Button size="xs" type="submit" color="primary" className="ml-auto">
Add Plugin
</Button>
)}
<IconButton
size="sm"
icon="refresh"
title="Reload plugins"
spin={refreshPlugins.isPending}
onClick={() => refreshPlugins.mutate()}
/>
</HStack>
</footer>
</form>
</div>
);
}
function PluginInfo({ plugin }: { plugin: Plugin }) {
const pluginInfo = usePluginInfo(plugin.id);
if (pluginInfo.data == null) return null;
return (
<tr className="group">
<td className="pr-2">
<Checkbox hideLabel checked={true} title="foo" onChange={() => null} />
</td>
<td className="py-2 select-text cursor-text w-full">
<InlineCode>{pluginInfo.data?.name}</InlineCode>
</td>
<td className="py-2 select-text cursor-text text-right">
<InlineCode>{pluginInfo.data?.version}</InlineCode>
</td>
<td className="py-2 select-text cursor-text pl-2">
<IconButton
size="sm"
icon="trash"
title="Uninstall plugin"
className="text-text-subtlest"
/>
</td>
</tr>
);
}

View File

@@ -73,7 +73,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
size === 'md' && 'h-md px-3 rounded-md',
size === 'sm' && 'h-sm px-2.5 rounded-md',
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
size === '2xs' && 'h-5 px-1 text-xs rounded',
size === '2xs' && 'h-2xs px-1 text-xs rounded',
// Solids
variant === 'solid' && 'border-transparent',

View File

@@ -58,9 +58,9 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
'!px-0',
color === 'custom' && 'text-text-subtle',
color === 'default' && 'text-text-subtle',
size === 'md' && 'w-9',
size === 'sm' && 'w-8',
size === 'xs' && 'w-6',
size === 'md' && 'w-md',
size === 'sm' && 'w-sm',
size === 'xs' && 'w-xs',
size === '2xs' && 'w-5',
)}
{...props}

View File

@@ -6,6 +6,7 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code
className={classNames(
className,
'select-text cursor-text',
'font-mono text-shrink bg-surface-highlight border border-border-subtle',
'px-1.5 py-0.5 rounded text shadow-inner break-words',
)}