gRPC schema from files!

This commit is contained in:
Gregory Schier
2024-02-06 19:20:32 -08:00
parent 1293870e11
commit c6b5e4d5df
13 changed files with 442 additions and 230 deletions

View File

@@ -75,11 +75,6 @@ export function GrpcConnectionSetupPane({
[updateRequest],
);
const handleSelectProtoFiles = useCallback(
(paths: string[]) => updateRequest.mutateAsync({ protoFiles: paths }),
[updateRequest],
);
const select = useMemo(() => {
const options =
services?.flatMap((s) =>
@@ -159,10 +154,12 @@ export function GrpcConnectionSetupPane({
shortLabel: o.label,
}))}
extraItems={[
{ type: 'separator' },
{
label: 'Custom',
label: 'Refresh',
type: 'default',
key: 'custom',
leftSlot: <Icon className="text-gray-600" size="sm" icon="refresh" />,
},
]}
>
@@ -234,7 +231,6 @@ export function GrpcConnectionSetupPane({
reflectionError={reflectionError}
reflectionLoading={reflectionLoading}
onReflect={onReflectRefetch}
onSelectProtoFiles={handleSelectProtoFiles}
request={activeRequest}
/>
</VStack>

View File

@@ -1,4 +1,4 @@
import { open } from '@tauri-apps/api/dialog';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import { updateSchema } from 'codemirror-json-schema';
import { useEffect, useRef } from 'react';
@@ -7,14 +7,12 @@ import type { ReflectResponseService } from '../hooks/useGrpc';
import { tryFormatJson } from '../lib/formatters';
import type { GrpcRequest } from '../lib/models';
import { count } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor';
import { Editor } from './core/Editor';
import { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks';
import { VStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { GrpcProtoSelection } from './GrpcProtoSelection';
@@ -23,16 +21,12 @@ type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
reflectionError?: string;
reflectionLoading?: boolean;
request: GrpcRequest;
onReflect: () => void;
onSelectProtoFiles: (paths: string[]) => void;
};
export function GrpcEditor({
services,
reflectionError,
reflectionLoading,
onReflect,
onSelectProtoFiles,
request,
...extraEditorProps
}: Props) {
@@ -97,6 +91,7 @@ export function GrpcEditor({
}, [alert, services, request.method, request.service]);
const reflectionUnavailable = reflectionError?.match(/unimplemented/i);
const reflectionSuccess = !reflectionError && services != null && request.protoFiles.length === 0;
reflectionError = reflectionUnavailable ? undefined : reflectionError;
return (
@@ -110,7 +105,7 @@ export function GrpcEditor({
placeholder="..."
ref={editorViewRef}
actions={[
<div key="reflection" className="!opacity-100">
<div key="reflection" className={classNames(!reflectionSuccess && '!opacity-100')}>
<Button
size="xs"
color={
@@ -128,39 +123,13 @@ export function GrpcEditor({
title: 'Configure Schema',
size: 'md',
id: 'reflection-failed',
render: ({ hide }) => (
<VStack space={6} className="pb-5">
{reflectionError && <FormattedError>{reflectionError}</FormattedError>}
{reflectionUnavailable && request.protoFiles.length === 0 && (
<Banner>
<VStack space={3}>
<p>
<InlineCode>{request.url}</InlineCode> doesn&apos;t implement{' '}
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
Server Reflection
</Link>{' '}
. Please manually add the <InlineCode>.proto</InlineCode> files to get
started.
</p>
<div>
<Button
size="xs"
color="gray"
variant="border"
onClick={() => {
hide();
onReflect();
}}
>
Retry Reflection
</Button>
</div>
</VStack>
</Banner>
)}
<GrpcProtoSelection requestId={request.id} />
</VStack>
),
render: ({ hide }) => {
return (
<VStack space={6} className="pb-5">
<GrpcProtoSelection onDone={hide} requestId={request.id} />
</VStack>
);
},
});
}}
>
@@ -170,10 +139,10 @@ export function GrpcEditor({
? 'Select Proto Files'
: reflectionError
? 'Server Error'
: services != null
? 'Proto Schema'
: request.protoFiles.length > 0
? count('Proto File', request.protoFiles.length)
? count('File', request.protoFiles.length)
: services != null && request.protoFiles.length === 0
? 'Schema Detected'
: 'Select Schema'}
</Button>
</div>,

View File

@@ -1,62 +1,42 @@
import { open } from '@tauri-apps/api/dialog';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcRequest } from '../hooks/useGrpcRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { count } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks';
export function GrpcProtoSelection({ requestId }: { requestId: string }) {
interface Props {
requestId: string;
onDone: () => void;
}
export function GrpcProtoSelection({ requestId }: Props) {
const request = useGrpcRequest(requestId);
const grpc = useGrpc(request, null);
const updateRequest = useUpdateGrpcRequest(request?.id ?? null);
const services = grpc.reflect.data;
const serverReflection = request?.protoFiles.length === 0 && services != null;
let reflectError = grpc.reflect.error ?? null;
const reflectionUnimplemented = `${reflectError}`.match(/unimplemented/i);
if (reflectionUnimplemented) {
reflectError = null;
}
if (request == null) {
return null;
}
return (
<div>
{request.protoFiles.length > 0 && (
<table className="w-full divide-y mb-3">
<thead>
<tr>
<th className="text-gray-600">
<span className="font-mono text-sm">*.proto</span> Files
</th>
<th></th>
</tr>
</thead>
<tbody className="divide-y">
{request.protoFiles.map((f, i) => (
<tr key={f + i} className="group">
<td className="pl-1 text-sm font-mono">{f.split('/').pop()}</td>
<td className="w-0 py-0.5">
<IconButton
title="Remove file"
size="sm"
icon="trash"
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100"
onClick={() => {
updateRequest.mutate({
protoFiles: request.protoFiles.filter((p) => p !== f),
});
}}
/>
</td>
</tr>
))}
</tbody>
</table>
)}
<HStack space={2} justifyContent="end">
<Button
color="gray"
size="sm"
onClick={async () => {
updateRequest.mutate({ protoFiles: [] });
}}
>
Clear Files
</Button>
<VStack className="flex-col-reverse" space={3}>
{/* Buttons on top so they get focus first */}
<HStack space={2} justifyContent="start" className="flex-row-reverse">
<Button
color="primary"
size="sm"
@@ -68,12 +48,101 @@ export function GrpcProtoSelection({ requestId }: { requestId: string }) {
});
if (files == null || typeof files === 'string') return;
const newFiles = files.filter((f) => !request.protoFiles.includes(f));
updateRequest.mutate({ protoFiles: [...request.protoFiles, ...newFiles] });
await updateRequest.mutateAsync({ protoFiles: [...request.protoFiles, ...newFiles] });
await grpc.reflect.refetch();
}}
>
Add Files
</Button>
<Button
isLoading={grpc.reflect.isFetching}
disabled={grpc.reflect.isFetching}
color="gray"
size="sm"
onClick={() => grpc.reflect.refetch()}
>
Refresh Schema
</Button>
</HStack>
</div>
<VStack space={5}>
{!serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2">
<p>
Found services
{services?.slice(0, 5).map((s, i) => {
return (
<span key={i}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
</span>
);
})}
{services?.length > 5 && count('other', services?.length - 5)}
</p>
</Banner>
)}
{serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2">
<p>
Server reflection found services
{services?.map((s, i) => {
return (
<span key={i}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
</span>
);
})}
. You can override this schema by manually selecting <InlineCode>*.proto</InlineCode>{' '}
files.
</p>
</Banner>
)}
{request.protoFiles.length > 0 && (
<table className="w-full divide-y">
<thead>
<tr>
<th className="text-gray-600">
<span className="font-mono text-sm">*.proto</span> Files
</th>
<th></th>
</tr>
</thead>
<tbody className="divide-y">
{request.protoFiles.map((f, i) => (
<tr key={f + i} className="group">
<td className="pl-1 text-sm font-mono">{f.split('/').pop()}</td>
<td className="w-0 py-0.5">
<IconButton
title="Remove file"
size="sm"
icon="trash"
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100"
onClick={async () => {
await updateRequest.mutateAsync({
protoFiles: request.protoFiles.filter((p) => p !== f),
});
grpc.reflect.remove();
}}
/>
</td>
</tr>
))}
</tbody>
</table>
)}
{reflectError && <FormattedError>{reflectError}</FormattedError>}
{reflectionUnimplemented && request.protoFiles.length === 0 && (
<Banner>
<InlineCode>{request.url}</InlineCode> doesn&apos;t implement{' '}
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
Server Reflection
</Link>{' '}
. Please manually add the <InlineCode>.proto</InlineCode> files to get started.
</Banner>
)}
</VStack>
</VStack>
);
}

View File

@@ -51,7 +51,23 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
),
render: ({ hide }) => {
return (
<HStack space={2} justifyContent="end" alignItems="center" className="mt-4 mb-6">
<HStack
space={2}
justifyContent="start"
alignItems="center"
className="mt-4 mb-6 flex-row-reverse"
>
<Button
className="focus"
color="gray"
onClick={async () => {
hide();
const environmentId = (await getRecentEnvironments(w.id))[0];
routes.navigate('workspace', { workspaceId: w.id, environmentId });
}}
>
This Window
</Button>
<Button
className="focus"
color="gray"
@@ -66,18 +82,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
>
New Window
</Button>
<Button
autoFocus
className="focus"
color="gray"
onClick={async () => {
hide();
const environmentId = (await getRecentEnvironments(w.id))[0];
routes.navigate('workspace', { workspaceId: w.id, environmentId });
}}
>
This Window
</Button>
</HStack>
);
},

View File

@@ -64,22 +64,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
variant === 'solid' && color === 'custom' && 'ring-blue-500/50',
variant === 'solid' &&
color === 'default' &&
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-500/50',
variant === 'solid' &&
color === 'gray' &&
'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
variant === 'solid' &&
color === 'primary' &&
'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
variant === 'solid' &&
color === 'secondary' &&
'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
variant === 'solid' &&
color === 'warning' &&
'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
variant === 'solid' &&
color === 'danger' &&
'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-gray-400',
variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700',
variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700',
variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700',
variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700',
// Borders
variant === 'border' && 'border',
variant === 'border' &&

View File

@@ -399,7 +399,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
{items.map((item, i) => {
if (item.type === 'separator') {
return (
<Separator key={i} className="ml-2 my-1.5">
<Separator key={i} className={classNames('my-1.5', item.label && 'ml-2')}>
{item.label}
</Separator>
);