From 7c86328514415d1b9ba5fc9fbd7f1c52919b2a24 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Thu, 12 Sep 2024 20:46:37 +0530 Subject: [PATCH] Introduced 'useSchemaStateSubscriber', which generates a state subscriber mananager instance. It helps multiple subscribers in a single control as we could have multiple subscribe within a control. (For example - value, options, errors, etc). --- .../js/SchemaView/DataGridView/grid.jsx | 14 ++- .../js/SchemaView/DataGridView/mappedCell.jsx | 13 ++- .../static/js/SchemaView/FieldSetView.jsx | 10 +- web/pgadmin/static/js/SchemaView/FormView.jsx | 18 ++-- .../static/js/SchemaView/MappedControl.jsx | 23 ++--- .../static/js/SchemaView/hooks/index.js | 2 + .../js/SchemaView/hooks/useFieldError.js | 15 +-- .../js/SchemaView/hooks/useFieldOptions.js | 13 +-- .../js/SchemaView/hooks/useFieldSchema.js | 20 ++-- .../js/SchemaView/hooks/useFieldValue.js | 13 +-- .../hooks/useSchemaStateSubscriber.js | 92 +++++++++++++++++++ .../js/SchemaView/utils/listenDepChanges.js | 11 ++- 12 files changed, 168 insertions(+), 76 deletions(-) create mode 100644 web/pgadmin/static/js/SchemaView/hooks/useSchemaStateSubscriber.js diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx index b60363f7e32..ad61929c1ed 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx @@ -33,7 +33,9 @@ import CustomPropTypes from 'sources/custom_prop_types'; import { StyleDataGridBox } from '../StyledComponents'; import { SchemaStateContext } from '../SchemaState'; -import { useFieldOptions, useFieldValue } from '../hooks'; +import { + useFieldOptions, useFieldValue, useSchemaStateSubscriber, +} from '../hooks'; import { registerView } from '../registry'; import { listenDepChanges } from '../utils'; @@ -49,20 +51,16 @@ export default function DataGridView({ }) { const pgAdmin = usePgAdmin(); const [refreshKey, setRefreshKey] = useState(0); + const subscriberManager = useSchemaStateSubscriber(setRefreshKey); const schemaState = useContext(SchemaStateContext); - const options = useFieldOptions( - accessPath, schemaState, refreshKey, setRefreshKey - ); + const options = useFieldOptions(accessPath, schemaState, subscriberManager); const value = useFieldValue(accessPath, schemaState); const schema = field.schema; const features = useRef(); // Update refresh key on changing the number of rows. useFieldValue( - [...accessPath, 'length'], schemaState, refreshKey, - (newKey) => { - setRefreshKey(newKey); - } + [...accessPath, 'length'], schemaState, subscriberManager ); useEffect(() => { diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx index a8081c6974e..6c422d09efb 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx @@ -16,7 +16,9 @@ import { evalFunc } from 'sources/utils'; import { MappedCellControl } from '../MappedControl'; import { SCHEMA_STATE_ACTIONS, SchemaStateContext } from '../SchemaState'; import { flatternObject } from '../common'; -import { useFieldOptions, useFieldValue } from '../hooks'; +import { + useFieldOptions, useFieldValue, useSchemaStateSubscriber +} from '../hooks'; import { listenDepChanges } from '../utils'; import { DataGridContext, DataGridRowContext } from './context'; @@ -25,14 +27,17 @@ import { DataGridContext, DataGridRowContext } from './context'; export function getMappedCell({field}) { const Cell = ({reRenderRow, getValue}) => { - const [key, setKey] = useState(0); + const [, setKey] = useState(0); + const subscriberManager = useSchemaStateSubscriber(setKey); const schemaState = useContext(SchemaStateContext); const { dataDispatch, accessPath } = useContext(DataGridContext); const { rowAccessPath, row } = useContext(DataGridRowContext); const colAccessPath = schemaState.accessPath(rowAccessPath, field.id); - let colOptions = useFieldOptions(colAccessPath, schemaState, key, setKey); - let value = useFieldValue(colAccessPath, schemaState, key, setKey); + let colOptions = useFieldOptions( + colAccessPath, schemaState, subscriberManager + ); + let value = useFieldValue(colAccessPath, schemaState, subscriberManager); let rowValue = useFieldValue(rowAccessPath, schemaState); listenDepChanges(colAccessPath, field, true, schemaState); diff --git a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx index 556b92305f5..ead9295ab5b 100644 --- a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx +++ b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx @@ -15,7 +15,9 @@ import CustomPropTypes from 'sources/custom_prop_types'; import { FieldControl } from './FieldControl'; import { SchemaStateContext } from './SchemaState'; -import { useFieldSchema, useFieldValue } from './hooks'; +import { + useFieldSchema, useFieldValue, useSchemaStateSubscriber, +} from './hooks'; import { registerView } from './registry'; import { createFieldControls, listenDepChanges } from './utils'; @@ -23,13 +25,15 @@ import { createFieldControls, listenDepChanges } from './utils'; export default function FieldSetView({ field, accessPath, dataDispatch, viewHelperProps, controlClassName, }) { - const [key, setRefreshKey] = useState(0); + const [, setKey] = useState(0); + const subscriberManager = useSchemaStateSubscriber(setKey); const schema = field.schema; const schemaState = useContext(SchemaStateContext); const value = useFieldValue(accessPath, schemaState); const options = useFieldSchema( - field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey + field, accessPath, value, viewHelperProps, schemaState, subscriberManager ); + const label = field.label; listenDepChanges(accessPath, field, options.visible, schemaState); diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index fce33f112d3..0d47b25b742 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -27,7 +27,9 @@ import { FieldControl } from './FieldControl'; import { SQLTab } from './SQLTab'; import { FormContentBox } from './StyledComponents'; import { SchemaStateContext } from './SchemaState'; -import { useFieldSchema, useFieldValue } from './hooks'; +import { + useFieldSchema, useFieldValue, useSchemaStateSubscriber, +} from './hooks'; import { registerView, View } from './registry'; import { createFieldControls, listenDepChanges } from './utils'; @@ -62,10 +64,11 @@ export default function FormView({ showError=false, resetKey, focusOnFirstInput=false }) { const [key, setKey] = useState(0); + const subscriberManager = useSchemaStateSubscriber(setKey); const schemaState = useContext(SchemaStateContext); const value = useFieldValue(accessPath, schemaState); const { visible } = useFieldSchema( - field, accessPath, value, viewHelperProps, schemaState, key, setKey + field, accessPath, value, viewHelperProps, schemaState, subscriberManager ); const [tabValue, setTabValue] = useState(0); @@ -106,13 +109,12 @@ export default function FormView({ useEffect(() => { // Refresh on message changes. - return schemaState.subscribe( - ['errors', 'message'], + return subscriberManager.current?.add( + schemaState, ['errors', 'message'], 'states', (newState, prevState) => { - if (_.isUndefined(newState) || _.isUndefined(prevState)); - setKey(Date.now()); - }, - 'states' + if (_.isUndefined(newState) || _.isUndefined(prevState)) + subscriberManager.current?.signal(); + } ); }, [key]); diff --git a/web/pgadmin/static/js/SchemaView/MappedControl.jsx b/web/pgadmin/static/js/SchemaView/MappedControl.jsx index 658af07c8a1..9aed32e059e 100644 --- a/web/pgadmin/static/js/SchemaView/MappedControl.jsx +++ b/web/pgadmin/static/js/SchemaView/MappedControl.jsx @@ -28,7 +28,7 @@ import { evalFunc } from 'sources/utils'; import { SchemaStateContext } from './SchemaState'; import { isValueEqual } from './common'; import { - useFieldOptions, useFieldValue, useFieldError + useFieldOptions, useFieldValue, useFieldError, useSchemaStateSubscriber, } from './hooks'; import { listenDepChanges } from './utils'; @@ -339,22 +339,15 @@ export const MappedFormControl = ({ }) => { const checkIsMounted = useIsMounted(); const [key, setKey] = useState(0); + const subscriberManager = useSchemaStateSubscriber(setKey); const schemaState = useContext(SchemaStateContext); const state = schemaState.data; - const avoidRenderingWhenNotMounted = (newKey) => { - if (checkIsMounted()) { - setKey(newKey); - } + const value = useFieldValue(accessPath, schemaState, subscriberManager); + const options = useFieldOptions(accessPath, schemaState, subscriberManager); + const {hasError} = useFieldError(accessPath, schemaState, subscriberManager); + const avoidRenderingWhenNotMounted = (...args) => { + if (checkIsMounted()) subscriberManager.current?.signal(...args); }; - const value = useFieldValue( - accessPath, schemaState, key, avoidRenderingWhenNotMounted - ); - const options = useFieldOptions( - accessPath, schemaState, key, avoidRenderingWhenNotMounted - ); - const { hasError } = useFieldError( - accessPath, schemaState, key, avoidRenderingWhenNotMounted - ); const origOnChange = onChange; @@ -371,7 +364,7 @@ export const MappedFormControl = ({ const depVals = listenDepChanges( accessPath, field, options.visible, schemaState, state, - key, avoidRenderingWhenNotMounted + avoidRenderingWhenNotMounted ); let newProps = { diff --git a/web/pgadmin/static/js/SchemaView/hooks/index.js b/web/pgadmin/static/js/SchemaView/hooks/index.js index c294172fd81..e64238d1d20 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/index.js +++ b/web/pgadmin/static/js/SchemaView/hooks/index.js @@ -12,6 +12,7 @@ import { useFieldOptions } from './useFieldOptions'; import { useFieldValue } from './useFieldValue'; import { useSchemaState } from './useSchemaState'; import { useFieldSchema } from './useFieldSchema'; +import { useSchemaStateSubscriber } from './useSchemaStateSubscriber'; export { @@ -20,4 +21,5 @@ export { useFieldValue, useFieldSchema, useSchemaState, + useSchemaStateSubscriber, }; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js index 07542725098..2179cef5b52 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js @@ -10,22 +10,23 @@ import { useEffect } from 'react'; -export const useFieldError = ( - path, schemaState, key, setRefreshKey -) => { +export const useFieldError = (path, schemaState, subscriberManager) => { + useEffect(() => { - if (!schemaState || !setRefreshKey) return; + if (!schemaState || !subscriberManager?.current) return; const checkPathError = (newState, prevState) => { if (prevState.name !== path && newState.name !== path) return; // We don't need to redraw the control on message change. if (prevState.name === newState.name) return; - setRefreshKey({id: Date.now()}); + subscriberManager.current?.signal(); }; - return schemaState.subscribe(['errors'], checkPathError, 'states'); - }, [key, schemaState?._id]); + return subscriberManager.current?.add( + schemaState, ['errors'], 'states', checkPathError + ); + }); const errors = schemaState?.errors || {}; const error = errors.name === path ? errors.message : null; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js index 5763edc2426..b4c6d5eccaa 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js @@ -10,16 +10,13 @@ import { useEffect } from 'react'; -export const useFieldOptions = ( - path, schemaState, key, setRefreshKey -) => { +export const useFieldOptions = (path, schemaState, subscriberManager) => { + useEffect(() => { - if (!schemaState) return; + if (!schemaState || !subscriberManager?.current) return; - return schemaState.subscribe( - path, () => setRefreshKey?.({id: Date.now()}), 'options' - ); - }, [key, schemaState?._id]); + return subscriberManager.current?.add(schemaState, path, 'options'); + }); return schemaState?.options(path) || {visible: true}; }; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js index 0cbd2bd94c4..5a1b7d1260d 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js @@ -14,32 +14,32 @@ import { booleanEvaluator } from '../options'; export const useFieldSchema = ( - field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey + field, accessPath, value, viewHelperProps, schemaState, subscriberManager ) => { + useEffect(() => { - if (!schemaState || !field) return; + if (!schemaState || !field || !subscriberManager?.current) return; // It already has 'id', 'options' is already evaluated. if (field.id) - return schemaState.subscribe( - accessPath, () => setRefreshKey?.({id: Date.now()}), 'options' - ); + return subscriberManager.current?.add(schemaState, accessPath, 'options'); // There are no dependencies. if (!_.isArray(field?.deps)) return; // Subscribe to all the dependents. const unsubscribers = field.deps.map((dep) => ( - schemaState.subscribe( - accessPath.concat(dep), () => setRefreshKey?.({id: Date.now()}), - 'value' + subscriberManager.current?.add( + schemaState, accessPath.concat(dep), 'value' ) )); return () => { - unsubscribers.forEach(unsubscribe => unsubscribe()); + unsubscribers.forEach( + unsubscribe => subscriberManager.current?.remove(unsubscribe) + ); }; - }, [key, schemaState?._id]); + }); if (!field) return { visible: true }; if (field.id) return schemaState?.options(accessPath); diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js index 0e92ae9e42a..4e58330aa1c 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js @@ -10,16 +10,13 @@ import { useEffect } from 'react'; -export const useFieldValue = ( - path, schemaState, key, setRefreshKey -) => { +export const useFieldValue = (path, schemaState, subscriberManager) => { + useEffect(() => { - if (!schemaState || !setRefreshKey) return; + if (!schemaState || !subscriberManager?.current) return; - return schemaState.subscribe( - path, () => setRefreshKey({id: Date.now()}), 'value' - ); - }, [key, schemaState?._id]); + return subscriberManager.current?.add(schemaState, path, 'value'); + }); return schemaState?.value(path); }; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useSchemaStateSubscriber.js b/web/pgadmin/static/js/SchemaView/hooks/useSchemaStateSubscriber.js new file mode 100644 index 00000000000..62a687829f0 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/useSchemaStateSubscriber.js @@ -0,0 +1,92 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; + +///////// +// +// A class to handle the ScheamState subscription for a control to avoid +// rendering multiple times. +// +class SubscriberManager { + + constructor(refreshKeyCallback) { + this.mounted = true; + this.callback = refreshKeyCallback; + this.unsubscribers = new Set(); + this._id = Date.now(); + } + + add(schemaState, accessPath, kind, callback) { + if (!schemaState) return; + + callback = callback || (() => this.signal()); + + return this._add(schemaState.subscribe(accessPath, callback, kind)); + } + + _add(unsubscriber) { + if (!unsubscriber) return; + // Avoid reinsertion of same unsubscriber. + if (this.unsubscribers.has(unsubscriber)) return; + this.unsubscribers.add(unsubscriber); + + return () => this.remove(unsubscriber); + } + + remove(unsubscriber) { + if (!unsubscriber) return; + if (!this.unsubscribers.has(unsubscriber)) return; + this.unsubscribers.delete(unsubscriber); + unsubscriber(); + } + + signal() { + // Do nothing - if already work is in progress. + if (!this.mounted) return; + this.mounted = false; + this.release(); + this.callback(Date.now()); + } + + release () { + const unsubscribers = this.unsubscribers; + this.unsubscribers = new Set(); + this.mounted = true; + + setTimeout(() => { + Set.prototype.forEach.call( + unsubscribers, (unsubscriber) => unsubscriber() + ); + }, 0); + } + + mount() { + this.mounted = true; + } +} + +export function useSchemaStateSubscriber(refreshKeyCallback) { + const subscriberManager = React.useRef(null); + + React.useEffect(() => { + if (!subscriberManager.current) return; + + return () => { + subscriberManager.current?.release(); + }; + }, []); + + if (!subscriberManager.current) + subscriberManager.current = new SubscriberManager(refreshKeyCallback); + else + subscriberManager.current.mount(); + + return subscriberManager; +} diff --git a/web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js b/web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js index 0c5d4e94190..5c7d92f1854 100644 --- a/web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js +++ b/web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js @@ -14,7 +14,7 @@ import { evalFunc } from 'sources/utils'; export const listenDepChanges = ( - accessPath, field, visible, schemaState, data, key, setRefreshKey + accessPath, field, visible, schemaState, data, setRefreshKey ) => { const deps = field?.deps ? (evalFunc(null, field.deps) || []) : null; const parentPath = accessPath ? [...accessPath] : []; @@ -47,9 +47,10 @@ export const listenDepChanges = ( ); } - schemaState.subscribe( - source, () => setRefreshKey(Date.now()), 'value' - ); + if (setRefreshKey) + schemaState.subscribe( + source, () => setRefreshKey(Date.now()), 'value' + ); }); } @@ -57,7 +58,7 @@ export const listenDepChanges = ( // Cleanup the listeners when unmounting. schemaState.removeDepListener(accessPath); }; - }, [key]); + }, []); return deps?.map((dep) => schemaState.value( _.isArray(dep) ? dep : parentPath.concat(dep)