mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-18 07:23:51 +01:00
Dynamic plugins (#68)
This commit is contained in:
@@ -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') {
|
||||
|
||||
@@ -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 = <>‎</>;
|
||||
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
95
src-web/components/Settings/SettingsPlugins.tsx
Normal file
95
src-web/components/Settings/SettingsPlugins.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
)}
|
||||
|
||||
@@ -14,9 +14,11 @@ export function useActiveWorkspaceChangedToast() {
|
||||
|
||||
setId(activeWorkspace?.id ?? null);
|
||||
|
||||
// Don't notify on first load
|
||||
// Don't notify on the first load
|
||||
if (id === null) return;
|
||||
|
||||
console.log('HELLO?', activeWorkspace?.id, id, window.location);
|
||||
|
||||
toast.show({
|
||||
id: 'workspace-changed',
|
||||
timeout: 3000,
|
||||
|
||||
13
src-web/hooks/useCreatePlugin.ts
Normal file
13
src-web/hooks/useCreatePlugin.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function useCreatePlugin() {
|
||||
return useMutation<void, unknown, string>({
|
||||
mutationKey: ['create_plugin'],
|
||||
mutationFn: async (directory: string) => {
|
||||
await invokeCmd('cmd_create_plugin', { directory });
|
||||
},
|
||||
onSettled: () => trackEvent('plugin', 'create'),
|
||||
});
|
||||
}
|
||||
13
src-web/hooks/usePluginInfo.ts
Normal file
13
src-web/hooks/usePluginInfo.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { BootResponse } from '@yaakapp/api';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function usePluginInfo(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ['plugin_info', id],
|
||||
queryFn: async () => {
|
||||
const info = (await invokeCmd('cmd_plugin_info', { id })) as BootResponse;
|
||||
return info;
|
||||
},
|
||||
});
|
||||
}
|
||||
23
src-web/hooks/usePlugins.ts
Normal file
23
src-web/hooks/usePlugins.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Plugin } from '@yaakapp/api';
|
||||
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||
import { listPlugins } from '../lib/store';
|
||||
|
||||
const plugins = await listPlugins();
|
||||
export const pluginsAtom = atom<Plugin[]>(plugins);
|
||||
|
||||
export function usePlugins() {
|
||||
return useAtomValue(pluginsAtom);
|
||||
}
|
||||
|
||||
export function useRefreshPlugins() {
|
||||
const setPlugins = useSetAtom(pluginsAtom);
|
||||
return useMutation({
|
||||
mutationKey: ['refresh_plugins'],
|
||||
mutationFn: async () => {
|
||||
const plugins = await minPromiseMillis(listPlugins());
|
||||
setPlugins(plugins);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export type TrackResource =
|
||||
| 'http_request'
|
||||
| 'http_response'
|
||||
| 'key_value'
|
||||
| 'plugin'
|
||||
| 'setting'
|
||||
| 'sidebar'
|
||||
| 'theme'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sleep } from './sleep';
|
||||
|
||||
/** Ensures a promise takes at least a certain number of milliseconds to resolve */
|
||||
export async function minPromiseMillis<T>(promise: Promise<T>, millis: number) {
|
||||
export async function minPromiseMillis<T>(promise: Promise<T>, millis = 300) {
|
||||
const start = Date.now();
|
||||
let result;
|
||||
let err;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
Folder,
|
||||
GrpcRequest,
|
||||
HttpRequest,
|
||||
Plugin,
|
||||
Settings,
|
||||
Workspace,
|
||||
} from '@yaakapp/api';
|
||||
@@ -63,6 +64,11 @@ export async function listWorkspaces(): Promise<Workspace[]> {
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
export async function listPlugins(): Promise<Plugin[]> {
|
||||
const plugins: Plugin[] = (await invokeCmd('cmd_list_plugins')) ?? [];
|
||||
return plugins;
|
||||
}
|
||||
|
||||
export async function getCookieJar(id: string | null): Promise<CookieJar | null> {
|
||||
if (id === null) return null;
|
||||
const cookieJar: CookieJar = (await invokeCmd('cmd_get_cookie_jar', { id })) ?? null;
|
||||
|
||||
@@ -6,6 +6,7 @@ type TauriCmd =
|
||||
| 'cmd_check_for_updates'
|
||||
| 'cmd_create_cookie_jar'
|
||||
| 'cmd_create_environment'
|
||||
| 'cmd_create_plugin'
|
||||
| 'cmd_template_tokens_to_string'
|
||||
| 'cmd_create_folder'
|
||||
| 'cmd_create_grpc_request'
|
||||
@@ -47,11 +48,13 @@ type TauriCmd =
|
||||
| 'cmd_list_grpc_requests'
|
||||
| 'cmd_list_http_requests'
|
||||
| 'cmd_list_http_responses'
|
||||
| 'cmd_list_plugins'
|
||||
| 'cmd_list_workspaces'
|
||||
| 'cmd_metadata'
|
||||
| 'cmd_new_nested_window'
|
||||
| 'cmd_new_window'
|
||||
| 'cmd_parse_template'
|
||||
| 'cmd_plugin_info'
|
||||
| 'cmd_render_template'
|
||||
| 'cmd_save_response'
|
||||
| 'cmd_send_ephemeral_request'
|
||||
|
||||
Reference in New Issue
Block a user