Template Tag Function Editor (#67)

![CleanShot 2024-08-15 at 16 53
09@2x](https://github.com/user-attachments/assets/8c0eb655-1daf-4dc8-811f-f606c770f7dc)
This commit is contained in:
Gregory Schier
2024-08-16 08:31:19 -07:00
committed by GitHub
parent a7f0fadeae
commit aa85ecb618
62 changed files with 1339 additions and 437 deletions

View File

@@ -14,7 +14,7 @@ export function useActiveEnvironment() {
export const QUERY_ENVIRONMENT_ID = 'environment_id';
export function useActiveEnvironmentId() {
function useActiveEnvironmentId() {
// NOTE: This query param is accessed from Rust side, so do not change
const [params, setParams] = useSearchParams();
const id = params.get(QUERY_ENVIRONMENT_ID);

View File

@@ -0,0 +1,24 @@
import type { EnvironmentVariable } from '@yaakapp/api';
import { useMemo } from 'react';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
export function useActiveEnvironmentVariables() {
const workspace = useActiveWorkspace();
const [environment] = useActiveEnvironment();
const variables = useMemo(() => {
const varMap: Record<string, EnvironmentVariable> = {};
const allVariables = [...(workspace?.variables ?? []), ...(environment?.variables ?? [])];
for (const v of allVariables) {
if (!v.enabled || !v.name) continue;
varMap[v.name] = v;
}
return Object.values(varMap);
}, [workspace, environment]);
return variables;
}

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import type { Workspace } from '@yaakapp/api';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import type { RouteParamsWorkspace } from './useAppRoutes';
import { useWorkspaces } from './useWorkspaces';
@@ -7,6 +7,7 @@ import { useWorkspaces } from './useWorkspaces';
export function useActiveWorkspace(): Workspace | null {
const workspaceId = useActiveWorkspaceId();
const workspaces = useWorkspaces();
return useMemo(
() => workspaces.find((w) => w.id === workspaceId) ?? null,
[workspaces, workspaceId],

View File

@@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query';
import { invokeCmd } from '../lib/tauri';
export interface AppInfo {
@@ -9,12 +8,8 @@ export interface AppInfo {
appLogDir: string;
}
const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
export function useAppInfo() {
return useQuery({
queryKey: ['appInfo'],
queryFn: async () => {
const metadata = await invokeCmd('cmd_metadata');
return metadata as AppInfo;
},
}).data;
return appInfo;
}

View File

@@ -18,7 +18,7 @@ export function useCheckForUpdates() {
title: 'No Update Available',
body: (
<>
You are currently on the latest version <InlineCode>{appInfo?.version}</InlineCode>
You are currently on the latest version <InlineCode>{appInfo.version}</InlineCode>
</>
),
});

View File

@@ -1,12 +1,11 @@
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { setKeyValue } from '../lib/keyValueStore';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { protoFilesArgs, useGrpcProtoFiles } from './useGrpcProtoFiles';
import { getGrpcProtoFiles, setGrpcProtoFiles } from './useGrpcProtoFiles';
export function useDuplicateGrpcRequest({
id,
@@ -18,7 +17,7 @@ export function useDuplicateGrpcRequest({
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const routes = useAppRoutes();
const protoFiles = useGrpcProtoFiles(id);
return useMutation<GrpcRequest, string>({
mutationKey: ['duplicate_grpc_request', id],
mutationFn: async () => {
@@ -27,8 +26,11 @@ export function useDuplicateGrpcRequest({
},
onSettled: () => trackEvent('grpc_request', 'duplicate'),
onSuccess: async (request) => {
if (id == null) return;
// Also copy proto files to new request
await setKeyValue({ ...protoFilesArgs(request.id), value: protoFiles.value ?? [] });
const protoFiles = await getGrpcProtoFiles(id);
await setGrpcProtoFiles(request.id, protoFiles);
if (navigateAfter && activeWorkspace !== null) {
routes.navigate('request', {

View File

@@ -1,24 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import type { Environment } from '@yaakapp/api';
import { atom, useAtom } from 'jotai/index';
import { useEffect } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
export function environmentsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['environments', { workspaceId }];
}
export const environmentsAtom = atom<Environment[]>([]);
export function useEnvironments() {
const [items, setItems] = useAtom(environmentsAtom);
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspace != null,
queryKey: environmentsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspace == null) return [];
return (await invokeCmd('cmd_list_environments', {
workspaceId: workspace.id,
})) as Environment[];
},
}).data ?? []
);
// Fetch new requests when workspace changes
useEffect(() => {
if (workspace == null) return;
invokeCmd<Environment[]>('cmd_list_environments', { workspaceId: workspace.id }).then(setItems);
}, [setItems, workspace]);
return items;
}

View File

@@ -13,6 +13,7 @@ export function useGrpcConnections(requestId: string | null) {
initialData: [],
queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
if (requestId == null) return [];
return (await invokeCmd('cmd_list_grpc_connections', {
requestId,
limit: 200,

View File

@@ -1,3 +1,4 @@
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
import { useKeyValue } from './useKeyValue';
export function protoFilesArgs(requestId: string | null) {
@@ -10,3 +11,11 @@ export function protoFilesArgs(requestId: string | null) {
export function useGrpcProtoFiles(activeRequestId: string | null) {
return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
}
export async function getGrpcProtoFiles(requestId: string) {
return getKeyValue<string[]>({ ...protoFilesArgs(requestId), fallback: [] });
}
export async function setGrpcProtoFiles(requestId: string, protoFiles: string[]) {
return setKeyValue<string[]>({ ...protoFilesArgs(requestId), value: protoFiles });
}

View File

@@ -1,24 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp/api';
import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
export function grpcRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['grpc_requests', { workspaceId }];
}
export const grpcRequestsAtom = atom<GrpcRequest[]>([]);
export function useGrpcRequests() {
const [items, setItems] = useAtom(grpcRequestsAtom);
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspace != null,
queryKey: grpcRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspace == null) return [];
return (await invokeCmd('cmd_list_grpc_requests', {
workspaceId: workspace.id,
})) as GrpcRequest[];
},
}).data ?? []
);
// Fetch new requests when workspace changes
useEffect(() => {
if (workspace == null) return;
invokeCmd<GrpcRequest[]>('cmd_list_grpc_requests', { workspaceId: workspace.id }).then(
setItems,
);
}, [setItems, workspace]);
return items;
}

View File

@@ -9,6 +9,7 @@ import { invokeCmd } from '../lib/tauri';
export function useHttpRequestActions() {
const httpRequestActions = useQuery({
queryKey: ['http_request_actions'],
refetchOnWindowFocus: false,
queryFn: async () => {
const responses = (await invokeCmd(
'cmd_http_request_actions',

View File

@@ -1,24 +1,21 @@
import { useQuery } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp/api';
import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
export function httpRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['http_requests', { workspaceId }];
}
export const httpRequestsAtom = atom<HttpRequest[]>([]);
export function useHttpRequests() {
const [items, setItems] = useAtom(httpRequestsAtom);
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspace != null,
queryKey: httpRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspace == null) return [];
return (await invokeCmd('cmd_list_http_requests', {
workspaceId: workspace.id,
})) as HttpRequest[];
},
}).data ?? []
);
useEffect(() => {
if (workspace == null) return;
invokeCmd<HttpRequest[]>('cmd_list_http_requests', { workspaceId: workspace.id }).then(
setItems,
);
}, [setItems, workspace]);
return items;
}

View File

@@ -13,6 +13,7 @@ export function useHttpResponses(requestId: string | null) {
initialData: [],
queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
if (requestId == null) return [];
return (await invokeCmd('cmd_list_http_responses', {
requestId,
limit: 200,

View File

@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import type { Tokens } from '../gen/Tokens';
import { invokeCmd } from '../lib/tauri';
export function useParseTemplate(template: string) {
return useQuery<Tokens>({
queryKey: ['parse_template', template],
queryFn: () => parseTemplate(template),
});
}
export async function parseTemplate(template: string): Promise<Tokens> {
return invokeCmd('cmd_parse_template', { template });
}

View File

@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
export function useRenderTemplate(template: string) {
const workspaceId = useActiveWorkspace()?.id ?? 'n/a';
const environmentId = useActiveEnvironment()[0]?.id ?? null;
return useQuery<string>({
placeholderData: (prev) => prev, // Keep previous data on refetch
queryKey: ['render_template', template],
queryFn: () => renderTemplate({ template, workspaceId, environmentId }),
});
}
export async function renderTemplate({
template,
workspaceId,
environmentId,
}: {
template: string;
workspaceId: string;
environmentId: string | null;
}): Promise<string> {
return invokeCmd('cmd_render_template', { template, workspaceId, environmentId });
}

View File

@@ -29,7 +29,7 @@ export function useSyncWorkspaceRequestTitle() {
newTitle += ` ${fallbackRequestName(activeRequest)}`;
}
if (appInfo?.isDev) {
if (appInfo.isDev) {
newTitle = `[DEV] ${newTitle}`;
}
@@ -40,5 +40,5 @@ export function useSyncWorkspaceRequestTitle() {
} else {
emit('yaak_title_changed', newTitle).catch(console.error);
}
}, [activeEnvironment, activeRequest, activeWorkspace, appInfo?.isDev, osInfo.osType]);
}, [activeEnvironment, activeRequest, activeWorkspace, appInfo.isDev, osInfo.osType]);
}

View File

@@ -0,0 +1,88 @@
import type { HttpRequest } from '@yaakapp/api';
export interface TemplateFunctionArgBase {
name: string;
optional?: boolean;
label?: string;
}
export interface TemplateFunctionSelectArg extends TemplateFunctionArgBase {
type: 'select';
defaultValue?: string;
options: readonly { name: string; value: string }[];
}
export interface TemplateFunctionTextArg extends TemplateFunctionArgBase {
type: 'text';
defaultValue?: string;
placeholder?: string;
}
export interface TemplateFunctionHttpRequestArg extends TemplateFunctionArgBase {
type: HttpRequest['model'];
}
export type TemplateFunctionArg =
| TemplateFunctionSelectArg
| TemplateFunctionTextArg
| TemplateFunctionHttpRequestArg;
export interface TemplateFunction {
name: string;
args: TemplateFunctionArg[];
}
export function useTemplateFunctions() {
const fns: TemplateFunction[] = [
{
name: 'timestamp',
args: [
{
type: 'text',
name: 'from',
label: 'From',
placeholder: '2023-23-12T04:03:03',
optional: true,
},
{
type: 'select',
label: 'Format',
name: 'format',
options: [
{ name: 'RFC3339', value: 'rfc3339' },
{ name: 'Unix', value: 'unix' },
{ name: 'Unix (ms)', value: 'unix_millis' },
],
optional: true,
defaultValue: 'RFC3339',
},
],
},
{
name: 'response',
args: [
{
type: 'http_request',
name: 'request',
label: 'Request',
},
{
type: 'select',
name: 'attribute',
label: 'Attribute',
options: [
{ name: 'Body', value: 'body' },
{ name: 'Header', value: 'header' },
],
},
{
type: 'text',
name: 'filter',
label: 'Filter',
placeholder: 'JSONPath or XPath expression',
},
],
},
];
return fns;
}

View File

@@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query';
import type { Tokens } from '../gen/Tokens';
import { invokeCmd } from '../lib/tauri';
export function useTemplateTokensToString(tokens: Tokens) {
return useQuery<string>({
placeholderData: (prev) => prev, // Keep previous data on refetch
queryKey: ['template_tokens_to_string', tokens],
queryFn: () => templateTokensToString(tokens),
});
}
export async function templateTokensToString(tokens: Tokens): Promise<string> {
return invokeCmd('cmd_template_tokens_to_string', { tokens });
}

View File

@@ -1,20 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import type { Workspace } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { atom, useAtomValue } from 'jotai';
import { listWorkspaces } from '../lib/store';
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/ban-types
export function workspacesQueryKey(_?: {}) {
return ['workspaces'];
}
const workspaces = await listWorkspaces();
export const workspacesAtom = atom<Workspace[]>(workspaces);
export function useWorkspaces() {
return (
useQuery({
queryKey: workspacesQueryKey(),
queryFn: async () => {
const workspaces = await invokeCmd('cmd_list_workspaces');
return workspaces as Workspace[];
},
}).data ?? []
);
return useAtomValue(workspacesAtom);
}