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",
"mammoth": "^1.5.1",
"marked": "^4.0.18",
"memoize-one": "^5.2.1",
"memoize-one": "^6.0.0",
"micro-memoize": "^4.0.10",
"moment": "2.29.4",
"moment-timezone": "^0.5.35",

View File

@ -15,6 +15,9 @@ export interface UpdateWidgetMetaPropertyPayload {
propertyValue: unknown;
}
export interface BatchUpdateWidgetMetaPropertyPayload {
batchMetaUpdates: UpdateWidgetMetaPropertyPayload[];
}
export const updateWidgetMetaPropAndEval = (
widgetId: 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 = (
widgetId: string,
propertyName: string,

View File

@ -382,6 +382,7 @@ export const ReduxActionTypes = {
CREATE_WORKSPACE_SUCCESS: "CREATE_WORKSPACE_SUCCESS",
ADD_USER_TO_WORKSPACE_INIT: "ADD_USER_TO_WORKSPACE_INIT",
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_AND_EVAL: "SET_META_PROP_AND_EVAL",
META_UPDATE_DEBOUNCED_EVAL: "META_UPDATE_DEBOUNCED_EVAL",

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { createReducer } from "utils/ReducerUtils";
import type {
UpdateWidgetMetaPropertyPayload,
ResetWidgetMetaPayload,
BatchUpdateWidgetMetaPropertyPayload,
} from "actions/metaActions";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
@ -53,6 +54,20 @@ export const metaReducer = createReducer(initialState, {
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]: (
state: MetaState,
action: ReduxAction<UpdateWidgetMetaPropertyPayload>,

View File

@ -1,7 +1,7 @@
import React from "react";
import type { WidgetProps } 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 AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
@ -10,6 +10,12 @@ import type { ExecuteTriggerPayload } from "constants/AppsmithActionConstants/Ac
import { connect } from "react-redux";
import { getWidgetMetaProps } from "sagas/selectors";
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<
ExecuteTriggerPayload,
@ -17,8 +23,15 @@ export type DebouncedExecuteActionPayload = Omit<
> & {
dynamicString?: string;
};
export type batchUpdateWidgetMetaPropertyType = {
propertyName: string;
propertyValue: unknown;
actionExecution?: DebouncedExecuteActionPayload;
}[];
export interface WithMeta {
commitBatchMetaUpdates: () => void;
pushBatchMetaUpdates: pushAction;
updateWidgetMetaProperty: (
propertyName: string,
propertyValue: unknown,
@ -36,6 +49,7 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
initialMetaState: Record<string, unknown>;
actionsToExecute: Record<string, DebouncedExecuteActionPayload>;
batchMetaUpdates: batchUpdateWidgetMetaPropertyType;
updatedProperties: Record<string, boolean>;
constructor(props: metaHOCProps) {
super(props);
@ -47,6 +61,7 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
);
this.updatedProperties = {};
this.actionsToExecute = {};
this.batchMetaUpdates = [];
}
addPropertyForEval = (
@ -140,6 +155,112 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
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 = (
propertyName: string,
@ -165,10 +286,11 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
// 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) {
const metaPropPath = this.getMetaPropPath(propertyName);
if (metaOptions && metaPropPath) {
syncUpdateWidgetMetaProperty(
metaOptions.widgetId,
`${metaOptions.metaPropPrefix}.${this.props.widgetName}.${propertyName}[${metaOptions.index}]`,
metaPropPath,
propertyValue,
);
}
@ -194,6 +316,8 @@ function withMeta(WrappedWidget: typeof BaseWidget) {
return (
<WrappedWidget
{...this.updatedProps()}
commitBatchMetaUpdates={this.commitBatchMetaUpdates}
pushBatchMetaUpdates={this.pushBatchMetaUpdates}
updateWidgetMetaProperty={this.updateWidgetMetaProperty}
/>
);

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { memo } from "react";
import { CellWrapper } from "../TableStyledWrappers";
import type { BaseCellComponentProps } from "../Constants";
@ -18,7 +18,7 @@ export interface RenderActionProps extends BaseCellComponentProps {
onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void;
}
export function ButtonCell(props: RenderActionProps) {
function ButtonCellComponent(props: RenderActionProps) {
const {
allowCellWrapping,
cellBackground,
@ -84,3 +84,4 @@ export function ButtonCell(props: RenderActionProps) {
</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 { ALIGN_ITEMS, JUSTIFY_CONTENT } from "../Constants";
import { CellWrapper, TooltipContentWrapper } from "../TableStyledWrappers";
@ -59,7 +59,7 @@ type CheckboxCellProps = BaseCellComponentProps & {
disabledCheckboxMessage: string;
};
export const CheckboxCell = (props: CheckboxCellProps) => {
const CheckboxCellComponent = (props: CheckboxCellProps) => {
const {
accentColor,
borderRadius,
@ -122,3 +122,5 @@ export const CheckboxCell = (props: CheckboxCellProps) => {
</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 { ButtonColumnActions } from "widgets/TableWidgetV2/constants";
import { EditableCellActions } from "widgets/TableWidgetV2/constants";
@ -18,7 +17,7 @@ type RenderEditActionsProps = BaseCellComponentProps & {
onDiscard: () => void;
};
export function EditActionCell(props: RenderEditActionsProps) {
function EditActionCellComponent(props: RenderEditActionsProps) {
const {
allowCellWrapping,
cellBackground,
@ -91,3 +90,4 @@ export function EditActionCell(props: RenderEditActionsProps) {
</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 Check from "remixicon-react/CheckFillIcon";
import ArrowDownIcon from "remixicon-react/ArrowDownSLineIcon";
@ -145,7 +151,7 @@ type HeaderProps = {
) => void;
};
export const HeaderCell = (props: HeaderProps) => {
const HeaderCellComponent = (props: HeaderProps) => {
const { column, editMode, isSortable } = props;
const [isMenuOpen, setIsMenuOpen] = useState(false);
@ -335,3 +341,4 @@ export const HeaderCell = (props: HeaderProps) => {
</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 { HeaderCell } from "../cellComponents/HeaderCell";
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;
export function PageNumberInput(props: {
function PageNumberInputComponent(props: {
pageNo: number;
pageCount: number;
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;
}
const defaultFilters = [{ ...DEFAULT_FILTER }];
const getTableFilters = (filters: ReactTableFilter[] | undefined) => {
if (!filters || filters.length === 0) {
return defaultFilters;
}
return filters;
};
function TableFilterPaneContent(props: TableFilterProps) {
const [filters, updateFilters] = React.useState(
new Array<ReactTableFilter>(),
getTableFilters(props.filters),
);
useEffect(() => {
const filters: ReactTableFilter[] = props.filters ? [...props.filters] : [];
if (filters.length === 0) {
filters.push({ ...DEFAULT_FILTER });
const updatedFiltersState = getTableFilters(props.filters);
//if props has been updated update the filters state
if (updatedFiltersState !== filters) {
updateFilters(updatedFiltersState);
}
updateFilters(filters);
}, [props.filters]);
const addFilter = () => {
@ -150,8 +158,8 @@ function TableFilterPaneContent(props: TableFilterProps) {
};
const clearFilters = useCallback(() => {
props.applyFilter([{ ...DEFAULT_FILTER }]);
}, []);
props.applyFilter(defaultFilters);
}, [props]);
const columns: DropdownOption[] = props.columns
.map((column: ReactTableColumnProps) => {

View File

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

View File

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

View File

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

View File

@ -162,44 +162,47 @@ function ReactTableComponent(props: ReactTableComponentProps) {
width,
} = props;
const sortTableColumn = (columnIndex: number, asc: boolean) => {
if (allowSorting) {
if (columnIndex === -1) {
_sortTableColumn("", asc);
} else {
const column = columns[columnIndex];
const columnType = column.metaProperties?.type || ColumnTypes.TEXT;
if (
columnType !== ColumnTypes.IMAGE &&
columnType !== ColumnTypes.VIDEO
) {
_sortTableColumn(column.alias, asc);
const sortTableColumn = useCallback(
(columnIndex: number, asc: boolean) => {
if (allowSorting) {
if (columnIndex === -1) {
_sortTableColumn("", asc);
} else {
const column = columns[columnIndex];
const columnType = column.metaProperties?.type || ColumnTypes.TEXT;
if (
columnType !== ColumnTypes.IMAGE &&
columnType !== ColumnTypes.VIDEO
) {
_sortTableColumn(column.alias, asc);
}
}
}
}
};
},
[_sortTableColumn, allowSorting, columns],
);
const selectTableRow = (row: {
original: Record<string, unknown>;
index: number;
}) => {
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);
const selectTableRow = useCallback(
(row: { original: Record<string, unknown>; index: number }) => {
if (allowRowSelection) {
onRowClick(row.original, row.index);
}
}
};
},
[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(
() => disableDrag(true),
@ -310,11 +313,13 @@ export default React.memo(ReactTableComponent, (prev, next) => {
prev.borderWidth === next.borderWidth &&
prev.borderColor === next.borderColor &&
prev.accentColor === next.accentColor &&
//shallow equal possible
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,
// 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) &&
prev.variant === next.variant &&
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"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz"
memoize-one@^5.2.1:
version "5.2.1"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz"
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
memoizerific@^1.11.3:
version "1.11.3"