-
Notifications
You must be signed in to change notification settings - Fork 60
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
Dataset preview data #143
Dataset preview data #143
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,88 @@ | ||
'use server' | ||
|
||
import { DatasetsRepository } from '@latitude-data/core/repositories' | ||
import { createDataset } from '@latitude-data/core/services/datasets/create' | ||
import disk from '$/lib/disk' | ||
import { z } from 'zod' | ||
|
||
import { authProcedure } from '../procedures' | ||
|
||
const DELIMITERS_KEYS = [ | ||
'comma', | ||
'semicolon', | ||
'tab', | ||
'space', | ||
'custom', | ||
] as const | ||
const DELIMITER_VALUES = { | ||
comma: ',', | ||
semicolon: ';', | ||
tab: '\t', | ||
space: ' ', | ||
} | ||
|
||
const MAX_SIZE = 3 | ||
const MAX_UPLOAD_SIZE_IN_MB = 3 * 1024 * 1024 | ||
|
||
export const createDatasetAction = authProcedure | ||
.createServerAction() | ||
.input( | ||
z.object({ | ||
name: z.string().min(1, { message: 'Name is required' }), | ||
dataset_file: z | ||
.instanceof(File) | ||
.refine((file) => { | ||
return !file || file.size <= MAX_UPLOAD_SIZE_IN_MB | ||
}, `Your dataset must be less than ${MAX_SIZE}MB in size`) | ||
async ({ ctx }) => { | ||
return z | ||
.object({ | ||
name: z | ||
.string() | ||
.min(1, { message: 'Name is required' }) | ||
.refine( | ||
async (name) => { | ||
const scope = new DatasetsRepository(ctx.workspace.id) | ||
const existing = await scope.findByName(name) | ||
return !existing | ||
}, | ||
{ | ||
message: | ||
'This name was already used, plese use something different', | ||
}, | ||
), | ||
csvDelimiter: z.enum(DELIMITERS_KEYS, { | ||
message: 'Choose a valid delimiter option', | ||
}), | ||
csvCustomDelimiter: z.string(), | ||
Comment on lines
+46
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's ok, we always send an empty string because the field is always sent. Is in the validation that we check user picked custom and this field has content |
||
dataset_file: z | ||
.instanceof(File) | ||
.refine((file) => { | ||
return !file || file.size <= MAX_UPLOAD_SIZE_IN_MB | ||
}, `Your dataset must be less than ${MAX_SIZE}MB in size`) | ||
.refine( | ||
(file) => file.type === 'text/csv', | ||
'Your dataset must be a CSV file', | ||
), | ||
}) | ||
.refine( | ||
(file) => file.type === 'text/csv', | ||
'Your dataset must be a CSV file', | ||
), | ||
}), | ||
(schema) => { | ||
if (schema.csvDelimiter !== 'custom') return true | ||
return schema.csvCustomDelimiter.length > 0 | ||
}, | ||
{ | ||
message: 'Custom delimiter is required', | ||
path: ['csvCustomDelimiter'], | ||
}, | ||
) | ||
}, | ||
{ type: 'formData' }, | ||
) | ||
.handler(async ({ input, ctx }) => { | ||
const csvDelimiter = | ||
input.csvDelimiter === 'custom' | ||
? input.csvCustomDelimiter | ||
: DELIMITER_VALUES[input.csvDelimiter] | ||
return createDataset({ | ||
workspace: ctx.workspace, | ||
author: ctx.user, | ||
disk: disk, | ||
data: { | ||
name: input.name, | ||
file: input.dataset_file, | ||
// TODO: Make UI radio button to pick delimiter | ||
csvDelimiter: ';', | ||
csvDelimiter, | ||
}, | ||
}).then((r) => r.unwrap()) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
'use server' | ||
|
||
import { DatasetsRepository } from '@latitude-data/core/repositories' | ||
import { previewDataset } from '@latitude-data/core/services/datasets/preview' | ||
import disk from '$/lib/disk' | ||
import { z } from 'zod' | ||
|
||
import { authProcedure } from '../procedures' | ||
|
||
export const previewDatasetAction = authProcedure | ||
.createServerAction() | ||
.input( | ||
z.object({ | ||
id: z.number(), | ||
}), | ||
) | ||
.handler(async ({ ctx, input }) => { | ||
const repo = new DatasetsRepository(ctx.workspace.id) | ||
const dataset = await repo.find(input.id).then((r) => r.unwrap()) | ||
return await previewDataset({ dataset, disk }).then((r) => r.unwrap()) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { Dataset } from '@latitude-data/core/browser' | ||
|
||
import '@latitude-data/web-ui' | ||
|
||
import { | ||
Button, | ||
Modal, | ||
ModalTrigger, | ||
ReactStateDispatch, | ||
Table, | ||
TableBody, | ||
TableCell, | ||
TableHead, | ||
TableHeader, | ||
TableRow, | ||
TableSkeleton, | ||
Text, | ||
} from '@latitude-data/web-ui' | ||
import useDatasetPreview from '$/stores/datasetPreviews' | ||
|
||
const VISIBLE_ROWS = 20 | ||
function PreviewModal({ | ||
dataset, | ||
setPreview, | ||
}: { | ||
dataset: Dataset | ||
setPreview: ReactStateDispatch<Dataset | null> | ||
}) { | ||
const { data, isLoading } = useDatasetPreview({ dataset }) | ||
const rows = data?.rows ?? [] | ||
const rowCount = Math.min(dataset.fileMetadata.rowCount, VISIBLE_ROWS) | ||
return ( | ||
<Modal | ||
size='large' | ||
open | ||
title={`${dataset.name} preview`} | ||
description='First 100 rows of the dataset' | ||
onOpenChange={(open: boolean) => !open && setPreview(null)} | ||
footer={ | ||
<ModalTrigger asChild> | ||
<Button fancy variant='outline'> | ||
Go back | ||
</Button> | ||
</ModalTrigger> | ||
} | ||
> | ||
{isLoading ? ( | ||
<TableSkeleton | ||
rows={rowCount} | ||
cols={dataset.fileMetadata.headers.length} | ||
/> | ||
) : ( | ||
<Table maxHeight={450}> | ||
<TableHeader> | ||
<TableRow verticalPadding> | ||
<TableHead> | ||
<Text.H4>#</Text.H4> | ||
</TableHead> | ||
{data.headers.map((header, i) => ( | ||
<TableHead key={`${header}-${i}`}> | ||
<Text.H4>{header}</Text.H4> | ||
</TableHead> | ||
))} | ||
</TableRow> | ||
</TableHeader> | ||
<TableBody> | ||
{rows.map((row, rowIndex) => { | ||
return ( | ||
<TableRow key={rowIndex} verticalPadding> | ||
{row.map((cell, cellIndex) => ( | ||
<TableCell key={cellIndex}>{cell}</TableCell> | ||
))} | ||
</TableRow> | ||
) | ||
})} | ||
</TableBody> | ||
</Table> | ||
)} | ||
</Modal> | ||
) | ||
} | ||
|
||
export default function PreviewDatasetModal({ | ||
dataset, | ||
setPreview, | ||
}: { | ||
dataset: Dataset | null | ||
setPreview: ReactStateDispatch<Dataset | null> | ||
}) { | ||
if (!dataset) return null | ||
|
||
return <PreviewModal dataset={dataset} setPreview={setPreview} /> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
'use client' | ||
|
||
import { useCallback, useRef, useState } from 'react' | ||
|
||
import { cn, FormFieldGroup, Input, Select } from '@latitude-data/web-ui' | ||
|
||
export enum DelimiterEnum { | ||
Comma = 'comma', | ||
Semicolon = 'semicolon', | ||
Tab = 'tab', | ||
Space = 'space', | ||
Custom = 'custom', | ||
} | ||
|
||
const DELIMITERS = [ | ||
{ value: DelimiterEnum.Comma, label: 'Comma (ex.: column1,column2,column3)' }, | ||
{ | ||
value: DelimiterEnum.Semicolon, | ||
label: 'Semicolon (ex.: column1;column2;column3)', | ||
}, | ||
{ | ||
value: DelimiterEnum.Tab, | ||
label: 'Tab (ex.: column1 \\t column2 \\t column3)', | ||
}, | ||
{ value: DelimiterEnum.Space, label: 'Space (ex.: column1 column2 column3)' }, | ||
{ value: DelimiterEnum.Custom, label: 'Custom' }, | ||
] | ||
export const DELIMITER_KEYS = DELIMITERS.map(({ value }) => value) | ||
|
||
export default function DelimiterSelector({ | ||
delimiterInputName, | ||
customDelimiterInputName, | ||
delimiterErrors, | ||
customDelimiterErrors, | ||
delimiterValue, | ||
customDelimiterValue, | ||
}: { | ||
delimiterInputName: string | ||
delimiterValue: string | undefined | ||
delimiterErrors: string[] | undefined | ||
customDelimiterInputName: string | ||
customDelimiterValue: string | ||
customDelimiterErrors: string[] | undefined | ||
}) { | ||
const inputRef = useRef<HTMLInputElement>(null) | ||
const [isCustom, setIsCustom] = useState(false) | ||
const onSelectChange = useCallback((value: string) => { | ||
const custom = value === 'custom' | ||
setIsCustom(custom) | ||
setTimeout(() => { | ||
if (!custom) return | ||
inputRef.current?.focus() | ||
}, 0) | ||
}, []) | ||
return ( | ||
<FormFieldGroup | ||
label='CSV header delimiter' | ||
description='Specify the delimiter used in the first line of the uploaded .csv file.' | ||
> | ||
<Select | ||
name={delimiterInputName} | ||
options={DELIMITERS} | ||
defaultValue={delimiterValue} | ||
onChange={onSelectChange} | ||
errors={delimiterErrors} | ||
/> | ||
<Input | ||
ref={inputRef} | ||
name={customDelimiterInputName} | ||
className={cn({ hidden: !isCustom })} | ||
defaultValue={customDelimiterValue} | ||
errors={customDelimiterErrors} | ||
placeholder='Your custom delimiter' | ||
/> | ||
</FormFieldGroup> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking but use
MAX_SIZE
to calculateMAX_SIZE_IN_MB
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No good catch 💯 . This needs to be changed to 15MB now. I forgot thanks