From f49474a893ce9321e2c1edda37d64cc0caac5063 Mon Sep 17 00:00:00 2001 From: Maksim Chervonnyi Date: Mon, 30 Sep 2024 19:43:39 +0200 Subject: [PATCH] wip: redesign tabulator tables list --- .../app/containers/Admin/Buckets/Buckets.tsx | 2 +- .../containers/Admin/Buckets/Tabulator.tsx | 1050 +++++++++++------ 2 files changed, 722 insertions(+), 330 deletions(-) diff --git a/catalog/app/containers/Admin/Buckets/Buckets.tsx b/catalog/app/containers/Admin/Buckets/Buckets.tsx index 600aa1bc4cf..ee2fdd48fcf 100644 --- a/catalog/app/containers/Admin/Buckets/Buckets.tsx +++ b/catalog/app/containers/Admin/Buckets/Buckets.tsx @@ -1707,7 +1707,7 @@ function EditPage({ back }: EditPageProps) { try { const { bucketUpdate: r } = await update({ name: bucket.name, input }) if (r.__typename !== 'BucketUpdateSuccess') { - // TS infered shape but not the actual type + // Generated `InputError` lacks optional properties and not infered correctly return r as Exclude< Model.GQLTypes.BucketUpdateResult, Model.GQLTypes.BucketUpdateSuccess diff --git a/catalog/app/containers/Admin/Buckets/Tabulator.tsx b/catalog/app/containers/Admin/Buckets/Tabulator.tsx index 0e714ab58ab..795f7d293f7 100644 --- a/catalog/app/containers/Admin/Buckets/Tabulator.tsx +++ b/catalog/app/containers/Admin/Buckets/Tabulator.tsx @@ -3,11 +3,11 @@ import * as FF from 'final-form' import * as React from 'react' import * as RF from 'react-final-form' import * as M from '@material-ui/core' -import { fade } from '@material-ui/core/styles' +// import { fade } from '@material-ui/core/styles' import tabulatorTableSchema from 'schemas/tabulatorTable.yml.json' -import { useConfirm } from 'components/Dialog' +// import { useConfirm } from 'components/Dialog' import { loadMode } from 'components/FileEditor/loader' import * as Notifications from 'containers/Notifications' import type * as Model from 'model' @@ -21,7 +21,7 @@ import * as yaml from 'utils/yaml' import * as Form from '../Form' -import * as OnDirty from './OnDirty' +// import * as OnDirty from './OnDirty' import SET_TABULATOR_TABLE_MUTATION from './gql/TabulatorTablesSet.generated' import RENAME_TABULATOR_TABLE_MUTATION from './gql/TabulatorTablesRename.generated' @@ -30,6 +30,14 @@ const TextEditor = React.lazy(() => import('components/FileEditor/TextEditor')) const TEXT_EDITOR_TYPE = { brace: 'yaml' as const } +type FormValuesSetTable = Pick + +type FormValuesRenameTable = Pick & { + newTableName: string +} + +type FormValuesDeleteTable = Pick + type YamlEditorFieldProps = RF.FieldRenderProps & M.TextFieldProps & { className: string } @@ -74,308 +82,423 @@ const validateTable: FF.FieldValidator = (inputStr?: string) => { return undefined } -const useTabulatorTableStyles = M.makeStyles((t) => ({ - delete: { - color: t.palette.error.main, - marginBottom: 'auto', - }, - editor: { - minHeight: t.spacing(15), - '& .ace_editor': { - minHeight: t.spacing(15), - }, - }, - header: { - alignItems: 'center', - display: 'flex', - marginBottom: t.spacing(1), - }, - name: { - marginBottom: t.spacing(2), - }, +// const useTabulatorTableStyles = M.makeStyles((t) => ({ +// delete: { +// color: t.palette.error.main, +// marginBottom: 'auto', +// }, +// editor: { +// minHeight: t.spacing(15), +// '& .ace_editor': { +// minHeight: t.spacing(15), +// }, +// }, +// header: { +// alignItems: 'center', +// display: 'flex', +// marginBottom: t.spacing(1), +// }, +// name: { +// marginBottom: t.spacing(2), +// }, +// root: { +// alignItems: 'stretch', +// display: 'flex', +// }, +// main: { +// flexGrow: 1, +// }, +// actions: { +// display: 'flex', +// flexDirection: 'column', +// flexShrink: 0, +// marginLeft: t.spacing(2), +// }, +// button: { +// '& + &': { +// marginTop: t.spacing(2), +// }, +// }, +// lock: { +// alignItems: 'center', +// animation: '$showLock .3s ease-out', +// background: fade(t.palette.background.paper, 0.7), +// bottom: 0, +// display: 'flex', +// justifyContent: 'center', +// left: 0, +// position: 'absolute', +// right: 0, +// top: 0, +// zIndex: 10, +// }, +// '@keyframes showLock': { +// '0%': { +// transform: 'scale(1.2x)', +// }, +// '100%': { +// transform: 'scale(1)', +// }, +// }, +// })) + +// type FormValues = Pick + +// interface TabulatorTableProps { +// bucketName: string +// className: string +// } + +// interface AddNew extends TabulatorTableProps { +// onClose: () => void +// tabulatorTable?: never // We create new table, so we don't have one +// } +// +// interface EditExisting extends TabulatorTableProps { +// onClose?: never // Don't close editing table +// tabulatorTable: FormValues +// } + +// function TabulatorTable({ +// bucketName, +// className, +// onClose, +// tabulatorTable, +// }: AddNew | EditExisting) { +// const renameTabulatorTable = GQL.useMutation(RENAME_TABULATOR_TABLE_MUTATION) +// const setTabulatorTable = GQL.useMutation(SET_TABULATOR_TABLE_MUTATION) +// const classes = useTabulatorTableStyles() +// +// const { push: notify } = Notifications.use() +// +// const renameTable = React.useCallback( +// async ( +// tableName: string, +// newTableName: string, +// ): Promise => { +// try { +// const { +// admin: { bucketRenameTabulatorTable: r }, +// } = await renameTabulatorTable({ bucketName, tableName, newTableName }) +// switch (r.__typename) { +// case 'BucketConfig': +// notify(`Successfully updated ${tableName} table`) +// return undefined +// case 'InvalidInput': +// return mapInputErrors(r.errors) +// case 'OperationError': +// return mkFormError(r.message) +// default: +// return assertNever(r) +// } +// } catch (e) { +// // eslint-disable-next-line no-console +// console.error('Error updating tabulator table') +// // eslint-disable-next-line no-console +// console.error(e) +// return mkFormError('unexpected') +// } +// }, +// [bucketName, notify, renameTabulatorTable], +// ) +// +// const setTable = React.useCallback( +// async ( +// tableName: string, +// config: string | null = null, +// ): Promise => { +// try { +// const { +// admin: { bucketSetTabulatorTable: r }, +// } = await setTabulatorTable({ bucketName, tableName, config }) +// switch (r.__typename) { +// case 'BucketConfig': +// notify(`Successfully updated ${tableName} table`) +// return undefined +// case 'InvalidInput': +// return mapInputErrors(r.errors) +// case 'OperationError': +// return mkFormError(r.message) +// default: +// return assertNever(r) +// } +// } catch (e) { +// // eslint-disable-next-line no-console +// console.error('Error updating tabulator table') +// // eslint-disable-next-line no-console +// console.error(e) +// return mkFormError('unexpected') +// } +// }, +// [bucketName, notify, setTabulatorTable], +// ) +// +// const onSubmit = React.useCallback( +// async (values: FormValues, form: FF.FormApi) => { +// // Rename +// // if theres is a table to rename +// // and the name was changed +// if (tabulatorTable && values.name !== tabulatorTable.name) { +// const renameResult = await renameTable(tabulatorTable.name, values.name) +// if (renameResult) { +// return renameResult +// } +// } +// +// // Create table if no one, +// // or update the config if it was changed +// // NOTE: table name could be new, just updated above +// if (!tabulatorTable || values.config !== tabulatorTable.config) { +// const result = await setTable(values.name, values.config) +// if (result) { +// return result +// } +// } +// +// form.reset(values) +// if (onClose) { +// onClose() +// } +// }, +// [onClose, renameTable, setTable, tabulatorTable], +// ) +// const [deleting, setDeleting] = React.useState< +// FF.SubmissionErrors | boolean | undefined +// >() +// const deleteExistingTable = React.useCallback(async () => { +// if (!tabulatorTable) { +// // Should have called onClose instead +// throw new Error('No tables to delete') +// } +// setDeleting(true) +// const errors = await setTable(tabulatorTable.name) +// setDeleting(errors) +// }, [setTable, tabulatorTable]) +// +// const confirm = useConfirm({ +// title: tabulatorTable +// ? `You are about to delete "${tabulatorTable.name}" table` +// : 'You have unsaved changes. Delete anyway?', +// submitTitle: 'Delete', +// onSubmit: React.useCallback( +// (confirmed) => { +// if (!confirmed) return +// if (tabulatorTable) deleteExistingTable() +// if (onClose) onClose() +// }, +// [tabulatorTable, deleteExistingTable, onClose], +// ), +// }) +// const { onChange: onFormSpy } = OnDirty.use() +// return ( +// +// {({ +// form, +// pristine, +// handleSubmit, +// submitting, +// submitFailed, +// hasValidationErrors, +// error, +// submitError, +// }) => ( +//
+// +// {confirm.render(<>)} +//
+// } +// variant="outlined" +// size="small" +// disabled={submitting || deleting} +// /> +// , +// validateYaml, +// validateTable, +// )} +// disabled={submitting || deleting} +// autoFocus={!tabulatorTable} +// /> +// {(submitFailed || typeof deleting === 'object') && ( +// +// )} +// {(submitting || deleting) && ( +//
+// +//
+// )} +//
+//
+// +// Delete +// +// form.reset()} +// className={classes.button} +// color="primary" +// disabled={pristine || submitting || deleting === true} +// variant="outlined" +// > +// Reset +// +// +// Save +// +//
+// +// )} +//
+// ) +// } + +const useEmptyStyles = M.makeStyles((t) => ({ root: { - alignItems: 'stretch', - display: 'flex', - }, - main: { - flexGrow: 1, - }, - actions: { display: 'flex', flexDirection: 'column', - flexShrink: 0, - marginLeft: t.spacing(2), - }, - button: { - '& + &': { - marginTop: t.spacing(2), - }, - }, - lock: { - alignItems: 'center', - animation: '$showLock .3s ease-out', - background: fade(t.palette.background.paper, 0.7), - bottom: 0, - display: 'flex', justifyContent: 'center', - left: 0, - position: 'absolute', - right: 0, - top: 0, - zIndex: 10, + alignItems: 'flex-start', }, - '@keyframes showLock': { - '0%': { - transform: 'scale(1.2x)', - }, - '100%': { - transform: 'scale(1)', - }, + title: { + marginBottom: t.spacing(2), }, })) -type FormValues = Pick - -interface TabulatorTableProps { - bucketName: string +interface EmptyProps { className: string + onClick: () => void } -interface AddNew extends TabulatorTableProps { - onClose: () => void - tabulatorTable?: never // We create new table, so we don't have one -} - -interface EditExisting extends TabulatorTableProps { - onClose?: never // Don't close editing table - tabulatorTable: FormValues -} - -function TabulatorTable({ - bucketName, - className, - onClose, - tabulatorTable, -}: AddNew | EditExisting) { - const renameTabulatorTable = GQL.useMutation(RENAME_TABULATOR_TABLE_MUTATION) - const setTabulatorTable = GQL.useMutation(SET_TABULATOR_TABLE_MUTATION) - const classes = useTabulatorTableStyles() - - const { push: notify } = Notifications.use() - - const renameTable = React.useCallback( - async ( - tableName: string, - newTableName: string, - ): Promise => { - try { - const { - admin: { bucketRenameTabulatorTable: r }, - } = await renameTabulatorTable({ bucketName, tableName, newTableName }) - switch (r.__typename) { - case 'BucketConfig': - notify(`Successfully updated ${tableName} table`) - return undefined - case 'InvalidInput': - return mapInputErrors(r.errors) - case 'OperationError': - return mkFormError(r.message) - default: - return assertNever(r) - } - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error updating tabulator table') - // eslint-disable-next-line no-console - console.error(e) - return mkFormError('unexpected') - } - }, - [bucketName, notify, renameTabulatorTable], - ) - - const setTable = React.useCallback( - async ( - tableName: string, - config: string | null = null, - ): Promise => { - try { - const { - admin: { bucketSetTabulatorTable: r }, - } = await setTabulatorTable({ bucketName, tableName, config }) - switch (r.__typename) { - case 'BucketConfig': - notify(`Successfully updated ${tableName} table`) - return undefined - case 'InvalidInput': - return mapInputErrors(r.errors) - case 'OperationError': - return mkFormError(r.message) - default: - return assertNever(r) - } - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error updating tabulator table') - // eslint-disable-next-line no-console - console.error(e) - return mkFormError('unexpected') - } - }, - [bucketName, notify, setTabulatorTable], +function Empty({ className, onClick }: EmptyProps) { + const classes = useEmptyStyles() + return ( +
+ + No tables configured + + + Add table + +
) +} - const onSubmit = React.useCallback( - async (values: FormValues, form: FF.FormApi) => { - // Rename - // if theres is a table to rename - // and the name was changed - if (tabulatorTable && values.name !== tabulatorTable.name) { - const renameResult = await renameTable(tabulatorTable.name, values.name) - if (renameResult) { - return renameResult - } - } +const useAddTableStyles = M.makeStyles((t) => ({ + root: { + display: 'flex', + flexDirection: 'column', + width: '100%', + alignItems: 'flex-end', + }, + button: { + marginLeft: t.spacing(2), + }, + editor: { + marginBottom: t.spacing(1), + }, +})) - // Create table if no one, - // or update the config if it was changed - // NOTE: table name could be new, just updated above - if (!tabulatorTable || values.config !== tabulatorTable.config) { - const result = await setTable(values.name, values.config) - if (result) { - return result - } - } +interface AddTableProps { + disabled?: boolean + onCancel: () => void + onSubmit: (values: FormValuesSetTable) => Promise +} - form.reset(values) - if (onClose) { - onClose() - } - }, - [onClose, renameTable, setTable, tabulatorTable], - ) - const [deleting, setDeleting] = React.useState< - FF.SubmissionErrors | boolean | undefined - >() - const deleteExistingTable = React.useCallback(async () => { - if (!tabulatorTable) { - // Should have called onClose instead - throw new Error('No tables to delete') - } - setDeleting(true) - const errors = await setTable(tabulatorTable.name) - setDeleting(errors) - }, [setTable, tabulatorTable]) - - const confirm = useConfirm({ - title: tabulatorTable - ? `You are about to delete "${tabulatorTable.name}" table` - : 'You have unsaved changes. Delete anyway?', - submitTitle: 'Delete', - onSubmit: React.useCallback( - (confirmed) => { - if (!confirmed) return - if (tabulatorTable) deleteExistingTable() - if (onClose) onClose() - }, - [tabulatorTable, deleteExistingTable, onClose], - ), - }) - const { onChange: onFormSpy } = OnDirty.use() +function AddTable({ disabled, onCancel, onSubmit }: AddTableProps) { + const classes = useAddTableStyles() return ( - - {({ - form, - pristine, - handleSubmit, - submitting, - submitFailed, - hasValidationErrors, - error, - submitError, - }) => ( -
- - {confirm.render(<>)} -
- } - variant="outlined" - size="small" - disabled={submitting || deleting} - /> - , - validateYaml, - validateTable, - )} - disabled={submitting || deleting} - autoFocus={!tabulatorTable} - /> - {(submitFailed || typeof deleting === 'object') && ( - + + {({ handleSubmit }) => ( + + } + variant="outlined" + /> + , + validateYaml, + validateTable, )} - {(submitting || deleting) && ( -
- -
- )} -
-
- - Delete - + disabled={disabled} + /> +
form.reset()} className={classes.button} color="primary" - disabled={pristine || submitting || deleting === true} - variant="outlined" + disabled={disabled} + size="small" + onClick={onCancel} > - Reset + Cancel - Save + Add
@@ -384,48 +507,215 @@ function TabulatorTable({ ) } -const useEmptyStyles = M.makeStyles((t) => ({ - root: { +const useTabulatorRowStyles = M.makeStyles((t) => ({ + root: {}, + name: { + marginTop: '3px', + }, + button: { + marginLeft: t.spacing(2), + }, + actions: { + display: 'flex', + }, + editor: { + marginBottom: t.spacing(1), + }, + delete: { + color: t.palette.error.main, + }, + config: { + alignItems: 'flex-end', display: 'flex', flexDirection: 'column', - justifyContent: 'center', - alignItems: 'flex-start', + width: '100%', }, - title: { - marginBottom: t.spacing(2), + nameForm: { + display: 'flex', + flexGrow: 1, + alignItems: 'center', + }, + submit: { + marginTop: t.spacing(1), }, })) -interface EmptyProps { - className: string - onClick: () => void +interface TabulatorRowProps { + disabled?: boolean + onDelete: (values: FormValuesDeleteTable) => Promise + onRename: (values: FormValuesRenameTable) => Promise + onSubmit: (values: FormValuesSetTable) => Promise + tabulatorTable: Model.GQLTypes.TabulatorTable } -function Empty({ className, onClick }: EmptyProps) { - const classes = useEmptyStyles() +function TabulatorRow({ + disabled, + onDelete, + onRename, + onSubmit, + tabulatorTable, +}: TabulatorRowProps) { + const classes = useTabulatorRowStyles() + const [open, setOpen] = React.useState(false) + const [editName, setEditName] = React.useState(false) + + const [anchorEl, setAnchorEl] = React.useState(null) + const [deleteError, setDeleteError] = React.useState>({}) + // TODO: add FormError component + const handleDelete = React.useCallback(async () => { + const error = await onDelete(tabulatorTable) + setDeleteError(error || {}) + }, [onDelete, tabulatorTable]) + return ( -
- - No tables configured - - - Add table - -
+ <> + setOpen((x) => !x)} + disabled={disabled} + > + + {open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'} + + {editName ? ( + + {({ handleSubmit }) => ( +
+ event.stopPropagation()} + fullWidth + initialValue={tabulatorTable.name} + errors={{ + required: 'Enter a table name', + }} + /> + { + event.stopPropagation() + handleSubmit() + }} + variant="contained" + color="primary" + > + Rename + + { + event.stopPropagation() + setEditName(false) + }} + color="primary" + > + Cancel + + + + )} +
+ ) : ( + + )} + + setAnchorEl(e.currentTarget)} size="small"> + more_vert + + setAnchorEl(null)}> + { + setAnchorEl(null) + setEditName(true) + }} + > + Rename + + { + setAnchorEl(null) + handleDelete() + }} + > + Delete + + + +
+ + + onSubmit({ ...tabulatorTable, ...values })} + > + {({ handleSubmit }) => ( +
+ , + validateYaml, + // validateTable, + )} + disabled={disabled} + /> + + Save + + + )} +
+
+
+ ) } +function parseResponseError( + r: Exclude, +): FF.SubmissionErrors | undefined { + switch (r.__typename) { + case 'InvalidInput': + return mapInputErrors(r.errors, { + config: 'config', + newTableName: 'newTableName', + tableName: 'newTableName', + }) + case 'OperationError': + return mkFormError(r.message) + default: + return assertNever(r) + } +} + const useStyles = M.makeStyles((t) => ({ actions: { margin: t.spacing(2, 0, 0), display: 'flex', justifyContent: 'flex-end', - padding: t.spacing(2, 0, 0), }, button: { - '& + &': { - marginLeft: t.spacing(2), - }, + marginLeft: 'auto', }, empty: { paddingBottom: t.spacing(2), @@ -454,41 +744,143 @@ interface TabulatorProps { } /** Have to be suspended because of `` */ -export default function Tabulator({ bucket, tabulatorTables }: TabulatorProps) { - const classes = useStyles() +export default function Tabulator({ + bucket: bucketName, + tabulatorTables, +}: TabulatorProps) { loadMode('yaml') + + const renameTabulatorTable = GQL.useMutation(RENAME_TABULATOR_TABLE_MUTATION) + const setTabulatorTable = GQL.useMutation(SET_TABULATOR_TABLE_MUTATION) + const { push: notify } = Notifications.use() + const [toAdd, setToAdd] = React.useState(false) + const [submitting, setSubmitting] = React.useState(false) + + const onDelete = React.useCallback( + async ({ + name: tableName, + }: FormValuesDeleteTable): Promise => { + try { + setSubmitting(true) + const response = await setTabulatorTable({ bucketName, tableName, config: null }) + // Generated `InputError` lacks optional properties and not infered correctly + const r = response.admin + .bucketSetTabulatorTable as Model.GQLTypes.BucketSetTabulatorTableResult + setSubmitting(false) + if (r.__typename === 'BucketConfig') { + notify(`Successfully deleted ${tableName} table`) + return undefined + } + return parseResponseError(r) + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error deleting table') + // eslint-disable-next-line no-console + console.error(e) + setSubmitting(false) + return mkFormError('unexpected') + } + }, + [bucketName, notify, setTabulatorTable], + ) + + const onRename = React.useCallback( + async (values: FormValuesRenameTable): Promise => { + const { name: tableName, newTableName } = values + try { + setSubmitting(true) + const response = await renameTabulatorTable({ + bucketName, + tableName, + newTableName, + }) + const r = response.admin + .bucketRenameTabulatorTable as Model.GQLTypes.BucketSetTabulatorTableResult + setSubmitting(false) + if (r.__typename === 'BucketConfig') { + notify(`Successfully updated ${tableName} table`) + return undefined + } + return parseResponseError(r) + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error updating table') + // eslint-disable-next-line no-console + console.error(e) + setSubmitting(false) + return mkFormError('unexpected') + } + }, + [bucketName, notify, renameTabulatorTable], + ) + + const onSubmit = React.useCallback( + async ({ + name: tableName, + config, + }: FormValuesSetTable): Promise => { + try { + setSubmitting(true) + const response = await setTabulatorTable({ bucketName, tableName, config }) + const r = response.admin + .bucketSetTabulatorTable as Model.GQLTypes.BucketSetTabulatorTableResult + setSubmitting(false) + if (r.__typename === 'BucketConfig') { + notify(`Successfully created ${tableName} table`) + return undefined + } + return parseResponseError(r) + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error creating table') + // eslint-disable-next-line no-console + console.error(e) + setSubmitting(false) + return mkFormError('unexpected') + } + }, + [bucketName, notify, setTabulatorTable], + ) + + const classes = useStyles() + if (!tabulatorTables.length && !toAdd) { return setToAdd(true)} /> } return ( <> - {tabulatorTables.map((tabulatorTable) => ( - - ))} - {toAdd && ( - setToAdd(false)} - /> - )} -
- setToAdd(true)} - startIcon={post_add} - type="button" - > - Add table - -
+ + {tabulatorTables.map((tabulatorTable) => ( + + ))} + + {toAdd ? ( + setToAdd(false)} + onSubmit={onSubmit} + /> + ) : ( + setToAdd(true)} + type="button" + > + Add table + + )} + + ) }