import { FC, useEffect, createContext, useContext } from 'react' import { makeAutoObservable } from 'mobx' import { observer, useLocalObservable } from 'mobx-react-lite' import { isEmpty, mapValues, has, without, union } from 'lodash' import { Input, SidePanel, Checkbox, Listbox, Typography, IconPlayCircle, IconPauseCircle, IconTerminal, Badge, Button, } from '@supabase/ui' import { Dictionary } from 'components/grid' import { useRouter } from 'next/router' import SVG from 'react-inlinesvg' import ChooseFunctionForm from './ChooseFunctionForm' import FormEmptyBox from 'components/ui/FormBoxEmpty' import NoTableState from 'components/ui/States/NoTableState' import { useStore } from 'hooks' class CreateTriggerFormState { id: number | undefined originalName: string | undefined // @ts-ignore activation: { value: string; error?: string } // @ts-ignore enabledMode: { value: string } // @ts-ignore events: { value: string[]; error?: string } // @ts-ignore functionName: { value: string; error?: string } // @ts-ignore functionSchema: { value: string; error?: string } // @ts-ignore orientation: { value: string; error?: string } // @ts-ignore name: { value: string; error?: string } // @ts-ignore schema: { value: string } // @ts-ignore table: { value: string } // @ts-ignore tableId: { value: number; error?: string } constructor() { makeAutoObservable(this) this.reset() } get requestBody() { return { id: this.id, activation: this.activation.value, enabled_mode: this.enabledMode.value, events: this.events.value, function_name: this.functionName.value, function_schema: this.functionSchema.value, orientation: this.orientation.value, name: this.name.value, schema: this.schema.value, table: this.table.value, } } reset(trigger?: Dictionary) { this.id = trigger?.id this.originalName = trigger?.name this.activation = { value: trigger?.activation ?? 'BEFORE' } this.enabledMode = { value: trigger?.enabled_mode ?? 'ORIGIN' } this.events = { value: trigger?.events ?? [] } this.functionName = { value: trigger?.function_name ?? '' } this.functionSchema = { value: trigger?.function_schema ?? '' } this.orientation = { value: trigger?.orientation ?? 'STATEMENT' } this.name = { value: trigger?.name ?? '' } this.schema = { value: trigger?.schema ?? '' } this.table = { value: trigger?.table ?? '' } this.tableId = { value: trigger?.table_id ?? '' } } update(state: Dictionary) { this.activation = state.activation this.enabledMode = state.enabledMode this.events = state.events this.functionName = state.functionName this.functionSchema = state.functionSchema this.orientation = state.orientation this.name = state.name this.schema = state.schema this.table = state.table this.tableId = state.tableId } } interface ICreateTriggerStore { chooseFunctionFormVisible: boolean loading: boolean formState: CreateTriggerFormState meta: any selectedFunction?: Dictionary[] tables: Dictionary[] triggerFunctions: Dictionary[] onFormChange: ({ key, value }: { key: string; value: any }) => void onSelectFunction: (id: number) => void setChooseFunctionFormVisible: (value: boolean) => void setDefaultSelectedTable: () => void setLoading: (value: boolean) => void setTables: (value: Dictionary[]) => void setTriggerFunctions: (value: Dictionary[]) => void validateForm: () => boolean } class CreateTriggerStore implements ICreateTriggerStore { chooseFunctionFormVisible = false loading = false formState = new CreateTriggerFormState() meta = null tables = [] triggerFunctions = [] constructor() { makeAutoObservable(this) } get selectedFunction() { const func = this.triggerFunctions.find( (x: any) => x.name == this.formState.functionName.value && x.schema == this.formState.functionSchema.value ) return func } get title() { return this.formState.id ? `Edit '${this.formState.originalName}' trigger` : 'Add a new Trigger' } get isEditing() { return this.formState.id != undefined } setChooseFunctionFormVisible = (value: boolean) => { this.chooseFunctionFormVisible = value } // set first table as default selection setDefaultSelectedTable = () => { if (this.tables?.length != 0) { this.formState.table.value = (this.tables[0] as any).name this.formState.schema.value = (this.tables[0] as any).schema this.formState.tableId.value = (this.tables[0] as any).id } } setLoading = (value: boolean) => { this.loading = value } setTables = (value: Dictionary[]) => { this.tables = value as any this.setDefaultSelectedTable() } setTriggerFunctions = (value: Dictionary[]) => { this.triggerFunctions = value as any } onFormChange = ({ key, value }: { key: string; value: any }) => { if (has(this.formState, key)) { const temp = (this.formState as any)[key] // @ts-ignore this.formState[key] = { ...temp, value, error: undefined } } else { // @ts-ignore this.formState[key] = { value } } } onSelectFunction = (id: number) => { const func = this.triggerFunctions.find((x: any) => x.id == id) if (func) { this.formState.functionName.value = (func as any).name this.formState.functionSchema.value = (func as any).schema } } 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 trigger name' } } else { return x } } case 'activation': { if (isEmpty(x.value)) { isValidated = false return { ...x, error: 'you have an error' } } else { return x } } case 'events': { if (isEmpty(x.value)) { isValidated = false return { ...x, error: 'Select at least 1 event' } } else { return x } } case 'tableId': { if (isEmpty(`${x.value}`)) { isValidated = false return { ...x, error: 'You must choose a table' } } else { return x } } default: return x } }) if (!isValidated) { this.formState.update(_state) } return isValidated } } function hasWhitespace(value: string) { return /\s/.test(value) } const CreateTriggerContext = createContext(null) type CreateTriggerProps = { trigger?: any visible: boolean setVisible: (value: boolean) => void } & any const CreateTrigger: FC = ({ trigger, visible, setVisible }) => { const { ui, meta } = useStore() const _localState = useLocalObservable(() => new CreateTriggerStore()) _localState.meta = meta as any // for the empty 'no tables' state link const router = useRouter() const { ref } = router.query useEffect(() => { const fetchTables = async () => { await (_localState!.meta as any)!.tables!.load() const tables = (_localState!.meta as any)!.tables.list() _localState.setTables(tables) } const fetchFunctions = async () => { await (_localState.meta as any).functions.load() const triggerFuncs = (_localState!.meta as any)!.functions.listTriggerFunctions() _localState.setTriggerFunctions(triggerFuncs) } fetchTables() fetchFunctions() }, []) useEffect(() => { if (trigger) { _localState.formState.reset(trigger) } else { _localState.formState.reset() _localState.setDefaultSelectedTable() } }, [visible, trigger]) async function handleSubmit() { try { if (_localState.validateForm()) { _localState.setLoading(true) const body = _localState.formState.requestBody const response: any = _localState.isEditing ? await (_localState.meta as any).triggers.update(body.id, body) : await (_localState.meta as any).triggers.create(body) if (response.error) { ui.setNotification({ category: 'error', message: `Failed to create trigger: ${ response.error?.message ?? 'submit request failed' }`, }) _localState.setLoading(false) } else { ui.setNotification({ category: 'success', message: `${_localState.isEditing ? 'Updated' : 'Created new'} trigger called ${ response.name }`, }) _localState.setLoading(false) setVisible(!visible) } } } catch (error: any) { ui.setNotification({ category: 'error', message: `Filed to create trigger: ${error.message}`, }) _localState.setLoading(false) } } const hasPublicTables = _localState.tables.length >= 1 return ( <> setVisible(!visible)} header={_localState.title} hideFooter={!hasPublicTables} className={ _localState.chooseFunctionFormVisible ? 'hooks-sidepanel transform transition-all duration-300 ease-in-out mr-16' : 'hooks-sidepanel transform transition-all duration-300 ease-in-out mr-0' } loading={_localState.loading} onConfirm={handleSubmit} > {hasPublicTables ? (
{_localState.isEditing ? (
) : ( <>
Conditions to fire trigger
)}
_localState.onSelectFunction(id)} />
) : ( )}
) } export default observer(CreateTrigger) const InputName: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( _localState!.onFormChange({ key: 'name', value: e.target.value, }) } size="small" error={_localState!.formState.name.error} descriptionText="The name is also stored as the actual postgres name of the trigger. Do not use spaces/whitespace." /> ) }) const SelectEnabledMode: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( _localState!.onFormChange({ key: 'enabledMode', value: value, }) } size="small" descriptionText="Determines if a trigger should or should not fire. Can also be used to disable a trigger, but not delete it." > { return (
) }} value="ORIGIN" label="Origin" > Origin This is a default behaviour
{ return (
) }} value="REPLICA" label="Replica" > Replica Will only fire if the session is in “replica” mode
{ return (
) }} value="ALWAYS" label="Always" > Always Will fire regardless of the current replication role
{ return (
) }} value="DISABLED" label="Disabled" > Disabled Will not fire
) }) const SelectOrientation: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( _localState!.onFormChange({ key: 'orientation', value: value, }) } size="small" descriptionText="Identifies whether the trigger fires once for each processed row or once for each statement" > Row fires once for each processed row Statement fires once for each statement ) }) const ListboxTable: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( { const _table = _localState!.tables.find((x) => x.id === id) if (_table) { _localState!.onFormChange({ key: 'table', value: _table.name, }) _localState!.onFormChange({ key: 'schema', value: _table.schema, }) _localState!.onFormChange({ key: 'tableId', value: id, }) } }} size="small" error={_localState!.formState.tableId.error} descriptionText="This is the table the trigger will watch for changes. You can only select 1 table for a trigger." > {_localState!.tables.map((x) => { return ( (
code.replace(/svg/, 'svg class="m-auto text-color-inherit"') } />
)} >
{x.name} {x.schema}
) })}
) }) const CheckboxEvents: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( // @ts-ignore { const temp = _localState!.formState.events.value const value = e.target.checked ? union(temp, [e.target.value]) : without(temp, e.target.value) _localState!.onFormChange({ key: 'events', value: value, }) }} error={_localState!.formState.events.error} > ) }) const ListboxActivation: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( { _localState!.onFormChange({ key: 'activation', value: _value, }) }} value={_localState!.formState.activation.value} layout="horizontal" size="small" error={_localState!.formState.activation.error} > (
)} >
{'before'} Trigger fires before the operation is attempted
(
)} >
{'after'} Trigger fires after the operation has completed
) }) const FunctionForm: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return (
Function to trigger
{isEmpty(_localState!.formState.functionName.value) ? ( ) : ( )}
) }) const FunctionEmpty: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( ) }) const FunctionWithArguments: FC = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( <>
{_localState!.formState.functionName.value}
{_localState!.formState.functionSchema.value}
) })