Skip to content

Commit

Permalink
refactor(frontend): replace react-hook-form with @tanstack/form (#524)
Browse files Browse the repository at this point in the history
refactor major management forms after #519 

### How to write a standard management form

#### Type Validator (Zod)

zod is a commonly used well-typed validating library.

Example for creating a zod type:

```ts
import { z } from zod;

const createEntitySchema = z.object({
  name: z.string().min(1),
  description: z.string(),
  is_default: z.boolean(),
});
```

There are two type utils to get the zod validator's type:

```ts
import { z } from zod;

type Input = z.input<typeof createEntitySchema>
type Output = z.infer<typeof createEntitySchema>

// Typing example
const input: Input = { ... };
const output = createEntitySchema.parse(input);

// output satisfies Output
```

#### @tanstack/form

See [documents](https://tanstack.com/form/latest)

#### Example 1. Create a form using `withCreateEntityForm` hoc

The withCreateEntityForm creates a component and type bound field layout
components.

```ts
import { withCreateEntityForm } from '@/components/form/create-entity-form';
import { FormInput, FormSwitch, FormTextarea } from '@/components/form/control-widget';

const Form = withCreateEntityForm(
  createEntitySchema, // The form schema
  async data => { // The submit handler
    const entity = await create(data);
    return entity;
  }
)

const form = (
  <Form 
    onCreated={entity => {
      startTransition(() => {
        router.push('/entities/${entity.id}');
      })
    }}
    transitioning={transitioning}
  >
    <Form.Basic name='name' label='Name'>
      <FormInput />
    </Form.Basic>
    <Form.Basic name='description' label='Description'>
      <FormTextarea />
    </Form.Basic>
    <Form.Contained name='is_default' label='Is Default' description='xxx...'>
      <FormSwitch />
    </Form.Contained>
  </Form>
)
```

#### Example 2. Create fully controlled complex form from scratch

See
[frontend/app/src/components/llm/CreateLLMForm.tsx](https://github.com/pingcap/tidb.ai/blob/afc16505e40625ab303d7b8d01eff78b48eaca1f/frontend/app/src/components/llm/CreateLLMForm.tsx)
  • Loading branch information
634750802 authored Dec 20, 2024
1 parent 6bfe56e commit 547789d
Show file tree
Hide file tree
Showing 55 changed files with 1,261 additions and 1,370 deletions.
2 changes: 1 addition & 1 deletion e2e/tests/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ test('Bootstrap', async ({ browser, page }) => {
await page.waitForSelector('[name=name]');
await page.fill('input[name=name]', 'My Knowledge Base');
await page.fill('textarea[name=description]', 'This is E2E Knowledge Base.');
await page.getByRole('button', { name: 'Submit', exact: true }).click();
await page.getByRole('button', { name: 'Create', exact: true }).click();

await page.waitForURL(/\/knowledge-bases\/1\/data-sources/);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/src/api/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const zDate = z.coerce.date().or(z.literal('').transform(() => undefined)).optio
export const listDocumentsFiltersSchema = z.object({
name: z.string().optional(),
source_uri: z.string().optional(),
knowledge_base_id: z.coerce.number().optional(),
knowledge_base_id: z.number().optional(),
created_at_start: zDate,
created_at_end: zDate,
updated_at_start: zDate,
Expand Down
1 change: 1 addition & 0 deletions frontend/app/src/api/embedding-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface CreateEmbeddingModel {
name: string;
provider: string;
model: string;
vector_dimension: number;
config?: any;
credentials: string | object;
}
Expand Down
16 changes: 11 additions & 5 deletions frontend/app/src/api/evaluations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export interface EvaluationTask {
created_at: Date;
updated_at: Date;
dataset_id: number;
}

export interface EvaluationTaskWithSummary extends EvaluationTask {
summary: EvaluationTaskSummary;
}

Expand Down Expand Up @@ -141,9 +144,12 @@ const evaluationTaskSchema = z.object({
created_at: zodJsonDate(),
updated_at: zodJsonDate(),
dataset_id: z.number(),
summary: evaluationTaskSummarySchema,
}) satisfies ZodType<EvaluationTask, any, any>;

const evaluationTaskWithSummarySchema = evaluationTaskSchema.extend({
summary: evaluationTaskSummarySchema,
});

const evaluationTaskItemSchema = z.object({
created_at: zodJsonDate(),
updated_at: zodJsonDate(),
Expand Down Expand Up @@ -276,18 +282,18 @@ export async function createEvaluationTask (params: CreateEvaluationTaskParams):
.then(handleResponse(evaluationTaskSchema));
}

export async function listEvaluationTasks ({ ...params }: PageParams & { keyword?: string }): Promise<Page<EvaluationTask>> {
export async function listEvaluationTasks ({ ...params }: PageParams & { keyword?: string }): Promise<Page<EvaluationTaskWithSummary>> {
return fetch(requestUrl('/api/v1/admin/evaluation/tasks', params), {
headers: await authenticationHeaders(),
})
.then(handleResponse(zodPage(evaluationTaskSchema)));
.then(handleResponse(zodPage(evaluationTaskWithSummarySchema)));
}

export async function getEvaluationTask (id: number): Promise<EvaluationTask> {
export async function getEvaluationTaskWithSummary (id: number): Promise<EvaluationTaskWithSummary> {
return fetch(requestUrl(`/api/v1/admin/evaluation/tasks/${id}/summary`), {
headers: await authenticationHeaders(),
})
.then(handleResponse(evaluationTaskSchema));
.then(handleResponse(evaluationTaskWithSummarySchema));
}

export async function cancelEvaluationTask (id: number): Promise<void> {
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/src/api/rerankers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface RerankerOption extends ProviderOption {
default_top_n: number;
}

export interface CreateRERANKER {
export interface CreateReranker {
name: string;
provider: string;
model: string;
Expand Down Expand Up @@ -71,7 +71,7 @@ export async function getReranker (id: number): Promise<Reranker> {
}).then(handleResponse(rerankerSchema));
}

export async function createReranker (create: CreateRERANKER) {
export async function createReranker (create: CreateReranker) {
return await fetch(requestUrl(`/api/v1/admin/reranker-models`), {
method: 'POST',
body: JSON.stringify(create),
Expand All @@ -89,10 +89,10 @@ export async function deleteReranker (id: number) {
}).then(handleErrors);
}

export async function testReranker (createRERANKER: CreateRERANKER) {
export async function testReranker (createReranker: CreateReranker) {
return await fetch(requestUrl(`/api/v1/admin/reranker-models/test`), {
method: 'POST',
body: JSON.stringify(createRERANKER),
body: JSON.stringify(createReranker),
headers: {
'Content-Type': 'application/json',
...await authenticationHeaders(),
Expand Down
44 changes: 13 additions & 31 deletions frontend/app/src/components/api-keys/CreateApiKeyForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { createApiKey, type CreateApiKey, type CreateApiKeyResponse } from '@/api/api-keys';
import { createApiKey, type CreateApiKeyResponse } from '@/api/api-keys';
import { FormInput } from '@/components/form/control-widget';
import { FormFieldBasicLayout } from '@/components/form/field-layout';
import { FormSubmit } from '@/components/form/submit';
import { handleSubmitHelper } from '@/components/form/utils';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { withCreateEntityForm } from '@/components/form/create-entity-form';
import { z } from 'zod';

const schema = z.object({
Expand All @@ -16,30 +11,17 @@ export interface CreateApiKeyFormProps {
onCreated?: (data: CreateApiKeyResponse) => void;
}

export function CreateApiKeyForm ({ onCreated }: CreateApiKeyFormProps) {
const form = useForm<CreateApiKey>({
resolver: zodResolver(schema),
defaultValues: {
description: '',
},
});
const FormImpl = withCreateEntityForm(schema, createApiKey, {
submitTitle: 'Create API Key',
submittingTitle: 'Creating API Key...',
});

export function CreateApiKeyForm ({ onCreated }: CreateApiKeyFormProps) {
return (
<Form {...form}>
<form
className="space-y-4"
onSubmit={handleSubmitHelper(form, async data => {
const response = await createApiKey(data);
onCreated?.(response);
})}
>
<FormFieldBasicLayout name="description" label="API Key Description">
<FormInput />
</FormFieldBasicLayout>
<FormSubmit submittingText="Creating API Key...">
Create API Key
</FormSubmit>
</form>
</Form>
<FormImpl onCreated={onCreated}>
<FormImpl.Basic name="description" label="API Key Description">
<FormInput />
</FormImpl.Basic>
</FormImpl>
);
}
}
Loading

0 comments on commit 547789d

Please sign in to comment.