diff --git a/ui/src/mocks/volumes-mock.ts b/ui/src/mocks/volumes-mock.ts index 267586257..c633f926b 100644 --- a/ui/src/mocks/volumes-mock.ts +++ b/ui/src/mocks/volumes-mock.ts @@ -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', + }, ]; diff --git a/ui/src/modules/shared/create-volume-dialog/create-volume-dialog-form.tsx b/ui/src/modules/shared/create-volume-dialog/create-volume-dialog-form.tsx index 6eb228df0..e72226361 100644 --- a/ui/src/modules/shared/create-volume-dialog/create-volume-dialog-form.tsx +++ b/ui/src/modules/shared/create-volume-dialog/create-volume-dialog-form.tsx @@ -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, @@ -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() === '')) { @@ -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 { @@ -90,7 +125,7 @@ const TypeOption: React.FC = ({ }; interface CreateVolumeDialogForm { - onSubmit: (data: z.infer) => void; + onSubmit: (data: VolumeCreatePayload) => void; } export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) => { @@ -100,6 +135,10 @@ export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) => name: '', type: 'file', path: '', + bucket: '', + endpoint: '', + awsAccessKeyId: '', + awsSecretAccessKey: '', }, }); @@ -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 = [ @@ -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) => { + 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 (
title={option.title} description={option.description} selectedValue={field.value} - disabled={option.disabled} radioControl={ - + } /> @@ -206,6 +290,62 @@ export const CreateVolumeDialogForm = ({ onSubmit }: CreateVolumeDialogForm) => )} /> )} + {type === 's3' && ( + <> + ( + + Bucket + + + + + + )} + /> + ( + + Endpoint + + + + + + )} + /> + ( + + AWS Access Key ID + + + + + + )} + /> + ( + + AWS Secret Access Key + + + + + + )} + /> + + )}
); diff --git a/ui/src/modules/shared/create-volume-dialog/create-volume-dialog.tsx b/ui/src/modules/shared/create-volume-dialog/create-volume-dialog.tsx index 4fd700f54..d7ef895ee 100644 --- a/ui/src/modules/shared/create-volume-dialog/create-volume-dialog.tsx +++ b/ui/src/modules/shared/create-volume-dialog/create-volume-dialog.tsx @@ -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'; @@ -53,7 +52,7 @@ export function CreateVolumeDialog({ opened, onSetOpened }: CreateVolumeDialogPr )} { - mutate({ data: formData as VolumeCreatePayload }); + mutate({ data: formData }); }} /> diff --git a/ui/ui_openapi.json b/ui/ui_openapi.json index 4239f7cb3..05756f5bd 100644 --- a/ui/ui_openapi.json +++ b/ui/ui_openapi.json @@ -1518,8 +1518,8 @@ "worksheetId": null, "query": "CREATE TABLE test(a INT);", "context": { - "database": "my_database", - "schema": "public" + "schema": "public", + "database": "my_database" } } }, @@ -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"], @@ -2599,6 +2666,9 @@ "QueryCreateResponse": { "$ref": "#/components/schemas/QueryRecord" }, + "QueryGetResponse": { + "$ref": "#/components/schemas/QueryRecord" + }, "QueryRecord": { "type": "object", "required": [