mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-24 10:21:15 +01:00
gRPC request actions and "copy as gRPCurl" (#232)
This commit is contained in:
@@ -10,6 +10,7 @@ import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { Link } from './core/Link';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { Icon } from './core/Icon';
|
||||
|
||||
interface Props {
|
||||
onDone: () => void;
|
||||
@@ -45,6 +46,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
||||
<HStack space={2} justifyContent="start" className="flex-row-reverse">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="border"
|
||||
onClick={async () => {
|
||||
const selected = await open({
|
||||
title: 'Select Proto Files',
|
||||
@@ -58,11 +60,28 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
||||
await grpc.reflect.refetch();
|
||||
}}
|
||||
>
|
||||
Add Proto File(s)
|
||||
Add Files
|
||||
</Button>
|
||||
<Button
|
||||
variant="border"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
const selected = await open({
|
||||
title: 'Select Proto Directory',
|
||||
directory: true,
|
||||
});
|
||||
if (selected == null) return;
|
||||
|
||||
await protoFilesKv.set([...protoFiles.filter((f) => f !== selected), selected]);
|
||||
await grpc.reflect.refetch();
|
||||
}}
|
||||
>
|
||||
Add Directories
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={grpc.reflect.isFetching}
|
||||
disabled={grpc.reflect.isFetching}
|
||||
variant="border"
|
||||
color="secondary"
|
||||
onClick={() => grpc.reflect.refetch()}
|
||||
>
|
||||
@@ -70,6 +89,14 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
||||
</Button>
|
||||
</HStack>
|
||||
<VStack space={5}>
|
||||
{reflectError && (
|
||||
<Banner color="warning">
|
||||
<h1 className="font-bold">
|
||||
Reflection failed on URL <InlineCode>{request.url || 'n/a'}</InlineCode>
|
||||
</h1>
|
||||
<p>{reflectError.trim()}</p>
|
||||
</Banner>
|
||||
)}
|
||||
{!serverReflection && services != null && services.length > 0 && (
|
||||
<Banner className="flex flex-col gap-2">
|
||||
<p>
|
||||
@@ -108,39 +135,41 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
||||
<table className="w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-text-subtlest">
|
||||
Added Files
|
||||
</th>
|
||||
<th/>
|
||||
<th />
|
||||
<th className="text-text-subtlest">Added File Paths</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{protoFiles.map((f, i) => (
|
||||
<tr key={f + i} className="group">
|
||||
<td className="pl-1 font-mono text-sm" title={f}>{f.split('/').pop()}</td>
|
||||
<td className="w-0 py-0.5">
|
||||
<IconButton
|
||||
title="Remove file"
|
||||
icon="trash"
|
||||
className="ml-auto opacity-50 transition-opacity group-hover:opacity-100"
|
||||
onClick={async () => {
|
||||
await protoFilesKv.set(protoFiles.filter((p) => p !== f));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{protoFiles.map((f, i) => {
|
||||
const parts = f.split('/');
|
||||
return (
|
||||
<tr key={f + i} className="group">
|
||||
<td>
|
||||
<Icon icon={f.endsWith('.proto') ? 'file_code' : 'folder_code'} />
|
||||
</td>
|
||||
<td className="pl-1 font-mono text-sm" title={f}>
|
||||
{parts.length > 3 && '.../'}
|
||||
{parts.slice(-3).join('/')}
|
||||
</td>
|
||||
<td className="w-0 py-0.5">
|
||||
<IconButton
|
||||
title="Remove file"
|
||||
variant="border"
|
||||
size="xs"
|
||||
icon="trash"
|
||||
className="my-0.5 ml-auto opacity-50 transition-opacity group-hover:opacity-100"
|
||||
onClick={async () => {
|
||||
await protoFilesKv.set(protoFiles.filter((p) => p !== f));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{reflectError && (
|
||||
<Banner color="warning">
|
||||
<h1 className="font-bold">
|
||||
Reflection failed on URL <InlineCode>{request.url || 'n/a'}</InlineCode>
|
||||
</h1>
|
||||
{reflectError}
|
||||
</Banner>
|
||||
)}
|
||||
{reflectionUnimplemented && protoFiles.length === 0 && (
|
||||
<Banner>
|
||||
<InlineCode>{request.url}</InlineCode> doesn't implement{' '}
|
||||
|
||||
@@ -14,7 +14,6 @@ export function Banner({ children, className, color }: BannerProps) {
|
||||
className={classNames(
|
||||
className,
|
||||
`x-theme-banner--${color}`,
|
||||
'whitespace-pre-wrap',
|
||||
'border border-border bg-surface',
|
||||
'px-4 py-3 rounded-lg select-auto',
|
||||
'overflow-auto text-text',
|
||||
|
||||
@@ -45,6 +45,7 @@ const icons = {
|
||||
eye: lucide.EyeIcon,
|
||||
eye_closed: lucide.EyeOffIcon,
|
||||
file_code: lucide.FileCodeIcon,
|
||||
folder_code: lucide.FolderCodeIcon,
|
||||
filter: lucide.FilterIcon,
|
||||
flame: lucide.FlameIcon,
|
||||
flask: lucide.FlaskConicalIcon,
|
||||
@@ -134,6 +135,8 @@ export const Icon = memo(function Icon({
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
!spin && 'transform-cpu',
|
||||
spin && 'animate-spin',
|
||||
'flex-shrink-0',
|
||||
size === 'xl' && 'h-6 w-6',
|
||||
size === 'lg' && 'h-5 w-5',
|
||||
@@ -149,7 +152,6 @@ export const Icon = memo(function Icon({
|
||||
color === 'success' && 'text-success',
|
||||
color === 'primary' && 'text-primary',
|
||||
color === 'secondary' && 'text-secondary',
|
||||
spin && 'animate-spin',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAtomValue } from 'jotai';
|
||||
import React, { useMemo } from 'react';
|
||||
import { openFolderSettings } from '../../commands/openFolderSettings';
|
||||
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
|
||||
import { useGrpcRequestActions } from '../../hooks/useGrpcRequestActions';
|
||||
import { useHttpRequestActions } from '../../hooks/useHttpRequestActions';
|
||||
import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace';
|
||||
import { useSendAnyHttpRequest } from '../../hooks/useSendAnyHttpRequest';
|
||||
@@ -25,6 +26,7 @@ interface Props {
|
||||
export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
const sendManyRequests = useSendManyRequests();
|
||||
const httpRequestActions = useHttpRequestActions();
|
||||
const grpcRequestActions = useGrpcRequestActions();
|
||||
const sendRequest = useSendAnyHttpRequest();
|
||||
const workspaces = useAtomValue(workspacesAtom);
|
||||
const moveToWorkspace = useMoveToWorkspace(child.id);
|
||||
@@ -65,25 +67,35 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
const requestItems: DropdownItem[] =
|
||||
child.model === 'http_request'
|
||||
? [
|
||||
{
|
||||
label: 'Send',
|
||||
hotKeyAction: 'http_request.send',
|
||||
hotKeyLabelOnly: true, // Already bound in URL bar
|
||||
leftSlot: <Icon icon="send_horizontal" />,
|
||||
onSelect: () => sendRequest.mutate(child.id),
|
||||
{
|
||||
label: 'Send',
|
||||
hotKeyAction: 'http_request.send',
|
||||
hotKeyLabelOnly: true, // Already bound in URL bar
|
||||
leftSlot: <Icon icon="send_horizontal" />,
|
||||
onSelect: () => sendRequest.mutate(child.id),
|
||||
},
|
||||
...httpRequestActions.map((a) => ({
|
||||
label: a.label,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
|
||||
onSelect: async () => {
|
||||
const request = getModel('http_request', child.id);
|
||||
if (request != null) await a.call(request);
|
||||
},
|
||||
...httpRequestActions.map((a) => ({
|
||||
label: a.label,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
|
||||
onSelect: async () => {
|
||||
const request = getModel('http_request', child.id);
|
||||
if (request != null) await a.call(request);
|
||||
},
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
]
|
||||
: [];
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
]
|
||||
: child.model === 'grpc_request'
|
||||
? grpcRequestActions.map((a) => ({
|
||||
label: a.label,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
|
||||
onSelect: async () => {
|
||||
const request = getModel('grpc_request', child.id);
|
||||
if (request != null) await a.call(request);
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
return [
|
||||
...requestItems,
|
||||
{
|
||||
@@ -134,6 +146,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
child.model,
|
||||
createDropdownItems,
|
||||
httpRequestActions,
|
||||
grpcRequestActions,
|
||||
moveToWorkspace.mutate,
|
||||
sendManyRequests,
|
||||
sendRequest,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getKeyValue } from '../lib/keyValueStore';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
|
||||
export function protoFilesArgs(requestId: string | null) {
|
||||
@@ -10,3 +11,7 @@ export function protoFilesArgs(requestId: string | null) {
|
||||
export function useGrpcProtoFiles(activeRequestId: string | null) {
|
||||
return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
|
||||
}
|
||||
|
||||
export async function getGrpcProtoFiles(activeRequestId: string | null) {
|
||||
return getKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
|
||||
}
|
||||
|
||||
51
src-web/hooks/useGrpcRequestActions.ts
Normal file
51
src-web/hooks/useGrpcRequestActions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { GrpcRequest } from '@yaakapp-internal/models';
|
||||
import type {
|
||||
CallGrpcRequestActionRequest,
|
||||
GetGrpcRequestActionsResponse,
|
||||
GrpcRequestAction,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import { useMemo } from 'react';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { getGrpcProtoFiles } from './useGrpcProtoFiles';
|
||||
import { usePluginsKey } from './usePlugins';
|
||||
|
||||
export type CallableGrpcRequestAction = Pick<GrpcRequestAction, 'label' | 'icon'> & {
|
||||
call: (grpcRequest: GrpcRequest) => Promise<void>;
|
||||
};
|
||||
|
||||
export function useGrpcRequestActions() {
|
||||
const pluginsKey = usePluginsKey();
|
||||
|
||||
const actionsResult = useQuery<CallableGrpcRequestAction[]>({
|
||||
queryKey: ['grpc_request_actions', pluginsKey],
|
||||
queryFn: async () => {
|
||||
const responses = await invokeCmd<GetGrpcRequestActionsResponse[]>(
|
||||
'cmd_grpc_request_actions',
|
||||
);
|
||||
|
||||
return responses.flatMap((r) =>
|
||||
r.actions.map((a, i) => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
call: async (grpcRequest: GrpcRequest) => {
|
||||
const protoFiles = await getGrpcProtoFiles(grpcRequest.id);
|
||||
const payload: CallGrpcRequestActionRequest = {
|
||||
index: i,
|
||||
pluginRefId: r.pluginRefId,
|
||||
args: { grpcRequest, protoFiles },
|
||||
};
|
||||
await invokeCmd('cmd_call_grpc_request_action', { req: payload });
|
||||
},
|
||||
})),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const actions = useMemo(() => {
|
||||
return actionsResult.data ?? [];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(actionsResult.data)]);
|
||||
|
||||
return actions;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
type TauriCmd =
|
||||
| 'cmd_get_themes'
|
||||
| 'cmd_call_http_authentication_action'
|
||||
| 'cmd_call_grpc_request_action'
|
||||
| 'cmd_call_http_request_action'
|
||||
| 'cmd_check_for_updates'
|
||||
| 'cmd_create_grpc_request'
|
||||
@@ -23,6 +24,7 @@ type TauriCmd =
|
||||
| 'cmd_get_workspace_meta'
|
||||
| 'cmd_grpc_go'
|
||||
| 'cmd_grpc_reflect'
|
||||
| 'cmd_grpc_request_actions'
|
||||
| 'cmd_http_request_actions'
|
||||
| 'cmd_import_data'
|
||||
| 'cmd_install_plugin'
|
||||
|
||||
Reference in New Issue
Block a user