import { FC, useEffect, useContext, createContext, FormEvent, useState } from 'react' import { isEmpty, mapValues, has, filter, keyBy, isUndefined, partition, isNull } from 'lodash' import { observer, useLocalObservable } from 'mobx-react-lite' import { Button, Input, SidePanel, IconTrash, Radio, IconPlus, Toggle, Modal, Listbox } from 'ui' import { Dictionary } from 'components/grid' import { makeAutoObservable } from 'mobx' import { useStore } from 'hooks' import Panel from 'components/ui/Panel' import SqlEditor from 'components/ui/SqlEditor' import { POSTGRES_DATA_TYPES } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants' import ConfirmationModal from 'components/ui/ConfirmationModal' // [Refactor] Remove local state, just use the Form component class CreateFunctionFormState { id: number | undefined originalName: string | undefined // @ts-ignore args: { value: { name: string; type: string; error?: string }[] } // @ts-ignore behavior: { value: string; error?: string } // @ts-ignore configParams: { value: { name: string; value: string; error?: { name?: string; value?: string } }[] } // @ts-ignore definition: { value: string; error?: string } // @ts-ignore language: { value: string; error?: string } // @ts-ignore name: { value: string; error?: string } // @ts-ignore returnType: { value: string; error?: string } // @ts-ignore schema: { value: string; error?: string } // @ts-ignore securityDefiner: { value: boolean; error?: string } constructor() { makeAutoObservable(this) this.reset() } get requestBody() { return { id: this.id, name: this.name.value, schema: this.schema.value, definition: this.definition.value, return_type: this.returnType.value, language: this.language.value, behavior: this.behavior.value, security_definer: this.securityDefiner.value, args: this.args.value.map((x: any) => `${x.name} ${x.type}`), config_params: mapValues(keyBy(this.configParams.value, 'name'), 'value'), } } reset(func?: Dictionary) { this.id = func?.id this.originalName = func?.name this.args = convertArgumentTypes(func?.argument_types) this.behavior = { value: func?.behavior ?? 'VOLATILE' } this.configParams = convertConfigParams(func?.config_params) this.definition = { value: func?.definition ?? '' } this.language = { value: func?.language ?? 'plpgsql' } this.name = { value: func?.name ?? '' } this.returnType = { value: func?.return_type ?? 'void' } this.schema = { value: func?.schema ?? 'public' } this.securityDefiner = { value: func?.security_definer ?? false } } update(state: Dictionary) { this.args = state.args this.behavior = state.behavior this.configParams = state.configParams this.definition = state.definition this.language = state.language this.name = state.name this.returnType = state.returnType this.schema = state.schema this.securityDefiner = state.securityDefiner } } /** * convert argument_types = "a integer, b integer" * to args = {value: [{name:'a', type:'integer'}, {name:'b', type:'integer'}]} */ function convertArgumentTypes(value: string) { const items = value?.split(',') if (isEmpty(value) || !items || items?.length == 0) return { value: [] } const temp = items.map((x) => { const str = x.trim() const space = str.indexOf(' ') const name = str.slice(0, space !== 1 ? space : 0) const type = str.slice(space + 1) return { name, type } }) return { value: temp } } /** * convert config_params = {search_path: "auth, public"} * to {value: [{name: 'search_path', value: 'auth, public'}]} */ function convertConfigParams(value: Dictionary) { const temp = [] if (value) { for (var key in value) { temp.push({ name: key, value: value[key] }) } } return { value: temp } } interface ICreateFunctionStore { loading: boolean formState: CreateFunctionFormState meta: any schemas: Dictionary[] isEditing: boolean onFormChange: (value: { key: string; value: any }) => void onFormArrayChange: (value: { operation: 'add' | 'delete' | 'update' key: string idx?: number value?: any }) => void setSchemas: (value: Dictionary[]) => void validateForm: () => boolean } class CreateFunctionStore implements ICreateFunctionStore { loading = false formState = new CreateFunctionFormState() meta = null schemas = [] advancedVisible = false isDirty = false constructor() { makeAutoObservable(this) } get title() { return this.formState.id ? `Edit '${this.formState.originalName}' function` : 'Add a new function' } get isEditing() { return this.formState.id != undefined } toggleAdvancedVisible = () => { this.advancedVisible = !this.advancedVisible } setSchemas = (value: Dictionary[]) => { this.schemas = value as any } setLoading = (value: boolean) => { this.loading = value } setIsDirty = (value: boolean) => { this.isDirty = value } onFormChange = ({ key, value }: { key: string; value: any }) => { this.isDirty = true if (has(this.formState, key)) { const temp = (this.formState as any)[key] as any ;(this.formState as any)[key] = { ...temp, value, error: undefined } } else { ;(this.formState as any)[key] = { value } } } onFormArrayChange = ({ operation, key, idx, value, }: { operation: 'add' | 'delete' | 'update' key: string idx?: number value?: any }) => { switch (operation) { case 'add': { if (has(this.formState, key)) { // @ts-ignore this.formState[key].value.push(value) } else { const values = [value] // @ts-ignore this.formState[key] = { value: [value] } } break } case 'delete': { if (has(this.formState, key)) { const temp = filter( // @ts-ignore this.formState[key].value, (_: any, index: number) => index != idx ) as any // @ts-ignore this.formState[key].value = temp } break } default: { if (has(this.formState, key) && !isUndefined(idx)) { // @ts-ignore this.formState[key].value[idx] = value } else { // @ts-ignore this.formState[key] = { value: [value] } } } } } validateForm = () => { let isValidated = true const _state = mapValues(this.formState, (x: { value: any }, key: string) => { switch (key) { case 'name': { if (isEmpty(x.value) || hasWhitespace(x.value)) { isValidated = false return { ...x, error: 'Invalid function name' } } else { return x } } case 'args': { const temp = x.value?.map((i: Dictionary) => { if (isEmpty(i.name) || hasWhitespace(i.name)) { isValidated = false return { ...i, error: 'Invalid argument name' } } else { return i } }) x.value = temp return x } case 'configParams': { const temp = x.value?.map((i: Dictionary) => { const error: any = { name: undefined, value: undefined } if (isEmpty(i.name) || hasWhitespace(i.name)) { isValidated = false error.name = 'Invalid config name' } if (isEmpty(i.value)) { isValidated = false error.value = 'Missing config value' } return { ...i, error } }) x.value = temp return x } default: return x } }) if (!isValidated) { this.formState.update(_state) } return isValidated } } function hasWhitespace(value: string) { return /\s/.test(value) } const CreateFunctionContext = createContext(null) type CreateFunctionProps = { func: any visible: boolean setVisible: (value: boolean) => void } & any const CreateFunction: FC = ({ func, visible, setVisible }) => { const { ui, meta } = useStore() const _localState = useLocalObservable(() => new CreateFunctionStore()) _localState.meta = meta as any const [isClosingPanel, setIsClosingPanel] = useState(false) useEffect(() => { const fetchSchemas = async () => { await (_localState!.meta as any).schemas.load() const schemas = (_localState!.meta as any).schemas.list() _localState.setSchemas(schemas) } fetchSchemas() }, []) useEffect(() => { _localState.formState.reset(func) _localState.setIsDirty(false) }, [visible, func]) async function handleSubmit() { try { if (_localState.validateForm()) { _localState.setLoading(true) const body = _localState.formState.requestBody const response: any = body.id ? await (_localState!.meta as any).functions.update(body.id, body) : await (_localState!.meta as any).functions.create(body) if (response.error) { ui.setNotification({ category: 'error', message: `Failed to create function: ${ response.error?.message ?? 'Submit request failed' }`, }) _localState.setLoading(false) } else { ui.setNotification({ category: 'success', message: `${_localState.isEditing ? 'Updated' : 'Created new'} function called ${ response.name }`, }) _localState.setLoading(false) setVisible(!visible) } } } catch (error: any) { ui.setNotification({ category: 'error', message: `Failed to create function: ${error.message}`, }) _localState.setLoading(false) } } function isClosingSidePanel() { _localState.isDirty ? setIsClosingPanel(true) : setVisible(!visible) } return ( <>
{_localState.isEditing ? ( <>
) : (
_localState.toggleAdvancedVisible()} label="Show advanced settings" checked={_localState.advancedVisible} labelOptional="These are settings that might be familiar for postgres heavy users " />
{/* advanced selections */}
{_localState.advancedVisible && ( <>
)}
)}
setIsClosingPanel(false)} onSelectConfirm={() => { setIsClosingPanel(false) setVisible(!visible) }} >

There are unsaved changes. Are you sure you want to close the panel? Your changes will be lost.

) } export default observer(CreateFunction) const InputName: FC = observer(({}) => { const _localState = useContext(CreateFunctionContext) return ( _localState!.onFormChange({ key: 'name', value: e.target.value, }) } size="small" error={_localState!.formState.name.error} descriptionText="Name will also be used for the function name in postgres" /> ) }) type InputMultiArgumentsProps = { readonly?: boolean } const InputMultiArguments: FC = observer(({ readonly }) => { const _localState = useContext(CreateFunctionContext) function onAddArgument() { _localState!.onFormArrayChange({ key: 'args', value: { name: '', type: 'bool' }, operation: 'add', }) } return (
Arguments

Arguments can be referenced in the function body using either names or numbers.

{readonly && isEmpty(_localState!.formState.args.value) && ( No argument for this function )} {_localState!.formState.args.value.map( (x: { name: string; type: string; error?: string }, idx: number) => ( ) )} {!readonly && (
)}
) }) type InputArgumentProps = { idx: number name: string type: string error?: string readonly?: boolean } const InputArgument: FC = observer(({ idx, name, type, error, readonly }) => { const _localState = useContext(CreateFunctionContext) function onNameChange(e: FormEvent) { const _value = e.currentTarget.value _localState!.onFormArrayChange({ key: 'args', value: { name: _value, type }, idx, operation: 'update', }) } function onTypeChange(_value: string) { _localState!.onFormArrayChange({ key: 'args', value: { name, type: _value }, idx, operation: 'update', }) } function onDelete() { _localState!.onFormArrayChange({ key: 'args', idx, operation: 'delete', }) } return (
integer {POSTGRES_DATA_TYPES.map((x: string) => ( {x} ))} {!readonly && (
) }) const InputMultiConfigParams: FC = observer(({}) => { const _localState = useContext(CreateFunctionContext) function onAddArgument() { _localState!.onFormArrayChange({ key: 'configParams', value: { name: '', value: '' }, operation: 'add', }) } return (
Config Params
{_localState!.formState.configParams.value.map( ( x: { name: string; value: string; error?: { name?: string; value?: string } }, idx: number ) => ( ) )}
) }) type InputConfigParamProps = { idx: number name: string value: string error?: { name?: string; value?: string } } const InputConfigParam: FC = observer(({ idx, name, value, error }) => { const _localState = useContext(CreateFunctionContext) function onNameChange(e: FormEvent) { const _value = e.currentTarget.value _localState!.onFormArrayChange({ key: 'configParams', value: { name: _value, value }, idx, operation: 'update', }) } function onValueChange(e: FormEvent) { const _value = e.currentTarget.value _localState!.onFormArrayChange({ key: 'configParams', value: { name, value: _value }, idx, operation: 'update', }) } function onDelete() { _localState!.onFormArrayChange({ key: 'configParams', idx, operation: 'delete', }) } return (
) }) const InputDefinition: FC = observer(({}) => { const _localState = useContext(CreateFunctionContext) return (
Definition

The language below should be written in `{_localState!.formState.language.value}`.

{!_localState?.isEditing && (

Change the language in the Advanced Settings below.

)}
{ _localState!.onFormChange({ key: 'definition', value: value, }) }} contextmenu={false} />
) }) const SelectSchema: FC = observer(({}) => { const _localState = useContext(CreateFunctionContext) return ( _localState!.onFormChange({ key: 'schema', value })} > {_localState!.schemas.map((x) => ( {x.name} ))} ) }) const SelectLanguage: FC = observer(({}) => { const { meta } = useStore() const _localState = useContext(CreateFunctionContext) const [enabledExtensions] = partition( meta.extensions.list(), (ext: any) => !isNull(ext.installed_version) ) return ( _localState!.onFormChange({ key: 'language', value })} > sql { //map through all selected extensions that start with pl enabledExtensions .filter((ex: any) => { return ex.name.startsWith('pl') }) .map((ex) => ( {ex.name} )) } ) }) const SelectReturnType: FC = observer(({}) => { const _localState = useContext(CreateFunctionContext) return ( _localState!.onFormChange({ key: 'returnType', value })} > void record trigger integer {POSTGRES_DATA_TYPES.map((x: string) => ( {x} ))} ) }) const SelectBehavior: FC = observer(({}) => { const _localState = useContext(CreateFunctionContext) return ( _localState!.onFormChange({ key: 'behavior', value })} > immutable stable volatile ) }) const RadioSecurity: FC = observer(({}) => { const _localState = useContext(CreateFunctionContext) return ( <>
{ _localState!.onFormChange({ key: 'securityDefiner', value: event.target.value == 'SECURITY_DEFINER', }) }} value={ _localState!.formState.securityDefiner.value ? 'SECURITY_DEFINER' : 'SECURITY_INVOKER' } error={_localState!.formState.securityDefiner.error} >
) })