chore: Table performance improvement (#20983)

## Description
Several changes made to enhance the table performance:
- Batch updates of meta properties to limit the number of rerenders
- Removed expensive comparator operations.
- Memoised components which are not susceptible to updates.
- Table filter code optimisation to limit the number of times setState
is triggered.

Fixes #20910
This commit is contained in:
Vemparala Surya Vamsi 2023-03-30 10:24:29 +05:30 committed by GitHub
parent 425a9f2220
commit 99afdcc2c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 897 additions and 531 deletions

View File

@ -110,7 +110,7 @@
"lottie-web": "^5.7.4", "lottie-web": "^5.7.4",
"mammoth": "^1.5.1", "mammoth": "^1.5.1",
"marked": "^4.0.18", "marked": "^4.0.18",
"memoize-one": "^5.2.1", "memoize-one": "^6.0.0",
"micro-memoize": "^4.0.10", "micro-memoize": "^4.0.10",
"moment": "2.29.4", "moment": "2.29.4",
"moment-timezone": "^0.5.35", "moment-timezone": "^0.5.35",

View File

@ -15,6 +15,9 @@ export interface UpdateWidgetMetaPropertyPayload {
propertyValue: unknown; propertyValue: unknown;
} }
export interface BatchUpdateWidgetMetaPropertyPayload {
batchMetaUpdates: UpdateWidgetMetaPropertyPayload[];
}
export const updateWidgetMetaPropAndEval = ( export const updateWidgetMetaPropAndEval = (
widgetId: string, widgetId: string,
propertyName: string, propertyName: string,
@ -79,6 +82,15 @@ export const triggerEvalOnMetaUpdate = () => {
}); });
}; };
export const syncBatchUpdateWidgetMetaProperties = (
batchMetaUpdates: UpdateWidgetMetaPropertyPayload[],
): ReduxAction<BatchUpdateWidgetMetaPropertyPayload> => {
return {
type: ReduxActionTypes.BATCH_UPDATE_META_PROPS,
payload: { batchMetaUpdates },
};
};
export const syncUpdateWidgetMetaProperty = ( export const syncUpdateWidgetMetaProperty = (
widgetId: string, widgetId: string,
propertyName: string, propertyName: string,

View File

@ -382,6 +382,7 @@ export const ReduxActionTypes = {
CREATE_WORKSPACE_SUCCESS: "CREATE_WORKSPACE_SUCCESS", CREATE_WORKSPACE_SUCCESS: "CREATE_WORKSPACE_SUCCESS",
ADD_USER_TO_WORKSPACE_INIT: "ADD_USER_TO_WORKSPACE_INIT", ADD_USER_TO_WORKSPACE_INIT: "ADD_USER_TO_WORKSPACE_INIT",
ADD_USER_TO_WORKSPACE_SUCCESS: "ADD_USER_TO_WORKSPACE_ERROR", ADD_USER_TO_WORKSPACE_SUCCESS: "ADD_USER_TO_WORKSPACE_ERROR",
BATCH_UPDATE_META_PROPS: "BATCH_UPDATE_META_PROPS",
SET_META_PROP: "SET_META_PROP", SET_META_PROP: "SET_META_PROP",
SET_META_PROP_AND_EVAL: "SET_META_PROP_AND_EVAL", SET_META_PROP_AND_EVAL: "SET_META_PROP_AND_EVAL",
META_UPDATE_DEBOUNCED_EVAL: "META_UPDATE_DEBOUNCED_EVAL", META_UPDATE_DEBOUNCED_EVAL: "META_UPDATE_DEBOUNCED_EVAL",

View File

@ -32,6 +32,7 @@ describe("EditorContextProvider", () => {
"setWidgetCache", "setWidgetCache",
"updateMetaWidgetProperty", "updateMetaWidgetProperty",
"syncUpdateWidgetMetaProperty", "syncUpdateWidgetMetaProperty",
"syncBatchUpdateWidgetMetaProperties",
"triggerEvalOnMetaUpdate", "triggerEvalOnMetaUpdate",
"deleteMetaWidgets", "deleteMetaWidgets",
"deleteWidgetProperty", "deleteWidgetProperty",
@ -69,6 +70,7 @@ describe("EditorContextProvider", () => {
"setWidgetCache", "setWidgetCache",
"updateMetaWidgetProperty", "updateMetaWidgetProperty",
"syncUpdateWidgetMetaProperty", "syncUpdateWidgetMetaProperty",
"syncBatchUpdateWidgetMetaProperties",
"triggerEvalOnMetaUpdate", "triggerEvalOnMetaUpdate",
"updateWidgetAutoHeight", "updateWidgetAutoHeight",
"checkContainersForAutoHeight", "checkContainersForAutoHeight",

View File

@ -17,8 +17,10 @@ import {
import type { ExecuteTriggerPayload } from "constants/AppsmithActionConstants/ActionConstants"; import type { ExecuteTriggerPayload } from "constants/AppsmithActionConstants/ActionConstants";
import type { OccupiedSpace } from "constants/CanvasEditorConstants"; import type { OccupiedSpace } from "constants/CanvasEditorConstants";
import type { UpdateWidgetMetaPropertyPayload } from "actions/metaActions";
import { import {
resetChildrenMetaProperty, resetChildrenMetaProperty,
syncBatchUpdateWidgetMetaProperties,
syncUpdateWidgetMetaProperty, syncUpdateWidgetMetaProperty,
triggerEvalOnMetaUpdate, triggerEvalOnMetaUpdate,
} from "actions/metaActions"; } from "actions/metaActions";
@ -69,6 +71,9 @@ export type EditorContextType<TCache = unknown> = {
propertyName: string, propertyName: string,
propertyValue: any, propertyValue: any,
) => void; ) => void;
syncBatchUpdateWidgetMetaProperties?: (
batchMetaUpdates: UpdateWidgetMetaPropertyPayload[],
) => void;
updateWidgetAutoHeight?: (widgetId: string, height: number) => void; updateWidgetAutoHeight?: (widgetId: string, height: number) => void;
checkContainersForAutoHeight?: () => void; checkContainersForAutoHeight?: () => void;
modifyMetaWidgets?: (modifications: ModifyMetaWidgetPayload) => void; modifyMetaWidgets?: (modifications: ModifyMetaWidgetPayload) => void;
@ -102,6 +107,7 @@ const COMMON_API_METHODS: EditorContextTypeKey[] = [
"setWidgetCache", "setWidgetCache",
"updateMetaWidgetProperty", "updateMetaWidgetProperty",
"syncUpdateWidgetMetaProperty", "syncUpdateWidgetMetaProperty",
"syncBatchUpdateWidgetMetaProperties",
"triggerEvalOnMetaUpdate", "triggerEvalOnMetaUpdate",
"updateWidgetAutoHeight", "updateWidgetAutoHeight",
"checkContainersForAutoHeight", "checkContainersForAutoHeight",
@ -191,6 +197,9 @@ const mapDispatchToProps = {
propertyName: string, propertyName: string,
propertyValue: any, propertyValue: any,
) => syncUpdateWidgetMetaProperty(widgetId, propertyName, propertyValue), ) => syncUpdateWidgetMetaProperty(widgetId, propertyName, propertyValue),
syncBatchUpdateWidgetMetaProperties: (
batchMetaUpdates: UpdateWidgetMetaPropertyPayload[],
) => syncBatchUpdateWidgetMetaProperties(batchMetaUpdates),
resetChildrenMetaProperty, resetChildrenMetaProperty,
disableDrag: disableDragAction, disableDrag: disableDragAction,
deleteWidgetProperty: deletePropertyAction, deleteWidgetProperty: deletePropertyAction,

View File

@ -107,6 +107,7 @@ export interface LogActionPayload {
analytics?: Record<string, any>; analytics?: Record<string, any>;
// plugin error details if any (only for plugin errors). // plugin error details if any (only for plugin errors).
pluginErrorDetails?: any; pluginErrorDetails?: any;
meta?: Record<string, any>;
} }
export interface Message { export interface Message {

View File

@ -3,6 +3,7 @@ import { createReducer } from "utils/ReducerUtils";
import type { import type {
UpdateWidgetMetaPropertyPayload, UpdateWidgetMetaPropertyPayload,
ResetWidgetMetaPayload, ResetWidgetMetaPayload,
BatchUpdateWidgetMetaPropertyPayload,
} from "actions/metaActions"; } from "actions/metaActions";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
@ -53,6 +54,20 @@ export const metaReducer = createReducer(initialState, {
return nextState; return nextState;
}, },
[ReduxActionTypes.BATCH_UPDATE_META_PROPS]: (
state: MetaState,
action: ReduxAction<BatchUpdateWidgetMetaPropertyPayload>,
) => {
const nextState = produce(state, (draftMetaState) => {
const { batchMetaUpdates } = action.payload;
batchMetaUpdates.forEach(({ propertyName, propertyValue, widgetId }) => {
set(draftMetaState, `${widgetId}.${propertyName}`, propertyValue);
});
return draftMetaState;
});
return nextState;
},
[ReduxActionTypes.SET_META_PROP_AND_EVAL]: ( [ReduxActionTypes.SET_META_PROP_AND_EVAL]: (
state: MetaState, state: MetaState,
action: ReduxAction<UpdateWidgetMetaPropertyPayload>, action: ReduxAction<UpdateWidgetMetaPropertyPayload>,

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import type { WidgetProps } from "./BaseWidget"; import type { WidgetProps } from "./BaseWidget";
import type BaseWidget from "./BaseWidget"; import type BaseWidget from "./BaseWidget";
import { debounce, fromPairs } from "lodash"; import { debounce, fromPairs, isEmpty } from "lodash";
import { EditorContext } from "components/editorComponents/EditorContextProvider"; import { EditorContext } from "components/editorComponents/EditorContextProvider";
import AppsmithConsole from "utils/AppsmithConsole"; import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole";
@ -10,6 +10,12 @@ import type { ExecuteTriggerPayload } from "constants/AppsmithActionConstants/Ac
import { connect } from "react-redux"; import { connect } from "react-redux";
import { getWidgetMetaProps } from "sagas/selectors"; import { getWidgetMetaProps } from "sagas/selectors";
import type { AppState } from "@appsmith/reducers"; import type { AppState } from "@appsmith/reducers";
import { error } from "loglevel";
export type pushAction = (
propertyName: string | batchUpdateWidgetMetaPropertyType,
propertyValue?: unknown,
actionExecution?: DebouncedExecuteActionPayload,
) => void;
export type DebouncedExecuteActionPayload = Omit< export type DebouncedExecuteActionPayload = Omit<
ExecuteTriggerPayload, ExecuteTriggerPayload,
@ -17,8 +23,15 @@ export type DebouncedExecuteActionPayload = Omit<
> & { > & {
dynamicString?: string; dynamicString?: string;
}; };
export type batchUpdateWidgetMetaPropertyType = {
propertyName: string;
propertyValue: unknown;
actionExecution?: DebouncedExecuteActionPayload;
}[];
export interface WithMeta { export interface WithMeta {
commitBatchMetaUpdates: () => void;
pushBatchMetaUpdates: pushAction;
updateWidgetMetaProperty: ( updateWidgetMetaProperty: (
propertyName: string, propertyName: string,
propertyValue: unknown, propertyValue: unknown,
@ -36,6 +49,7 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
initialMetaState: Record<string, unknown>; initialMetaState: Record<string, unknown>;
actionsToExecute: Record<string, DebouncedExecuteActionPayload>; actionsToExecute: Record<string, DebouncedExecuteActionPayload>;
batchMetaUpdates: batchUpdateWidgetMetaPropertyType;
updatedProperties: Record<string, boolean>; updatedProperties: Record<string, boolean>;
constructor(props: metaHOCProps) { constructor(props: metaHOCProps) {
super(props); super(props);
@ -47,6 +61,7 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
); );
this.updatedProperties = {}; this.updatedProperties = {};
this.actionsToExecute = {}; this.actionsToExecute = {};
this.batchMetaUpdates = [];
} }
addPropertyForEval = ( addPropertyForEval = (
@ -140,6 +155,112 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
actionExecution, actionExecution,
); );
}; };
/**
This function pushes meta updates that can be commited later.
If there are multiple updates, use this function to batch those updates together.
*/
pushBatchMetaUpdates: pushAction = (firstArgument, ...restArgs) => {
//if first argument is an array its a batch lets push it
if (Array.isArray(firstArgument)) {
this.batchMetaUpdates.push(...firstArgument);
return;
}
//if first argument is a string its a propertyName arg and we are pushing a single action
if (typeof firstArgument === "string") {
const [propertyValue, actionExecution] = restArgs;
this.batchMetaUpdates.push({
propertyName: firstArgument,
propertyValue,
actionExecution,
});
return;
}
const allArgs = [firstArgument, ...restArgs];
error("unknown args ", allArgs);
};
/**
This function commits all batched updates in one go.
*/
commitBatchMetaUpdates = () => {
//ignore commit if batch array is empty
if (!this.batchMetaUpdates || !this.batchMetaUpdates.length) return;
const metaUpdates = this.batchMetaUpdates.reduce(
(acc: any, { propertyName, propertyValue }) => {
acc[propertyName] = propertyValue;
return acc;
},
{},
);
AppsmithConsole.info({
logType: LOG_TYPE.WIDGET_UPDATE,
text: "Widget property was updated",
source: {
type: ENTITY_TYPE.WIDGET,
id: this.props.widgetId,
name: this.props.widgetName,
},
meta: metaUpdates,
});
// extract payload from updates
const payload = [...this.batchMetaUpdates];
//clear batch updates
this.batchMetaUpdates = [];
this.handleBatchUpdateWidgetMetaProperties(payload);
};
getMetaPropPath = (propertyName: string | undefined) => {
// look at this.props.__metaOptions, check for metaPropPath value
// if they exist, then update the propertyName
// Below code of updating metaOptions can be removed once we have ListWidget v2 where we better manage meta values of ListWidget.
const metaOptions = this.props.__metaOptions;
if (!metaOptions) return;
return `${metaOptions.metaPropPrefix}.${this.props.widgetName}.${propertyName}[${metaOptions.index}]`;
};
handleBatchUpdateWidgetMetaProperties = (
batchMetaUpdates: batchUpdateWidgetMetaPropertyType,
) => {
//if no updates ignore update call
if (!batchMetaUpdates || isEmpty(batchMetaUpdates)) return;
const { syncBatchUpdateWidgetMetaProperties } = this.context;
const widgetId = this.props.metaWidgetId || this.props.widgetId;
if (syncBatchUpdateWidgetMetaProperties) {
const metaOptions = this.props.__metaOptions;
const consolidatedUpdates = batchMetaUpdates.reduce(
(acc: any, { propertyName, propertyValue }) => {
acc.push({ widgetId, propertyName, propertyValue });
if (metaOptions) {
acc.push({
widgetId: metaOptions.widgetId,
propertyName: this.getMetaPropPath(propertyName),
propertyValue,
});
}
return acc;
},
[],
);
syncBatchUpdateWidgetMetaProperties(consolidatedUpdates);
}
batchMetaUpdates.forEach(({ actionExecution, propertyName }) =>
this.addPropertyForEval(propertyName, actionExecution),
);
this.setState({}, () => {
// react batches the setState call
// this will result in batching multiple updateWidgetMetaProperty calls.
this.debouncedTriggerEvalOnMetaUpdate();
});
};
handleUpdateWidgetMetaProperty = ( handleUpdateWidgetMetaProperty = (
propertyName: string, propertyName: string,
@ -165,10 +286,11 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
// if they exist, then update the propertyName // if they exist, then update the propertyName
// Below code of updating metaOptions can be removed once we have ListWidget v2 where we better manage meta values of ListWidget. // Below code of updating metaOptions can be removed once we have ListWidget v2 where we better manage meta values of ListWidget.
const metaOptions = this.props.__metaOptions; const metaOptions = this.props.__metaOptions;
if (metaOptions) { const metaPropPath = this.getMetaPropPath(propertyName);
if (metaOptions && metaPropPath) {
syncUpdateWidgetMetaProperty( syncUpdateWidgetMetaProperty(
metaOptions.widgetId, metaOptions.widgetId,
`${metaOptions.metaPropPrefix}.${this.props.widgetName}.${propertyName}[${metaOptions.index}]`, metaPropPath,
propertyValue, propertyValue,
); );
} }
@ -194,6 +316,8 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
return ( return (
<WrappedWidget <WrappedWidget
{...this.updatedProps()} {...this.updatedProps()}
commitBatchMetaUpdates={this.commitBatchMetaUpdates}
pushBatchMetaUpdates={this.pushBatchMetaUpdates}
updateWidgetMetaProperty={this.updateWidgetMetaProperty} updateWidgetMetaProperty={this.updateWidgetMetaProperty}
/> />
); );

View File

@ -27,6 +27,9 @@ function MetaWidgetContextProvider({
const updateWidgetProperty = const updateWidgetProperty =
metaEditorContextProps.updateWidgetProperty ?? metaEditorContextProps.updateWidgetProperty ??
editorContextProps.updateWidgetProperty; editorContextProps.updateWidgetProperty;
const syncBatchUpdateWidgetMetaProperties =
metaEditorContextProps.syncBatchUpdateWidgetMetaProperties ??
editorContextProps.syncBatchUpdateWidgetMetaProperties;
const syncUpdateWidgetMetaProperty = const syncUpdateWidgetMetaProperty =
metaEditorContextProps.syncUpdateWidgetMetaProperty ?? metaEditorContextProps.syncUpdateWidgetMetaProperty ??
editorContextProps.syncUpdateWidgetMetaProperty; editorContextProps.syncUpdateWidgetMetaProperty;
@ -77,6 +80,7 @@ function MetaWidgetContextProvider({
getWidgetCache, getWidgetCache,
deleteMetaWidgets, deleteMetaWidgets,
updateMetaWidgetProperty, updateMetaWidgetProperty,
syncBatchUpdateWidgetMetaProperties,
}), }),
[ [
executeAction, executeAction,
@ -93,6 +97,7 @@ function MetaWidgetContextProvider({
getWidgetCache, getWidgetCache,
deleteMetaWidgets, deleteMetaWidgets,
updateMetaWidgetProperty, updateMetaWidgetProperty,
syncBatchUpdateWidgetMetaProperties,
], ],
); );

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef } from "react"; import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { pick, reduce } from "lodash"; import { reduce } from "lodash";
import type { Row as ReactTableRowType } from "react-table"; import type { Row as ReactTableRowType } from "react-table";
import { import {
useTable, useTable,
@ -157,6 +157,7 @@ export type HeaderComponentProps = {
widgetId: string; widgetId: string;
}; };
const emptyArr: any = [];
export function Table(props: TableProps) { export function Table(props: TableProps) {
const isResizingColumn = React.useRef(false); const isResizingColumn = React.useRef(false);
const handleResizeColumn = (columnWidths: Record<string, number>) => { const handleResizeColumn = (columnWidths: Record<string, number>) => {
@ -176,17 +177,14 @@ export function Table(props: TableProps) {
} }
props.handleResizeColumn(columnWidthMap); props.handleResizeColumn(columnWidthMap);
}; };
const data = React.useMemo(() => props.data, [JSON.stringify(props.data)]); const { columns, data, multiRowSelection, toggleAllRowSelect } = props;
const columnString = JSON.stringify(
pick(props, ["columns", "compactMode", "columnWidthMap"]),
);
const columns = React.useMemo(() => props.columns, [columnString]);
const tableHeadercolumns = React.useMemo( const tableHeadercolumns = React.useMemo(
() => () =>
props.columns.filter((column: ReactTableColumnProps) => { columns.filter((column: ReactTableColumnProps) => {
return column.alias !== "actions"; return column.alias !== "actions";
}), }),
[columnString], [columns],
); );
/* /*
For serverSidePaginationEnabled we are taking props.data.length as the page size. For serverSidePaginationEnabled we are taking props.data.length as the page size.
@ -212,7 +210,8 @@ export function Table(props: TableProps) {
totalColumnsWidth, totalColumnsWidth,
} = useTable( } = useTable(
{ {
columns: columns, //columns and data needs to be memoised as per useTable specs
columns,
data, data,
defaultColumn, defaultColumn,
initialState: { initialState: {
@ -234,6 +233,7 @@ export function Table(props: TableProps) {
} else { } else {
// We are updating column size since the drag is complete when we are changing value of isResizing from true to false // We are updating column size since the drag is complete when we are changing value of isResizing from true to false
if (isResizingColumn.current) { if (isResizingColumn.current) {
//clear timeout logic
//update isResizingColumn in next event loop so that dragEnd event does not trigger click event. //update isResizingColumn in next event loop so that dragEnd event does not trigger click event.
setTimeout(function () { setTimeout(function () {
isResizingColumn.current = false; isResizingColumn.current = false;
@ -247,15 +247,18 @@ export function Table(props: TableProps) {
startIndex = 0; startIndex = 0;
endIndex = props.data.length; endIndex = props.data.length;
} }
const subPage = page.slice(startIndex, endIndex); const subPage = useMemo(
const selectedRowIndices = props.selectedRowIndices || []; () => page.slice(startIndex, endIndex),
[page, startIndex, endIndex],
);
const selectedRowIndices = props.selectedRowIndices || emptyArr;
const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT]; const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT];
const tableWrapperRef = useRef<HTMLDivElement | null>(null); const tableWrapperRef = useRef<HTMLDivElement | null>(null);
const scrollBarRef = useRef<SimpleBar | null>(null); const scrollBarRef = useRef<SimpleBar | null>(null);
const tableHeaderWrapperRef = React.createRef<HTMLDivElement>(); const tableHeaderWrapperRef = React.createRef<HTMLDivElement>();
const rowSelectionState = React.useMemo(() => { const rowSelectionState = React.useMemo(() => {
// return : 0; no row selected | 1; all row selected | 2: some rows selected // return : 0; no row selected | 1; all row selected | 2: some rows selected
if (!props.multiRowSelection) return null; if (!multiRowSelection) return null;
const selectedRowCount = reduce( const selectedRowCount = reduce(
page, page,
(count, row) => { (count, row) => {
@ -266,16 +269,17 @@ export function Table(props: TableProps) {
const result = const result =
selectedRowCount === 0 ? 0 : selectedRowCount === page.length ? 1 : 2; selectedRowCount === 0 ? 0 : selectedRowCount === page.length ? 1 : 2;
return result; return result;
}, [selectedRowIndices, page]); }, [multiRowSelection, page, selectedRowIndices]);
const handleAllRowSelectClick = ( const handleAllRowSelectClick = useCallback(
e: React.MouseEvent<HTMLDivElement, MouseEvent>, (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
) => { // if all / some rows are selected we remove selection on click
// if all / some rows are selected we remove selection on click // else select all rows
// else select all rows toggleAllRowSelect(!Boolean(rowSelectionState), page);
props.toggleAllRowSelect(!Boolean(rowSelectionState), page); // loop over subPage rows and toggleRowSelected if required
// loop over subPage rows and toggleRowSelected if required e.stopPropagation();
e.stopPropagation(); },
}; [page, rowSelectionState, toggleAllRowSelect],
);
const isHeaderVisible = const isHeaderVisible =
props.isVisibleSearch || props.isVisibleSearch ||
props.isVisibleFilters || props.isVisibleFilters ||

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { memo } from "react";
import { CellWrapper } from "../TableStyledWrappers"; import { CellWrapper } from "../TableStyledWrappers";
import type { BaseCellComponentProps } from "../Constants"; import type { BaseCellComponentProps } from "../Constants";
@ -18,7 +18,7 @@ export interface RenderActionProps extends BaseCellComponentProps {
onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void; onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void;
} }
export function ButtonCell(props: RenderActionProps) { function ButtonCellComponent(props: RenderActionProps) {
const { const {
allowCellWrapping, allowCellWrapping,
cellBackground, cellBackground,
@ -84,3 +84,4 @@ export function ButtonCell(props: RenderActionProps) {
</CellWrapper> </CellWrapper>
); );
} }
export const ButtonCell = memo(ButtonCellComponent);

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { memo } from "react";
import type { BaseCellComponentProps, CellAlignment } from "../Constants"; import type { BaseCellComponentProps, CellAlignment } from "../Constants";
import { ALIGN_ITEMS, JUSTIFY_CONTENT } from "../Constants"; import { ALIGN_ITEMS, JUSTIFY_CONTENT } from "../Constants";
import { CellWrapper, TooltipContentWrapper } from "../TableStyledWrappers"; import { CellWrapper, TooltipContentWrapper } from "../TableStyledWrappers";
@ -59,7 +59,7 @@ type CheckboxCellProps = BaseCellComponentProps & {
disabledCheckboxMessage: string; disabledCheckboxMessage: string;
}; };
export const CheckboxCell = (props: CheckboxCellProps) => { const CheckboxCellComponent = (props: CheckboxCellProps) => {
const { const {
accentColor, accentColor,
borderRadius, borderRadius,
@ -122,3 +122,5 @@ export const CheckboxCell = (props: CheckboxCellProps) => {
</CheckboxCellWrapper> </CheckboxCellWrapper>
); );
}; };
export const CheckboxCell = memo(CheckboxCellComponent);

View File

@ -1,5 +1,4 @@
import React from "react"; import React, { memo } from "react";
import type { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import type { ButtonColumnActions } from "widgets/TableWidgetV2/constants"; import type { ButtonColumnActions } from "widgets/TableWidgetV2/constants";
import { EditableCellActions } from "widgets/TableWidgetV2/constants"; import { EditableCellActions } from "widgets/TableWidgetV2/constants";
@ -18,7 +17,7 @@ type RenderEditActionsProps = BaseCellComponentProps & {
onDiscard: () => void; onDiscard: () => void;
}; };
export function EditActionCell(props: RenderEditActionsProps) { function EditActionCellComponent(props: RenderEditActionsProps) {
const { const {
allowCellWrapping, allowCellWrapping,
cellBackground, cellBackground,
@ -91,3 +90,4 @@ export function EditActionCell(props: RenderEditActionsProps) {
</CellWrapper> </CellWrapper>
); );
} }
export const EditActionCell = memo(EditActionCellComponent);

View File

@ -1,4 +1,10 @@
import React, { createRef, useCallback, useEffect, useState } from "react"; import React, {
createRef,
useCallback,
useEffect,
useState,
memo,
} from "react";
import { MenuItem, Tooltip, Menu } from "@blueprintjs/core"; import { MenuItem, Tooltip, Menu } from "@blueprintjs/core";
import Check from "remixicon-react/CheckFillIcon"; import Check from "remixicon-react/CheckFillIcon";
import ArrowDownIcon from "remixicon-react/ArrowDownSLineIcon"; import ArrowDownIcon from "remixicon-react/ArrowDownSLineIcon";
@ -145,7 +151,7 @@ type HeaderProps = {
) => void; ) => void;
}; };
export const HeaderCell = (props: HeaderProps) => { const HeaderCellComponent = (props: HeaderProps) => {
const { column, editMode, isSortable } = props; const { column, editMode, isSortable } = props;
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
@ -335,3 +341,4 @@ export const HeaderCell = (props: HeaderProps) => {
</div> </div>
); );
}; };
export const HeaderCell = memo(HeaderCellComponent);

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { memo } from "react";
import { getDragHandlers } from "widgets/TableWidgetV2/widget/utilities"; import { getDragHandlers } from "widgets/TableWidgetV2/widget/utilities";
import { HeaderCell } from "../cellComponents/HeaderCell"; import { HeaderCell } from "../cellComponents/HeaderCell";
import type { ReactTableColumnProps } from "../Constants"; import type { ReactTableColumnProps } from "../Constants";
@ -143,4 +143,4 @@ const TableColumnHeader = (props: TableColumnHeaderProps) => {
); );
}; };
export default TableColumnHeader; export default memo(TableColumnHeader);

View File

@ -117,4 +117,4 @@ function ActionItem(props: ActionItemProps) {
} }
} }
export default ActionItem; export default React.memo(ActionItem);

View File

@ -45,7 +45,7 @@ const PageNumberInputWrapper = styled(NumericInput)<{
const MIN_PAGE_COUNT = 1; const MIN_PAGE_COUNT = 1;
export function PageNumberInput(props: { function PageNumberInputComponent(props: {
pageNo: number; pageNo: number;
pageCount: number; pageCount: number;
updatePageNo: (pageNo: number, event?: EventType) => void; updatePageNo: (pageNo: number, event?: EventType) => void;
@ -109,3 +109,4 @@ export function PageNumberInput(props: {
/> />
); );
} }
export const PageNumberInput = React.memo(PageNumberInputComponent);

View File

@ -1,79 +0,0 @@
/* eslint-disable @typescript-eslint/ban-types */
// TODO(vikcy): Fix the banned types in this file
import React from "react";
import type { IconName } from "@blueprintjs/core";
import { Icon } from "@blueprintjs/core";
import styled from "styled-components";
const PagerContainer = styled.div`
&&& {
height: 49px;
}
`;
function PagerIcon(props: {
icon: IconName;
onClick: Function;
className: string;
}) {
return (
<Icon
className={props.className}
icon={props.icon}
iconSize={14}
onClick={props.onClick as any}
style={{
padding: 14,
marginTop: 5,
}}
/>
);
}
interface PagerProps {
pageNo: number;
prevPageClick: Function;
nextPageClick: Function;
}
const PageWrapper = styled.div`
&& {
width: 140px;
display: flex;
margin: 0 auto;
}
`;
export function TablePagination(props: PagerProps) {
return (
<PagerContainer className={"e-control e-pager e-lib"}>
<PageWrapper>
<PagerIcon
className={
props.pageNo <= 1
? "e-prev e-icons e-icon-prev e-prevpagedisabled e-disable"
: "e-prev e-icons e-icon-prev e-prevpage"
}
icon={"chevron-left"}
onClick={props.prevPageClick}
/>
<div
className={"e-numericcontainer"}
style={{
marginTop: 12,
marginLeft: 6,
}}
>
<button
className={"e-link e-numericitem e-spacing e-currentitem e-active"}
>
{props.pageNo}
</button>
</div>
<PagerIcon
className={"e-next e-icons e-icon-next e-nextpage"}
icon={"chevron-right"}
onClick={props.nextPageClick}
/>
</PageWrapper>
</PagerContainer>
);
}

View File

@ -113,17 +113,25 @@ interface TableFilterProps {
borderRadius: string; borderRadius: string;
} }
const defaultFilters = [{ ...DEFAULT_FILTER }];
const getTableFilters = (filters: ReactTableFilter[] | undefined) => {
if (!filters || filters.length === 0) {
return defaultFilters;
}
return filters;
};
function TableFilterPaneContent(props: TableFilterProps) { function TableFilterPaneContent(props: TableFilterProps) {
const [filters, updateFilters] = React.useState( const [filters, updateFilters] = React.useState(
new Array<ReactTableFilter>(), getTableFilters(props.filters),
); );
useEffect(() => { useEffect(() => {
const filters: ReactTableFilter[] = props.filters ? [...props.filters] : []; const updatedFiltersState = getTableFilters(props.filters);
if (filters.length === 0) { //if props has been updated update the filters state
filters.push({ ...DEFAULT_FILTER }); if (updatedFiltersState !== filters) {
updateFilters(updatedFiltersState);
} }
updateFilters(filters);
}, [props.filters]); }, [props.filters]);
const addFilter = () => { const addFilter = () => {
@ -150,8 +158,8 @@ function TableFilterPaneContent(props: TableFilterProps) {
}; };
const clearFilters = useCallback(() => { const clearFilters = useCallback(() => {
props.applyFilter([{ ...DEFAULT_FILTER }]); props.applyFilter(defaultFilters);
}, []); }, [props]);
const columns: DropdownOption[] = props.columns const columns: DropdownOption[] = props.columns
.map((column: ReactTableColumnProps) => { .map((column: ReactTableColumnProps) => {

View File

@ -105,5 +105,5 @@ function TableFilters(props: TableFilterProps) {
</> </>
); );
} }
const TableFiltersMemoised = React.memo(TableFilters);
export default TableFilters; export default TableFiltersMemoised;

View File

@ -37,7 +37,7 @@ export interface AddNewRowBannerType {
disabledAddNewRowSave: boolean; disabledAddNewRowSave: boolean;
} }
export function AddNewRowBanner(props: AddNewRowBannerType) { function AddNewRowBannerComponent(props: AddNewRowBannerType) {
const [isDiscardLoading, setIsDiscardLoading] = useState(false); const [isDiscardLoading, setIsDiscardLoading] = useState(false);
const [isSaveLoading, setIsSaveLoading] = useState(false); const [isSaveLoading, setIsSaveLoading] = useState(false);
@ -81,3 +81,4 @@ export function AddNewRowBanner(props: AddNewRowBannerType) {
</Container> </Container>
); );
} }
export const AddNewRowBanner = React.memo(AddNewRowBannerComponent);

View File

@ -6,7 +6,7 @@ export interface BannerPropType extends AddNewRowBannerType {
isAddRowInProgress: boolean; isAddRowInProgress: boolean;
} }
export function Banner(props: BannerPropType) { function BannerComponent(props: BannerPropType) {
return props.isAddRowInProgress ? ( return props.isAddRowInProgress ? (
<AddNewRowBanner <AddNewRowBanner
accentColor={props.accentColor} accentColor={props.accentColor}
@ -17,3 +17,4 @@ export function Banner(props: BannerPropType) {
/> />
) : null; ) : null;
} }
export const Banner = React.memo(BannerComponent);

View File

@ -162,44 +162,47 @@ function ReactTableComponent(props: ReactTableComponentProps) {
width, width,
} = props; } = props;
const sortTableColumn = (columnIndex: number, asc: boolean) => { const sortTableColumn = useCallback(
if (allowSorting) { (columnIndex: number, asc: boolean) => {
if (columnIndex === -1) { if (allowSorting) {
_sortTableColumn("", asc); if (columnIndex === -1) {
} else { _sortTableColumn("", asc);
const column = columns[columnIndex]; } else {
const columnType = column.metaProperties?.type || ColumnTypes.TEXT; const column = columns[columnIndex];
if ( const columnType = column.metaProperties?.type || ColumnTypes.TEXT;
columnType !== ColumnTypes.IMAGE && if (
columnType !== ColumnTypes.VIDEO columnType !== ColumnTypes.IMAGE &&
) { columnType !== ColumnTypes.VIDEO
_sortTableColumn(column.alias, asc); ) {
_sortTableColumn(column.alias, asc);
}
} }
} }
} },
}; [_sortTableColumn, allowSorting, columns],
);
const selectTableRow = (row: { const selectTableRow = useCallback(
original: Record<string, unknown>; (row: { original: Record<string, unknown>; index: number }) => {
index: number; if (allowRowSelection) {
}) => { onRowClick(row.original, row.index);
if (allowRowSelection) {
onRowClick(row.original, row.index);
}
};
const toggleAllRowSelect = (
isSelect: boolean,
pageData: Row<Record<string, unknown>>[],
) => {
if (allowRowSelection) {
if (isSelect) {
selectAllRow(pageData);
} else {
unSelectAllRow(pageData);
} }
} },
}; [allowRowSelection, onRowClick],
);
const toggleAllRowSelect = useCallback(
(isSelect: boolean, pageData: Row<Record<string, unknown>>[]) => {
if (allowRowSelection) {
if (isSelect) {
selectAllRow(pageData);
} else {
unSelectAllRow(pageData);
}
}
},
[allowRowSelection, selectAllRow, unSelectAllRow],
);
const memoziedDisableDrag = useCallback( const memoziedDisableDrag = useCallback(
() => disableDrag(true), () => disableDrag(true),
@ -310,11 +313,13 @@ export default React.memo(ReactTableComponent, (prev, next) => {
prev.borderWidth === next.borderWidth && prev.borderWidth === next.borderWidth &&
prev.borderColor === next.borderColor && prev.borderColor === next.borderColor &&
prev.accentColor === next.accentColor && prev.accentColor === next.accentColor &&
//shallow equal possible
equal(prev.columnWidthMap, next.columnWidthMap) && equal(prev.columnWidthMap, next.columnWidthMap) &&
equal(prev.tableData, next.tableData) && //static reference
prev.tableData === next.tableData &&
// Using JSON stringify becuase isEqual doesnt work with functions, // Using JSON stringify becuase isEqual doesnt work with functions,
// and we are not changing the columns manually. // and we are not changing the columns manually.
JSON.stringify(prev.columns) === JSON.stringify(next.columns) && prev.columns === next.columns &&
equal(prev.editableCell, next.editableCell) && equal(prev.editableCell, next.editableCell) &&
prev.variant === next.variant && prev.variant === next.variant &&
prev.primaryColumnId === next.primaryColumnId && prev.primaryColumnId === next.primaryColumnId &&

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,164 @@
import { isBoolean, isArray, findIndex, isEqual } from "lodash";
import type { RenderMode } from "constants/WidgetConstants";
import { RenderModes } from "constants/WidgetConstants";
import { StickyType } from "../../component/Constants";
import {
COLUMN_MIN_WIDTH,
DEFAULT_COLUMN_WIDTH,
DEFAULT_COLUMN_NAME,
} from "../../constants";
import { fetchSticky } from "../utilities";
import type {
ColumnProperties,
ReactTableColumnProps,
} from "../../component/Constants";
import memoizeOne from "memoize-one";
export type getColumns = (
renderCell: any,
columnWidthMap: { [key: string]: number } | undefined,
orderedTableColumns: any,
componentWidth: number,
primaryColumns: Record<string, ColumnProperties>,
renderMode: RenderMode,
widgetId: string,
) => ReactTableColumnProps[];
//TODO: (Vamsi) need to unit test this function
export const getColumnsPureFn: getColumns = (
renderCell,
columnWidthMap = {},
orderedTableColumns = [],
componentWidth,
primaryColumns,
renderMode,
widgetId,
) => {
let columns: ReactTableColumnProps[] = [];
const hiddenColumns: ReactTableColumnProps[] = [];
let totalColumnWidth = 0;
if (isArray(orderedTableColumns)) {
orderedTableColumns.forEach((column: any) => {
const isHidden = !column.isVisible;
const columnData = {
id: column.id,
Header:
column.hasOwnProperty("label") && typeof column.label === "string"
? column.label
: DEFAULT_COLUMN_NAME,
alias: column.alias,
accessor: (row: any) => row[column.alias],
width: columnWidthMap[column.id] || DEFAULT_COLUMN_WIDTH,
minWidth: COLUMN_MIN_WIDTH,
draggable: true,
isHidden: false,
isAscOrder: column.isAscOrder,
isDerived: column.isDerived,
sticky: fetchSticky(column.id, primaryColumns, renderMode, widgetId),
metaProperties: {
isHidden: isHidden,
type: column.columnType,
format: column.outputFormat || "",
inputFormat: column.inputFormat || "",
},
columnProperties: column,
Cell: renderCell,
};
const isAllCellVisible: boolean | boolean[] = column.isCellVisible;
/*
* If all cells are not visible or column itself is not visible,
* set isHidden and push it to hiddenColumns array else columns array
*/
if (
(isBoolean(isAllCellVisible) && !isAllCellVisible) ||
(isArray(isAllCellVisible) &&
isAllCellVisible.every((visibility) => visibility === false)) ||
isHidden
) {
columnData.isHidden = true;
hiddenColumns.push(columnData);
} else {
totalColumnWidth += columnData.width;
columns.push(columnData);
}
});
}
const lastColumnIndex = columns.length - 1;
if (totalColumnWidth < componentWidth) {
/*
This "if" block is responsible for upsizing the last column width
if there is space left in the table container towards the right
*/
if (columns[lastColumnIndex]) {
const lastColumnWidth =
columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH;
const remainingWidth = componentWidth - totalColumnWidth;
// Adding the remaining width i.e. space left towards the right, to the last column width
columns[lastColumnIndex].width = lastColumnWidth + remainingWidth;
}
} else if (totalColumnWidth > componentWidth) {
/*
This "else-if" block is responsible for downsizing the last column width
if the last column spills over resulting in horizontal scroll
*/
const extraWidth = totalColumnWidth - componentWidth;
const lastColWidth = columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH;
/*
Below if condition explanation:
Condition 1: (lastColWidth > COLUMN_MIN_WIDTH)
We will downsize the last column only if its greater than COLUMN_MIN_WIDTH
Condition 2: (extraWidth < lastColWidth)
This condition checks whether the last column is the only column that is spilling over.
If more than one columns are spilling over we won't downsize the last column
*/
if (lastColWidth > COLUMN_MIN_WIDTH && extraWidth < lastColWidth) {
const availableWidthForLastColumn = lastColWidth - extraWidth;
/*
Below we are making sure last column width doesn't go lower than COLUMN_MIN_WIDTH again
as availableWidthForLastColumn might go lower than COLUMN_MIN_WIDTH in some cases
*/
columns[lastColumnIndex].width =
availableWidthForLastColumn < COLUMN_MIN_WIDTH
? COLUMN_MIN_WIDTH
: availableWidthForLastColumn;
}
}
if (hiddenColumns.length && renderMode === RenderModes.CANVAS) {
// Get the index of the first column that is frozen to right
const rightFrozenColumnIdx = findIndex(
columns,
(col) => col.sticky === StickyType.RIGHT,
);
if (rightFrozenColumnIdx !== -1) {
columns.splice(rightFrozenColumnIdx, 0, ...hiddenColumns);
} else {
columns = columns.concat(hiddenColumns);
}
}
return columns.filter((column: ReactTableColumnProps) => !!column.id);
};
// the result of this cache function is a prop for the useTable hook, this prop needs to memoised as per their docs
// we have noticed expensive computation from the useTable if columns isnt memoised
export const getMemoiseGetColumnsWithLocalStorageFn = () => {
const memoisedGetColumns = memoizeOne(getColumnsPureFn);
return memoizeOne(
//we are not using this parameter it is used by the memoisation comparator
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(widgetLocalStorageState) => {
memoisedGetColumns.clear();
return memoisedGetColumns as getColumns;
},
isEqual,
);
};

View File

@ -0,0 +1,162 @@
import log from "loglevel";
import type { MomentInput } from "moment";
import moment from "moment";
import _, { isNumber, isNil, isArray } from "lodash";
import type { EditableCell } from "../../constants";
import { ColumnTypes, DateInputFormat } from "../../constants";
import type { ReactTableColumnProps } from "../../component/Constants";
import memoizeOne from "memoize-one";
import shallowEqual from "shallowequal";
export type tableData = Array<Record<string, unknown>>;
//TODO: (Vamsi) need to unit test this function
export const transformDataPureFn = (
tableData: Array<Record<string, unknown>>,
columns: ReactTableColumnProps[],
): tableData => {
if (isArray(tableData)) {
return tableData.map((row, rowIndex) => {
const newRow: { [key: string]: any } = {};
columns.forEach((column) => {
const { alias } = column;
let value = row[alias];
if (column.metaProperties) {
switch (column.metaProperties.type) {
case ColumnTypes.DATE:
let isValidDate = true;
const outputFormat = _.isArray(column.metaProperties.format)
? column.metaProperties.format[rowIndex]
: column.metaProperties.format;
let inputFormat;
try {
const type = _.isArray(column.metaProperties.inputFormat)
? column.metaProperties.inputFormat[rowIndex]
: column.metaProperties.inputFormat;
if (
type !== DateInputFormat.EPOCH &&
type !== DateInputFormat.MILLISECONDS
) {
inputFormat = type;
moment(value as MomentInput, inputFormat);
} else if (!isNumber(value)) {
isValidDate = false;
}
} catch (e) {
isValidDate = false;
}
if (isValidDate && value) {
try {
if (
column.metaProperties.inputFormat ===
DateInputFormat.MILLISECONDS
) {
value = Number(value);
} else if (
column.metaProperties.inputFormat === DateInputFormat.EPOCH
) {
value = 1000 * Number(value);
}
newRow[alias] = moment(
value as MomentInput,
inputFormat,
).format(outputFormat);
} catch (e) {
log.debug("Unable to parse Date:", { e });
newRow[alias] = "";
}
} else if (value) {
newRow[alias] = "Invalid Value";
} else {
newRow[alias] = "";
}
break;
default:
let data;
if (
_.isString(value) ||
_.isNumber(value) ||
_.isBoolean(value)
) {
data = value;
} else if (isNil(value)) {
data = "";
} else {
data = JSON.stringify(value);
}
newRow[alias] = data;
break;
}
}
});
return newRow;
});
} else {
return [];
}
};
// lazily generate the cache so that we can create several memoised instances
const getMemoizedTransformData = () => memoizeOne(transformDataPureFn);
export const injectEditableCellToTableData = (
tableData: tableData,
editableCell: EditableCell | undefined,
): tableData => {
/*
* Inject the edited cell value from the editableCell object
*/
if (!editableCell || !tableData.length) return tableData;
const { column, index: updatedRowIndex, inputValue } = editableCell;
const inRangeForUpdate =
updatedRowIndex >= 0 && updatedRowIndex < tableData.length;
if (!inRangeForUpdate) return tableData;
//if same value ignore update
if (tableData[updatedRowIndex][column] === inputValue) return tableData;
//create copies of data
const copy = [...tableData];
copy[updatedRowIndex] = { ...copy[updatedRowIndex], [column]: inputValue };
return copy;
};
const getMemoiseInjectEditableCellToTableData = () =>
memoizeOne(injectEditableCellToTableData, (prev, next) => {
const [prevTableData, prevCellEditable] = prev;
const [nextTableData, nextCellEditable] = next;
//shallow compare the cellEditable properties
if (!shallowEqual(prevCellEditable, nextCellEditable)) return false;
return shallowEqual(prevTableData, nextTableData);
});
export type transformDataWithEditableCell = (
editableCell: EditableCell | undefined,
tableData: Array<Record<string, unknown>>,
columns: ReactTableColumnProps[],
) => tableData;
// the result of this cache function is a prop for the useTable hook, this prop needs to memoised as per their docs
// we have noticed expensive computation from the useTable if tableData isnt memoised
export const getMemoiseTransformDataWithEditableCell =
(): transformDataWithEditableCell => {
const memoizedTransformData = getMemoizedTransformData();
const memoiseInjectEditableCellToTableData =
getMemoiseInjectEditableCellToTableData();
return memoizeOne((editableCell, tableData, columns) => {
const transformedData = memoizedTransformData(tableData, columns);
return memoiseInjectEditableCellToTableData(
transformedData,
editableCell,
);
});
};

View File

@ -14943,9 +14943,10 @@ memfs@^3.2.2:
version "5.1.1" version "5.1.1"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz"
memoize-one@^5.2.1: memoize-one@^6.0.0:
version "5.2.1" version "6.0.0"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
memoizerific@^1.11.3: memoizerific@^1.11.3:
version "1.11.3" version "1.11.3"