Skip to content

Commit

Permalink
csv
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed Apr 30, 2024
1 parent cf65b18 commit d89c900
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 20 deletions.
1 change: 1 addition & 0 deletions packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@vitejs/plugin-react": "^4.2.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"csv-stringify": "^6.4.6",
"eslint-plugin-mmkal": "0.5.0",
"express": "^4.18.2",
"fetchomatic": "^0.0.2",
Expand Down
13 changes: 8 additions & 5 deletions packages/admin/src/client/sql-codemirror.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {type CompletionSource, acceptCompletion, autocompletion} from '@codemirror/autocomplete'
import {sql} from '@codemirror/lang-sql'
import {linter, lintGutter} from '@codemirror/lint'
import {EditorState, Prec} from '@codemirror/state'
import {type EditorView, keymap} from '@codemirror/view'
import {EditorState, Extension, Prec} from '@codemirror/state'
import {EditorView, keymap} from '@codemirror/view'
import CodeMirror from '@uiw/react-codemirror'
import clsx from 'clsx'
import React from 'react'
import {useMeasure} from 'react-use'
import {SuggestionType, getSuggester} from '../packlets/autocomplete/suggest'
Expand All @@ -16,6 +17,7 @@ export interface SqlCodeMirrorProps {
errors?: Array<{position: number; message: string}>
height: string
readonly?: boolean
wrapText?: boolean
}

type MeasuredCodeMirrorProps = Omit<SqlCodeMirrorProps, 'height'> & {
Expand All @@ -26,7 +28,7 @@ type MeasuredCodeMirrorProps = Omit<SqlCodeMirrorProps, 'height'> & {
export const MeasuredCodeMirror = (props: MeasuredCodeMirrorProps) => {
const [ref, measurements] = useMeasure<HTMLDivElement>()
return (
<div className={props.className || 'h-full'} ref={ref}>
<div className={clsx('h-full', props.className)} ref={ref}>
<SqlCodeMirror {...props} height={measurements.height + 'px'} />
</div>
)
Expand Down Expand Up @@ -77,12 +79,13 @@ export const SqlCodeMirror = ({code, onChange, onExecute, errors, height, ...pro
})
})

const baseExtensions = [
const baseExtensions: Extension[] = [
keymapExtension,
sql(),
linterExtension,
lintGutter(),
EditorState.readOnly.of(props.readonly || false),
props.wrapText ? EditorView.lineWrapping : [],
]
if (!schema || !searchPath) {
return baseExtensions
Expand Down Expand Up @@ -118,7 +121,7 @@ export const SqlCodeMirror = ({code, onChange, onExecute, errors, height, ...pro
closeOnBlur: false,
})
return [...baseExtensions, dbAutocompletion]
}, [schema, onExecute, errors, searchPath])
}, [schema, onExecute, errors, searchPath, props.wrapText])

return (
<CodeMirror
Expand Down
4 changes: 2 additions & 2 deletions packages/admin/src/client/views/Migrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ const useMigrations = () => {
const down = useDestructive(trpc.migrations.down.useMutation(mutationConfig), 'Are you sure?', {
description: (
<>
<div className="mb-3">This may delete data, which will not be restored even if you reapply the migration.</div>
<div className="mb-3">This may delete data which will not be restored even if you reapply the migration.</div>
<ZForm
schema={z.object({
dontShowAgain: z.boolean().field({
label: 'Do not show this warning again (this can always be changed in settings)',
label: "Don't show this warning again (can be changed later in settings)",
}),
})}
onTouch={value =>
Expand Down
21 changes: 18 additions & 3 deletions packages/admin/src/client/views/Querier.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const Querier = () => {
const [ref, mes] = useMeasure<HTMLDivElement>()
const [storedCode = '', setStoredCode] = useLocalStorage(`sql-editor-code:0.0.1`, `show search_path`)
const [wrapText, setWrapText] = useLocalStorage(`sql-editor-wrap-text:0.0.1`, true)
const fileMutation = trpc.csv.useMutation()
const settings = useSettings()
const execute = trpc.executeSql.useMutation({
onSuccess: data => {
Expand Down Expand Up @@ -102,12 +103,26 @@ export const Querier = () => {
errors={errors}
onChange={setStoredCode}
onExecute={query => execute.mutate({query})}
wrapText={wrapText}
/>
<div className="absolute bottom-2 right-2">
<Button onClick={() => setWrapText(old => !old)} className="text-gray-100" size="sm" variant="ghost">
<div className="absolute bottom-2 right-2 flex gap-1">
<Button onClick={() => setWrapText(old => !old)} className="text-gray-100" size="sm">
<icons.RemoveFormatting className="w-4 h-4 text-gray-100" />
</Button>
<Button className="text-gray-100" size="sm" variant="ghost">
<Button
className="text-gray-100"
size="sm"
onClick={async () => {
const {csv} = await fileMutation.mutateAsync({query: storedCode})
const blob = new Blob([csv], {type: 'text/csv'})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
// a.download = `query.csv`
a.click()
URL.revokeObjectURL(url)
}}
>
<icons.Download className="w-4 h-4 text-gray-100" />
</Button>
</div>
Expand Down
50 changes: 40 additions & 10 deletions packages/admin/src/client/views/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@ export const Table = ({identifier}: {identifier: string}) => {
const [offset, setOffset] = React.useState(0)
const {data: {inspected} = {}} = trpc.inspect.useQuery({})
const rowsMutation = trpc.executeSql.useMutation()
const addMutation = trpc.executeSql.useMutation()
const addMutation = trpc.executeSql.useMutation({
onSuccess: () => setColumns(columns.slice()),
})
const fileMutation = trpc.csv.useMutation()
const prevEnabled = offset > 0
const values = rowsMutation.data?.results[0].result || []
const nextEnabled = Boolean(limit) && values.length >= limit

const columnNames = Object.values(inspected?.tables[identifier]?.columns || {}).map(c => c.name)
const tableInfo = inspected?.tables[identifier]
const columnNames = Object.values(tableInfo?.columns || {}).map(c => c.name)
const [whereClause, setWhereClause] = React.useState('')
const [columns, setColumns] = React.useState<string[]>(['*'])

const query = React.useMemo(() => {
const baseQuery = React.useMemo(() => {
if (identifier && inspected && identifier in inspected.tables) {
const table = identifier
for (const name of [table, ...columns]) {
Expand All @@ -52,16 +56,22 @@ export const Table = ({identifier}: {identifier: string}) => {
return `
with
counts as (select count(1) from ${table}),
dummy as (select 1 from counts where count = 0)
dummy as (select 1 from counts where count = 0 and ${Math.random()} != 2)
select ${columns.map(c => `t.${c}`).join(', ')} from ${table} t
full outer join dummy on true
${whereClause ? `where ${whereClause}` : ''}
limit ${Number(limit)}
offset ${Number(offset)}
`
}
return null
}, [identifier, inspected, limit, offset, whereClause, columns])
}, [identifier, inspected, whereClause, columns])

const query = React.useMemo(() => {
return `
${baseQuery}
limit ${Number(limit)}
offset ${Number(offset)}
`
}, [baseQuery, limit, offset])

React.useEffect(() => {
if (query) {
Expand Down Expand Up @@ -119,7 +129,11 @@ export const Table = ({identifier}: {identifier: string}) => {
schema={z.object(Object.fromEntries(columnNames.map(c => [c, z.string().optional()])))}
onSubmit={data =>
addMutation.mutate({
query: `insert into ${identifier} (${Object.keys(data).join(', ')}) values (${Object.values(data).join(', ')})`,
query: `
insert into ${identifier} (${Object.keys(data).join(', ')})
values (${Object.values(data).join(', ')})
returning now()
`,
})
}
title="Columns"
Expand All @@ -136,9 +150,25 @@ export const Table = ({identifier}: {identifier: string}) => {
>
<icons.RefreshCcw className="w-4 h-4" />
</Button>
<Button title="Download" className="" size="sm">
<PopoverZFormButton
schema={z.object({limit: z.number().optional()})}
title="Download"
className=""
size="sm"
onSubmit={async data => {
const limitedQuery = `${baseQuery}\n${data.limit ? `limit ${data.limit}` : ''}`
const {csv} = await fileMutation.mutateAsync({query: limitedQuery})
const blob = new Blob([csv], {type: 'text/csv'})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${tableInfo?.name || identifier}.csv`
a.click()
URL.revokeObjectURL(url)
}}
>
<icons.Download className="w-4 h-4" />
</Button>
</PopoverZFormButton>
</div>
</div>
<div className="relative h-[calc(100%-200px)] max-w-[100%] overlow-scroll border-white-1">
Expand Down
13 changes: 13 additions & 0 deletions packages/admin/src/server/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {sql} from '@pgkit/client'
import * as migra from '@pgkit/migra'
import {PostgreSQL} from '@pgkit/schemainspect'
import {stringify as csvStringify} from 'csv-stringify/sync'
import {fetchomatic} from 'fetchomatic'
import {z} from 'zod'
import {migrationsRotuer} from './migrations.js'
Expand All @@ -20,6 +21,18 @@ export const appRouter = trpc.router({
results: await runQuery(input.query, ctx),
}
}),
csv: publicProcedure
.input(
z.object({
query: z.string(),
}),
)
.mutation(async ({input, ctx}) => {
const client = ctx.connection
const {rows} = await client.query(sql.raw(input.query))
const csv = csvStringify(rows, {header: true})
return {csv}
}),
execute2: publicProcedure
.input(
z.object({
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d89c900

Please sign in to comment.