Skip to content

[UI] Volumes page - enable creating s3 volumes #1265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ui/src/mocks/volumes-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ export const VOLUMES_MOCK: Volume[] = [
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
},
{
type: 's3',
name: 's3-volume',
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { cn } from '@/lib/utils';
import type { FileVolume, S3Volume, VolumeCreatePayload } from '@/orval/models';
import {
VolumeTypeOneOfAllOfType,
VolumeTypeOneOfFourAllOfType,
Expand All @@ -34,6 +35,10 @@ const schema = z
VolumeTypeOneOfFourAllOfType.s3Tables,
]),
path: z.string().optional(),
bucket: z.string().optional(),
endpoint: z.string().optional(),
awsAccessKeyId: z.string().optional(),
awsSecretAccessKey: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.type === 'file' && (!data.path || data.path.trim() === '')) {
Expand All @@ -43,6 +48,36 @@ const schema = z
path: ['path'],
});
}
if (data.type === 's3') {
if (!data.bucket || data.bucket.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Bucket is required for S3 Volume',
path: ['bucket'],
});
}
if (!data.endpoint || data.endpoint.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Endpoint is required for S3 Volume',
path: ['endpoint'],
});
}
if (!data.awsAccessKeyId || data.awsAccessKeyId.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'AWS Access Key ID is required for S3 Volume',
path: ['awsAccessKeyId'],
});
}
if (!data.awsSecretAccessKey || data.awsSecretAccessKey.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'AWS Secret Access Key is required for S3 Volume',
path: ['awsSecretAccessKey'],
});
}
}
});

interface TypeOptionProps {
Expand Down Expand Up @@ -90,7 +125,7 @@ const TypeOption: React.FC<TypeOptionProps> = ({
};

interface CreateVolumeDialogForm {
onSubmit: (data: z.infer<typeof schema>) => void;
onSubmit: (data: VolumeCreatePayload) => void;
}

export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) => {
Expand All @@ -100,6 +135,10 @@ export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) =>
name: '',
type: 'file',
path: '',
bucket: '',
endpoint: '',
awsAccessKeyId: '',
awsSecretAccessKey: '',
},
});

Expand All @@ -109,6 +148,9 @@ export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) =>
if (type !== 'file') {
form.clearErrors('path');
}
if (type !== 's3') {
form.clearErrors(['bucket', 'endpoint', 'awsAccessKeyId', 'awsSecretAccessKey']);
}
}, [type, form]);

const volumeOptions = [
Expand All @@ -129,15 +171,62 @@ export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) =>
value: 's3',
title: 'S3 Volume',
description: 'Cloud-based storage with AWS S3.',
disabled: true,
},
];

const onFormSubmit = (data: z.infer<typeof schema>) => {
switch (data.type) {
case 's3': {
const s3VolumeData: S3Volume = {
bucket: data.bucket,
endpoint: data.endpoint,
credentials: {
accessKey: {
awsAccessKeyId: data.awsAccessKeyId ?? '',
awsSecretAccessKey: data.awsSecretAccessKey ?? '',
},
},
};
const volumeData: VolumeCreatePayload = {
name: data.name,
type: data.type,
...s3VolumeData,
};
onSubmit(volumeData);
break;
}
case 'file': {
const fileVolumeData: FileVolume = {
path: data.path ?? '',
};
const volumeData: VolumeCreatePayload = {
name: data.name,
type: data.type,
...fileVolumeData,
};
onSubmit(volumeData);
break;
}
case 'memory': {
// TODO: Implement memory volume
const memoryVolumeData: VolumeCreatePayload = {
name: data.name,
type: data.type,
};
onSubmit(memoryVolumeData);
break;
}
default: {
throw new Error(`Unsupported volume type: ${data.type}`);
}
}
};

return (
<FormProvider {...form}>
<form
id="createVolumeDialogForm"
onSubmit={form.handleSubmit(onSubmit)}
onSubmit={form.handleSubmit(onFormSubmit)}
className="flex flex-col gap-4"
>
<FormField
Expand Down Expand Up @@ -173,14 +262,9 @@ export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) =>
title={option.title}
description={option.description}
selectedValue={field.value}
disabled={option.disabled}
radioControl={
<FormControl>
<RadioGroupItem
value={option.value}
id={option.id}
disabled={option.disabled}
/>
<RadioGroupItem value={option.value} id={option.id} />
</FormControl>
}
/>
Expand All @@ -206,6 +290,62 @@ export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) =>
)}
/>
)}
{type === 's3' && (
<>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input {...field} required />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint</FormLabel>
<FormControl>
<Input {...field} required />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="awsAccessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>AWS Access Key ID</FormLabel>
<FormControl>
<Input {...field} required />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="awsSecretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>AWS Secret Access Key</FormLabel>
<FormControl>
<Input {...field} type="password" required />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</form>
</FormProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { getGetDashboardQueryKey } from '@/orval/dashboard';
import type { VolumeCreatePayload } from '@/orval/models';
import { getGetVolumesQueryKey, useCreateVolume } from '@/orval/volumes';

import { CreateVolumeDialogForm } from './create-volume-dialog-form';
Expand Down Expand Up @@ -53,7 +52,7 @@ export function CreateVolumeDialog({ opened, onSetOpened }: CreateVolumeDialogPr
)}
<CreateVolumeDialogForm
onSubmit={(formData) => {
mutate({ data: formData as VolumeCreatePayload });
mutate({ data: formData });
}}
/>
<DialogFooter>
Expand Down
74 changes: 72 additions & 2 deletions ui/ui_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1518,8 +1518,8 @@
"worksheetId": null,
"query": "CREATE TABLE test(a INT);",
"context": {
"database": "my_database",
"schema": "public"
"schema": "public",
"database": "my_database"
}
}
},
Expand Down Expand Up @@ -1597,6 +1597,73 @@
}
}
},
"/ui/queries/{queryRecordId}": {
"get": {
"tags": ["queries"],
"operationId": "getQuery",
"parameters": [
{
"name": "queryRecordId",
"in": "path",
"description": "Query Record Id",
"required": true,
"schema": {
"$ref": "#/components/schemas/i64"
}
}
],
"responses": {
"200": {
"description": "Returns result of the query",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueryGetResponse"
}
}
}
},
"400": {
"description": "Bad query record id",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"headers": {
"WWW-Authenticate": {
"schema": {
"type": "string"
},
"description": "Bearer authentication scheme with error details"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/ui/volumes": {
"get": {
"tags": ["volumes"],
Expand Down Expand Up @@ -2599,6 +2666,9 @@
"QueryCreateResponse": {
"$ref": "#/components/schemas/QueryRecord"
},
"QueryGetResponse": {
"$ref": "#/components/schemas/QueryRecord"
},
"QueryRecord": {
"type": "object",
"required": [
Expand Down