chore: send diff updates from worker (#24933)
## Description - Optimisation around evaluation updates to the state - Updates generation logic moved from main thread to worker thread - The diff between previous state and next state is less exacting to limit the number of updates - Logic to compress similar updates to reduce the diff updates sent from worker thread to main thread - Memoisation fixes and some selector optimisation for improved performance. #### PR fixes following issue(s) Fixes #24866 #### Type of change - Chore (housekeeping or task changes that don't impact user perception) ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [x] Manual - [x] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
parent
a4f9f05380
commit
ec922d1366
|
|
@ -155,7 +155,7 @@ describe("Validate Mongo URI CRUD with JSON Form", () => {
|
|||
|
||||
table.OpenNFilterTable("title", "contains", "USB");
|
||||
for (let i = 0; i < 3; i++) {
|
||||
table.ReadTableRowColumnData(i, 5, "v1").then(($cellData) => {
|
||||
table.ReadTableRowColumnData(i, 6, "v1").then(($cellData) => {
|
||||
expect($cellData).contains("USB");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import {
|
|||
ReduxActionTypes,
|
||||
} from "@appsmith/constants/ReduxActionConstants";
|
||||
import { intersection, union } from "lodash";
|
||||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import type { DependencyMap } from "utils/DynamicBindingUtils";
|
||||
import type { Diff } from "deep-diff";
|
||||
import type { QueryActionConfig } from "entities/Action";
|
||||
import type { DatasourceConfiguration } from "entities/Datasource";
|
||||
import type { DiffWithReferenceState } from "workers/Evaluation/helpers";
|
||||
|
||||
export const FIRST_EVAL_REDUX_ACTIONS = [
|
||||
// Pages
|
||||
|
|
@ -158,8 +157,8 @@ export function shouldLog(action: ReduxAction<unknown>) {
|
|||
}
|
||||
|
||||
export const setEvaluatedTree = (
|
||||
updates: Diff<DataTree, DataTree>[],
|
||||
): ReduxAction<{ updates: Diff<DataTree, DataTree>[] }> => {
|
||||
updates: DiffWithReferenceState[],
|
||||
): ReduxAction<{ updates: DiffWithReferenceState[] }> => {
|
||||
return {
|
||||
type: ReduxActionTypes.SET_EVALUATED_TREE,
|
||||
payload: { updates },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import { makeEntityConfigsAsObjProperties } from "@appsmith/workers/Evaluation/dataTreeUtils";
|
||||
import { smallDataSet } from "workers/Evaluation/__tests__/generateOpimisedUpdates.test";
|
||||
import produce from "immer";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const unevalTreeFromMainThread = {
|
||||
Api2: {
|
||||
|
|
@ -143,11 +146,215 @@ const unevalTreeFromMainThread = {
|
|||
};
|
||||
|
||||
describe("7. Test util methods", () => {
|
||||
it("3. makeDataTreeEntityConfigAsProperty method", () => {
|
||||
const dataTree = makeEntityConfigsAsObjProperties(
|
||||
unevalTreeFromMainThread as unknown as DataTree,
|
||||
);
|
||||
describe("makeDataTreeEntityConfigAsProperty", () => {
|
||||
it("should not introduce __evaluation__ property", () => {
|
||||
const dataTree = makeEntityConfigsAsObjProperties(
|
||||
unevalTreeFromMainThread as unknown as DataTree,
|
||||
);
|
||||
|
||||
expect(dataTree.Api2).not.toHaveProperty("__evaluation__");
|
||||
expect(dataTree.Api2).not.toHaveProperty("__evaluation__");
|
||||
});
|
||||
describe("identicalEvalPathsPatches decompress updates", () => {
|
||||
it("should decompress identicalEvalPathsPatches updates into evalProps and state", () => {
|
||||
const state = {
|
||||
Table1: {
|
||||
filteredTableData: smallDataSet,
|
||||
selectedRows: [],
|
||||
pageSize: 0,
|
||||
__evaluation__: {
|
||||
evaluatedValues: {},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const identicalEvalPathsPatches = {
|
||||
"Table1.__evaluation__.evaluatedValues.['filteredTableData']":
|
||||
"Table1.filteredTableData",
|
||||
};
|
||||
const evalProps = {
|
||||
Table1: {
|
||||
__evaluation__: {
|
||||
evaluatedValues: {
|
||||
someProp: "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const dataTree = makeEntityConfigsAsObjProperties(state as any, {
|
||||
sanitizeDataTree: true,
|
||||
evalProps,
|
||||
identicalEvalPathsPatches,
|
||||
});
|
||||
const expectedState = produce(state, (draft: any) => {
|
||||
draft.Table1.__evaluation__.evaluatedValues.someProp = "abc";
|
||||
draft.Table1.__evaluation__.evaluatedValues.filteredTableData =
|
||||
smallDataSet;
|
||||
});
|
||||
|
||||
expect(dataTree).toEqual(expectedState);
|
||||
//evalProps should have decompressed updates in it coming from identicalEvalPathsPatches
|
||||
const expectedEvalProps = produce(evalProps, (draft: any) => {
|
||||
draft.Table1.__evaluation__.evaluatedValues.filteredTableData =
|
||||
smallDataSet;
|
||||
});
|
||||
expect(evalProps).toEqual(expectedEvalProps);
|
||||
});
|
||||
|
||||
it("should not make any updates to evalProps when the identicalEvalPathsPatches is empty", () => {
|
||||
const state = {
|
||||
Table1: {
|
||||
filteredTableData: smallDataSet,
|
||||
selectedRows: [],
|
||||
pageSize: 0,
|
||||
__evaluation__: {
|
||||
evaluatedValues: {},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const identicalEvalPathsPatches = {};
|
||||
const initialEvalProps = {} as any;
|
||||
const evalProps = cloneDeep(initialEvalProps);
|
||||
const dataTree = makeEntityConfigsAsObjProperties(state, {
|
||||
sanitizeDataTree: true,
|
||||
evalProps,
|
||||
identicalEvalPathsPatches,
|
||||
});
|
||||
|
||||
expect(dataTree).toEqual(dataTree);
|
||||
//evalProps not be mutated with any updates
|
||||
expect(evalProps).toEqual(initialEvalProps);
|
||||
});
|
||||
|
||||
it("should ignore non relevant identicalEvalPathsPatches updates into evalProps and state", () => {
|
||||
const state = {
|
||||
Table1: {
|
||||
filteredTableData: smallDataSet,
|
||||
selectedRows: [],
|
||||
pageSize: 0,
|
||||
__evaluation__: {
|
||||
evaluatedValues: {},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
//ignore non existent widget state
|
||||
const identicalEvalPathsPatches = {
|
||||
"SomeWidget.__evaluation__.evaluatedValues.['filteredTableData']":
|
||||
"SomeWidget.filteredTableData",
|
||||
};
|
||||
|
||||
const initialEvalProps = {
|
||||
Table1: {
|
||||
__evaluation__: {
|
||||
evaluatedValues: {
|
||||
someProp: "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const evalProps = cloneDeep(initialEvalProps);
|
||||
const dataTree = makeEntityConfigsAsObjProperties(state, {
|
||||
sanitizeDataTree: true,
|
||||
evalProps,
|
||||
identicalEvalPathsPatches,
|
||||
});
|
||||
const expectedState = produce(state, (draft: any) => {
|
||||
draft.Table1.__evaluation__.evaluatedValues.someProp = "abc";
|
||||
});
|
||||
|
||||
expect(dataTree).toEqual(expectedState);
|
||||
//evalProps not be mutated with any updates
|
||||
expect(evalProps).toEqual(initialEvalProps);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serialise", () => {
|
||||
it("should clean out all functions in the generated state", () => {
|
||||
const state = {
|
||||
Table1: {
|
||||
filteredTableData: smallDataSet,
|
||||
selectedRows: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
someFn: () => {},
|
||||
pageSize: 0,
|
||||
__evaluation__: {
|
||||
evaluatedValues: {},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const identicalEvalPathsPatches = {
|
||||
"Table1.__evaluation__.evaluatedValues.['filteredTableData']":
|
||||
"Table1.filteredTableData",
|
||||
};
|
||||
const evalProps = {
|
||||
Table1: {
|
||||
__evaluation__: {
|
||||
evaluatedValues: {
|
||||
someProp: "abc",
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
someEvalFn: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const dataTree = makeEntityConfigsAsObjProperties(state, {
|
||||
sanitizeDataTree: true,
|
||||
evalProps,
|
||||
identicalEvalPathsPatches,
|
||||
}) as any;
|
||||
const expectedState = produce(state, (draft: any) => {
|
||||
draft.Table1.__evaluation__.evaluatedValues.someProp = "abc";
|
||||
delete draft.Table1.someFn;
|
||||
draft.Table1.__evaluation__.evaluatedValues.filteredTableData =
|
||||
smallDataSet;
|
||||
});
|
||||
|
||||
expect(dataTree).toEqual(expectedState);
|
||||
//function introduced by evalProps is cleaned out
|
||||
expect(
|
||||
dataTree.Table1.__evaluation__.evaluatedValues.someEvalFn,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should serialise bigInteger values", () => {
|
||||
const someBigInt = BigInt(121221);
|
||||
const state = {
|
||||
Table1: {
|
||||
pageSize: someBigInt,
|
||||
__evaluation__: {
|
||||
evaluatedValues: {},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const identicalEvalPathsPatches = {
|
||||
"Table1.__evaluation__.evaluatedValues.['pageSize']":
|
||||
"Table1.pageSize",
|
||||
};
|
||||
const evalProps = {
|
||||
Table1: {
|
||||
__evaluation__: {
|
||||
evaluatedValues: {
|
||||
someProp: someBigInt,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const dataTree = makeEntityConfigsAsObjProperties(state, {
|
||||
sanitizeDataTree: true,
|
||||
evalProps,
|
||||
identicalEvalPathsPatches,
|
||||
});
|
||||
const expectedState = produce(state, (draft: any) => {
|
||||
draft.Table1.pageSize = "121221";
|
||||
draft.Table1.__evaluation__.evaluatedValues.pageSize = "121221";
|
||||
draft.Table1.__evaluation__.evaluatedValues.someProp = "121221";
|
||||
});
|
||||
|
||||
expect(dataTree).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import { set } from "lodash";
|
||||
import { get, set, unset } from "lodash";
|
||||
import type { EvalProps } from "workers/common/DataTreeEvaluator";
|
||||
import { removeFunctions } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||
import { removeFunctionsAndSerialzeBigInt } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||
|
||||
/**
|
||||
* This method loops through each entity object of dataTree and sets the entity config from prototype as object properties.
|
||||
|
|
@ -12,21 +12,61 @@ export function makeEntityConfigsAsObjProperties(
|
|||
option = {} as {
|
||||
sanitizeDataTree?: boolean;
|
||||
evalProps?: EvalProps;
|
||||
identicalEvalPathsPatches?: Record<string, string>;
|
||||
},
|
||||
): DataTree {
|
||||
const { evalProps, sanitizeDataTree = true } = option;
|
||||
const {
|
||||
evalProps,
|
||||
identicalEvalPathsPatches,
|
||||
sanitizeDataTree = true,
|
||||
} = option;
|
||||
const newDataTree: DataTree = {};
|
||||
for (const entityName of Object.keys(dataTree)) {
|
||||
const entity = dataTree[entityName];
|
||||
newDataTree[entityName] = Object.assign({}, entity);
|
||||
}
|
||||
const dataTreeToReturn = sanitizeDataTree
|
||||
? JSON.parse(JSON.stringify(newDataTree))
|
||||
? removeFunctionsAndSerialzeBigInt(newDataTree)
|
||||
: newDataTree;
|
||||
|
||||
if (!evalProps) return dataTreeToReturn;
|
||||
|
||||
const sanitizedEvalProps = removeFunctions(evalProps) as EvalProps;
|
||||
//clean up deletes widget states
|
||||
Object.entries(identicalEvalPathsPatches || {}).forEach(
|
||||
([evalPath, statePath]) => {
|
||||
const [entity] = statePath.split(".");
|
||||
if (!dataTreeToReturn[entity]) {
|
||||
delete identicalEvalPathsPatches?.[evalPath];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// decompressIdenticalEvalPaths
|
||||
Object.entries(identicalEvalPathsPatches || {}).forEach(
|
||||
([evalPath, statePath]) => {
|
||||
const referencePathValue = get(dataTreeToReturn, statePath);
|
||||
set(evalProps, evalPath, referencePathValue);
|
||||
},
|
||||
);
|
||||
|
||||
const alreadySanitisedDataSet = {} as EvalProps;
|
||||
Object.keys(identicalEvalPathsPatches || {}).forEach((evalPath) => {
|
||||
const val = get(evalProps, evalPath);
|
||||
//serialised already
|
||||
alreadySanitisedDataSet[evalPath] = val;
|
||||
//we are seperating it from evalProps because we don't want to serialise this identical data unecessarily again
|
||||
unset(evalProps, evalPath);
|
||||
});
|
||||
|
||||
const sanitizedEvalProps = removeFunctionsAndSerialzeBigInt(
|
||||
evalProps,
|
||||
) as EvalProps;
|
||||
Object.entries(alreadySanitisedDataSet).forEach(([path, val]) => {
|
||||
// add it to sanitised Eval props
|
||||
set(sanitizedEvalProps, path, val);
|
||||
//restore it to evalProps
|
||||
set(evalProps, path, val);
|
||||
});
|
||||
for (const [entityName, entityEvalProps] of Object.entries(
|
||||
sanitizedEvalProps,
|
||||
)) {
|
||||
|
|
|
|||
|
|
@ -418,17 +418,17 @@ export function isDataTreeEntity(entity: unknown) {
|
|||
return !!entity && typeof entity === "object" && "ENTITY_TYPE" in entity;
|
||||
}
|
||||
|
||||
export const removeFunctionsAndSerialzeBigInt = (value: any) =>
|
||||
JSON.parse(
|
||||
JSON.stringify(value, (_, v) => (typeof v === "bigint" ? v.toString() : v)),
|
||||
);
|
||||
// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing
|
||||
// Check issue https://github.com/appsmithorg/appsmith/issues/719
|
||||
export const removeFunctions = (value: any) => {
|
||||
if (_.isFunction(value)) {
|
||||
return "Function call";
|
||||
} else if (_.isObject(value)) {
|
||||
return JSON.parse(
|
||||
JSON.stringify(value, (_, v) =>
|
||||
typeof v === "bigint" ? v.toString() : v,
|
||||
),
|
||||
);
|
||||
return removeFunctionsAndSerialzeBigInt(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,4 +199,4 @@ function Description(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default Description;
|
||||
export default React.memo(Description);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import type { AppState } from "@appsmith/reducers";
|
|||
import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors";
|
||||
import { importRemixIcon } from "design-system-old";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const AddLineIcon = importRemixIcon(
|
||||
() => import("remixicon-react/AddLineIcon"),
|
||||
);
|
||||
|
|
@ -44,12 +43,13 @@ export const useFilteredFileOperations = (query = "") => {
|
|||
const { appWideDS = [], otherDS = [] } = useAppWideAndOtherDatasource();
|
||||
const recentDatasourceIds = useSelector(getRecentDatasourceIds);
|
||||
// helper map for sorting based on recent usage
|
||||
const recentlyUsedOrderMap = recentDatasourceIds.reduce(
|
||||
(map: Record<string, number>, id, index) => {
|
||||
map[id] = index;
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
const recentlyUsedOrderMap = useMemo(
|
||||
() =>
|
||||
recentDatasourceIds.reduce((map: Record<string, number>, id, index) => {
|
||||
map[id] = index;
|
||||
return map;
|
||||
}, {}),
|
||||
[recentDatasourceIds],
|
||||
);
|
||||
/**
|
||||
* Work around to get the rest api cloud image.
|
||||
|
|
@ -89,7 +89,15 @@ export const useFilteredFileOperations = (query = "") => {
|
|||
canCreateDatasource,
|
||||
pagePermissions,
|
||||
),
|
||||
[query, appWideDS, otherDS],
|
||||
[
|
||||
appWideDS,
|
||||
canCreateActions,
|
||||
canCreateDatasource,
|
||||
otherDS,
|
||||
pagePermissions,
|
||||
query,
|
||||
recentlyUsedOrderMap,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -198,8 +206,8 @@ export const getFilteredAndSortedFileOperations = (
|
|||
|
||||
export const useFilteredWidgets = (query: string) => {
|
||||
const allWidgets = useSelector(getAllPageWidgets);
|
||||
const pages = useSelector(getPageList) || [];
|
||||
const pageMap = keyBy(pages, "pageId");
|
||||
const pages = useSelector(getPageList);
|
||||
const pageMap = useMemo(() => keyBy(pages || [], "pageId"), [pages]);
|
||||
const searchableWidgets = useMemo(
|
||||
() =>
|
||||
allWidgets.filter(
|
||||
|
|
@ -218,7 +226,7 @@ export const useFilteredWidgets = (query: string) => {
|
|||
|
||||
return isWidgetNameMatching || isPageNameMatching;
|
||||
});
|
||||
}, [allWidgets, query, pages]);
|
||||
}, [query, searchableWidgets, pageMap]);
|
||||
};
|
||||
|
||||
export const useFilteredActions = (query: string) => {
|
||||
|
|
@ -234,7 +242,7 @@ export const useFilteredActions = (query: string) => {
|
|||
|
||||
return isActionNameMatching || isPageNameMatching;
|
||||
});
|
||||
}, [actions, query, pages]);
|
||||
}, [query, actions, pageMap]);
|
||||
};
|
||||
|
||||
export const useFilteredJSCollections = (query: string) => {
|
||||
|
|
@ -252,12 +260,14 @@ export const useFilteredJSCollections = (query: string) => {
|
|||
|
||||
return isActionNameMatching || isPageNameMatching;
|
||||
});
|
||||
}, [jsActions, query, pages]);
|
||||
}, [query, jsActions, pageMap]);
|
||||
};
|
||||
|
||||
export const useFilteredPages = (query: string) => {
|
||||
const pages = useSelector(getPageList) || [];
|
||||
const pages = useSelector(getPageList);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!pages) return [];
|
||||
if (!query) return attachKind(pages, SEARCH_ITEM_TYPES.page);
|
||||
return attachKind(
|
||||
pages.filter(
|
||||
|
|
|
|||
|
|
@ -71,4 +71,4 @@ function ResultsNotFound() {
|
|||
);
|
||||
}
|
||||
|
||||
export default ResultsNotFound;
|
||||
export default React.memo(ResultsNotFound);
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ function SearchBox({ category, query, setCategory, setQuery }: SearchBoxProps) {
|
|||
setQuery(query);
|
||||
(document.querySelector("#global-search") as HTMLInputElement)?.focus();
|
||||
},
|
||||
[listenToChange],
|
||||
[listenToChange, setQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -162,4 +162,4 @@ function SearchBox({ category, query, setCategory, setQuery }: SearchBoxProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export default SearchBox;
|
||||
export default React.memo(SearchBox);
|
||||
|
|
|
|||
|
|
@ -477,4 +477,4 @@ function SearchResults({
|
|||
);
|
||||
}
|
||||
|
||||
export default SearchResults;
|
||||
export default React.memo(SearchResults);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import React, {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { shallowEqual, useDispatch, useSelector } from "react-redux";
|
||||
import styled, { ThemeProvider } from "styled-components";
|
||||
import { useParams } from "react-router";
|
||||
import history, { NavigationMethod } from "utils/history";
|
||||
|
|
@ -140,7 +140,7 @@ const getSortedResults = (
|
|||
};
|
||||
|
||||
const filterCategoryList = getFilterCategoryList();
|
||||
|
||||
const emptyObj = {};
|
||||
function GlobalSearch() {
|
||||
const currentPageId = useSelector(getCurrentPageId) as string;
|
||||
const modalOpen = useSelector(isModalOpenSelector);
|
||||
|
|
@ -160,7 +160,7 @@ function GlobalSearch() {
|
|||
(category: SearchCategory) => {
|
||||
dispatch(setGlobalSearchFilterContext({ category: category }));
|
||||
},
|
||||
[dispatch, setGlobalSearchFilterContext],
|
||||
[dispatch],
|
||||
);
|
||||
const params = useParams<ExplorerURLParams>();
|
||||
|
||||
|
|
@ -197,29 +197,32 @@ function GlobalSearch() {
|
|||
return state.entities.datasources.list.filter(
|
||||
(datasource) => datasource.id !== TEMP_DATASOURCE_ID,
|
||||
);
|
||||
});
|
||||
}, shallowEqual);
|
||||
const datasourcesList = useMemo(() => {
|
||||
return reducerDatasources.map((datasource) => ({
|
||||
...datasource,
|
||||
pageId: params?.pageId,
|
||||
}));
|
||||
}, [reducerDatasources]);
|
||||
}, [params?.pageId, reducerDatasources]);
|
||||
|
||||
const filteredDatasources = useMemo(() => {
|
||||
if (!query) return datasourcesList;
|
||||
return datasourcesList.filter((datasource) =>
|
||||
isMatching(datasource.name, query),
|
||||
);
|
||||
}, [reducerDatasources, query]);
|
||||
}, [datasourcesList, query]);
|
||||
const recentEntities = useRecentEntities();
|
||||
const recentEntityIds = recentEntities
|
||||
.map((r) => getEntityId(r))
|
||||
.filter(Boolean);
|
||||
const recentEntityIndex = (entity: any) => {
|
||||
const id =
|
||||
entity.id || entity.widgetId || entity.config?.id || entity.pageId;
|
||||
return recentEntityIds.indexOf(id);
|
||||
};
|
||||
const recentEntityIndex = useCallback(
|
||||
(entity: any) => {
|
||||
const id =
|
||||
entity.id || entity.widgetId || entity.config?.id || entity.pageId;
|
||||
return recentEntityIds.indexOf(id);
|
||||
},
|
||||
[recentEntityIds],
|
||||
);
|
||||
|
||||
const resetSearchQuery = useSelector(searchQuerySelector);
|
||||
const lastSelectedWidgetId = useSelector(getLastSelectedWidget);
|
||||
|
|
@ -276,16 +279,20 @@ function GlobalSearch() {
|
|||
currentPageId,
|
||||
);
|
||||
}, [
|
||||
filteredWidgets,
|
||||
category,
|
||||
currentPageId,
|
||||
filteredActions,
|
||||
filteredJSCollections,
|
||||
filteredDatasources,
|
||||
filteredFileOperations,
|
||||
filteredJSCollections,
|
||||
filteredPages,
|
||||
filteredWidgets,
|
||||
query,
|
||||
recentEntities,
|
||||
recentEntityIndex,
|
||||
]);
|
||||
|
||||
const activeItem = useMemo(() => {
|
||||
return searchResults[activeItemIndex] || {};
|
||||
return searchResults[activeItemIndex] || emptyObj;
|
||||
}, [searchResults, activeItemIndex]);
|
||||
|
||||
const getNextActiveItem = (nextIndex: number) => {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ import { get } from "lodash";
|
|||
import type { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
|
||||
import { FocusEntity } from "navigation/FocusEntity";
|
||||
import type { DataTreeEntityObject } from "entities/DataTree/dataTreeFactory";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const recentEntitiesSelector = (state: AppState) =>
|
||||
state.ui.globalSearch.recentEntities || [];
|
||||
|
||||
const emptyArr: any = [];
|
||||
const useResentEntities = (): Array<
|
||||
DataTreeEntityObject & {
|
||||
entityType: FocusEntity;
|
||||
|
|
@ -28,50 +29,56 @@ const useResentEntities = (): Array<
|
|||
return state.entities.datasources.list;
|
||||
});
|
||||
|
||||
const pages = useSelector(getPageList) || [];
|
||||
|
||||
return (recentEntities || [])
|
||||
.map((entity) => {
|
||||
const { id, pageId, type } = entity;
|
||||
if (type === FocusEntity.PAGE) {
|
||||
const result = pages.find((page) => page.pageId === id);
|
||||
if (result) {
|
||||
return {
|
||||
...result,
|
||||
entityType: type,
|
||||
kind: SEARCH_ITEM_TYPES.page,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (type === FocusEntity.DATASOURCE) {
|
||||
const datasource = reducerDatasources.find(
|
||||
(reducerDatasource) => reducerDatasource.id === id,
|
||||
);
|
||||
return (
|
||||
datasource && {
|
||||
...datasource,
|
||||
entityType: type,
|
||||
pageId,
|
||||
const pages = useSelector(getPageList);
|
||||
const result = useMemo(
|
||||
() =>
|
||||
(recentEntities || emptyArr)
|
||||
.map((entity) => {
|
||||
const { id, pageId, type } = entity;
|
||||
if (type === FocusEntity.PAGE) {
|
||||
if (!pages) return null;
|
||||
const result = pages.find((page) => page.pageId === id);
|
||||
if (result) {
|
||||
return {
|
||||
...result,
|
||||
entityType: type,
|
||||
kind: SEARCH_ITEM_TYPES.page,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (type === FocusEntity.DATASOURCE) {
|
||||
const datasource = reducerDatasources.find(
|
||||
(reducerDatasource) => reducerDatasource.id === id,
|
||||
);
|
||||
return (
|
||||
datasource && {
|
||||
...datasource,
|
||||
entityType: type,
|
||||
pageId,
|
||||
}
|
||||
);
|
||||
} else if (type === FocusEntity.API || type === FocusEntity.QUERY)
|
||||
return {
|
||||
...actions.find((action) => action?.config?.id === id),
|
||||
entityType: type,
|
||||
};
|
||||
else if (type === FocusEntity.JS_OBJECT)
|
||||
return {
|
||||
...jsActions.find(
|
||||
(action: JSCollectionData) => action?.config?.id === id,
|
||||
),
|
||||
entityType: type,
|
||||
};
|
||||
else if (type === FocusEntity.PROPERTY_PANE) {
|
||||
return { ...get(widgetsMap, id, null), entityType: type };
|
||||
}
|
||||
);
|
||||
} else if (type === FocusEntity.API || type === FocusEntity.QUERY)
|
||||
return {
|
||||
...actions.find((action) => action?.config?.id === id),
|
||||
entityType: type,
|
||||
};
|
||||
else if (type === FocusEntity.JS_OBJECT)
|
||||
return {
|
||||
...jsActions.find(
|
||||
(action: JSCollectionData) => action?.config?.id === id,
|
||||
),
|
||||
entityType: type,
|
||||
};
|
||||
else if (type === FocusEntity.PROPERTY_PANE) {
|
||||
return { ...get(widgetsMap, id, null), entityType: type };
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
})
|
||||
.filter(Boolean),
|
||||
[recentEntities, actions, jsActions, pages, reducerDatasources, widgetsMap],
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default useResentEntities;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import useHorizontalResize from "utils/hooks/useHorizontalResize";
|
|||
import { getIsDraggingForSelection } from "selectors/canvasSelectors";
|
||||
import MultiSelectPropertyPane from "pages/Editor/MultiSelectPropertyPane";
|
||||
import { getIsDraggingOrResizing } from "selectors/widgetSelectors";
|
||||
import equal from "fast-deep-equal";
|
||||
import { selectedWidgetsPresentInCanvas } from "selectors/propertyPaneSelectors";
|
||||
import { getIsAppSettingsPaneOpen } from "selectors/appSettingsPaneSelectors";
|
||||
import AppSettingsPane from "pages/Editor/AppSettingsPane";
|
||||
|
|
@ -72,7 +71,9 @@ export const PropertyPaneSidebar = memo((props: Props) => {
|
|||
const keepThemeWhileDragging =
|
||||
prevSelectedWidgetId.current === undefined && shouldNotRenderPane;
|
||||
|
||||
const selectedWidgets = useSelector(selectedWidgetsPresentInCanvas, equal);
|
||||
const selectedWidgetsLength = useSelector(
|
||||
(state) => selectedWidgetsPresentInCanvas(state).length,
|
||||
);
|
||||
|
||||
const isDraggingForSelection = useSelector(getIsDraggingForSelection);
|
||||
|
||||
|
|
@ -96,19 +97,19 @@ export const PropertyPaneSidebar = memo((props: Props) => {
|
|||
switch (true) {
|
||||
case isAppSettingsPaneOpen:
|
||||
return <AppSettingsPane />;
|
||||
case selectedWidgets.length > 1:
|
||||
case selectedWidgetsLength > 1:
|
||||
return <MultiSelectPropertyPane />;
|
||||
case selectedWidgets.length === 1:
|
||||
case selectedWidgetsLength === 1:
|
||||
if (shouldNotRenderPane) return <CanvasPropertyPane />;
|
||||
else return <WidgetPropertyPane />;
|
||||
case selectedWidgets.length === 0:
|
||||
case selectedWidgetsLength === 0:
|
||||
return <CanvasPropertyPane />;
|
||||
default:
|
||||
return <CanvasPropertyPane />;
|
||||
}
|
||||
}, [
|
||||
isAppSettingsPaneOpen,
|
||||
selectedWidgets.length,
|
||||
selectedWidgetsLength,
|
||||
isDraggingForSelection,
|
||||
shouldNotRenderPane,
|
||||
keepThemeWhileDragging,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export type ValidationResponse = {
|
|||
parsed: any;
|
||||
messages?: Array<Error>;
|
||||
transformed?: any;
|
||||
isParsedValueTheSame?: boolean;
|
||||
};
|
||||
|
||||
export type Validator = (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import type { MutableRefObject } from "react";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import { compact, get, groupBy } from "lodash";
|
||||
import type { Datasource } from "entities/Datasource";
|
||||
import { isStoredDatasource } from "entities/Action";
|
||||
import { debounce } from "lodash";
|
||||
import type { WidgetProps } from "widgets/BaseWidget";
|
||||
import log from "loglevel";
|
||||
import produce from "immer";
|
||||
|
|
@ -39,13 +37,6 @@ const findWidgets = (widgets: CanvasStructure, keyword: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const findDataSources = (dataSources: Datasource[], keyword: string) => {
|
||||
return dataSources.filter(
|
||||
(dataSource: Datasource) =>
|
||||
dataSource.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1,
|
||||
);
|
||||
};
|
||||
|
||||
export const useDatasourcesPageMapInCurrentApplication = () => {
|
||||
const actions = useActions();
|
||||
const reducerDatasources = useSelector((state: AppState) => {
|
||||
|
|
@ -76,56 +67,72 @@ export const useDatasourcesPageMapInCurrentApplication = () => {
|
|||
export const useCurrentApplicationDatasource = () => {
|
||||
const actions = useSelector(getActions);
|
||||
const allDatasources = useSelector(getDatasources);
|
||||
const datasourceIdsUsedInCurrentApplication = actions.reduce(
|
||||
(acc, action: ActionData) => {
|
||||
if (
|
||||
isStoredDatasource(action.config.datasource) &&
|
||||
action.config.datasource.id
|
||||
) {
|
||||
acc.add(action.config.datasource.id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Set(),
|
||||
);
|
||||
return allDatasources.filter((ds) =>
|
||||
datasourceIdsUsedInCurrentApplication.has(ds.id),
|
||||
);
|
||||
const currentApplicationDatasource = useMemo(() => {
|
||||
const datasourceIdsUsedInCurrentApplication = actions.reduce(
|
||||
(acc, action: ActionData) => {
|
||||
if (
|
||||
isStoredDatasource(action.config.datasource) &&
|
||||
action.config.datasource.id
|
||||
) {
|
||||
acc.add(action.config.datasource.id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Set(),
|
||||
);
|
||||
return allDatasources.filter((ds) =>
|
||||
datasourceIdsUsedInCurrentApplication.has(ds.id),
|
||||
);
|
||||
}, [actions, allDatasources]);
|
||||
|
||||
return currentApplicationDatasource;
|
||||
};
|
||||
|
||||
export const useOtherDatasourcesInWorkspace = () => {
|
||||
const actions = useSelector(getActions);
|
||||
const allDatasources = useSelector(getDatasources);
|
||||
const datasourceIdsUsedInCurrentApplication = actions.reduce(
|
||||
(acc, action: ActionData) => {
|
||||
if (
|
||||
isStoredDatasource(action.config.datasource) &&
|
||||
action.config.datasource.id
|
||||
) {
|
||||
acc.add(action.config.datasource.id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Set(),
|
||||
);
|
||||
return allDatasources.filter(
|
||||
(ds) =>
|
||||
!datasourceIdsUsedInCurrentApplication.has(ds.id) &&
|
||||
ds.id !== TEMP_DATASOURCE_ID,
|
||||
);
|
||||
const otherDatasourcesInWorkspace = useMemo(() => {
|
||||
const datasourceIdsUsedInCurrentApplication = actions.reduce(
|
||||
(acc, action: ActionData) => {
|
||||
if (
|
||||
isStoredDatasource(action.config.datasource) &&
|
||||
action.config.datasource.id
|
||||
) {
|
||||
acc.add(action.config.datasource.id);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Set(),
|
||||
);
|
||||
return allDatasources.filter(
|
||||
(ds) =>
|
||||
!datasourceIdsUsedInCurrentApplication.has(ds.id) &&
|
||||
ds.id !== TEMP_DATASOURCE_ID,
|
||||
);
|
||||
}, [actions, allDatasources]);
|
||||
return otherDatasourcesInWorkspace;
|
||||
};
|
||||
|
||||
export const useAppWideAndOtherDatasource = () => {
|
||||
const datasourcesUsedInApplication = useCurrentApplicationDatasource();
|
||||
const otherDatasourceInWorkspace = useOtherDatasourcesInWorkspace();
|
||||
|
||||
const appWideDS = useMemo(
|
||||
() =>
|
||||
[...datasourcesUsedInApplication].sort((ds1, ds2) =>
|
||||
ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()),
|
||||
),
|
||||
[datasourcesUsedInApplication],
|
||||
);
|
||||
const otherDS = useMemo(
|
||||
() =>
|
||||
[...otherDatasourceInWorkspace].sort((ds1, ds2) =>
|
||||
ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()),
|
||||
),
|
||||
[otherDatasourceInWorkspace],
|
||||
);
|
||||
return {
|
||||
appWideDS: datasourcesUsedInApplication.sort((ds1, ds2) =>
|
||||
ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()),
|
||||
),
|
||||
otherDS: otherDatasourceInWorkspace.sort((ds1, ds2) =>
|
||||
ds1.name?.toLowerCase()?.localeCompare(ds2.name?.toLowerCase()),
|
||||
),
|
||||
appWideDS,
|
||||
otherDS,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -143,69 +150,6 @@ export const useDatasourceSuggestions = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const useFilteredDatasources = (searchKeyword?: string) => {
|
||||
const pageIds = usePageIds(searchKeyword);
|
||||
const datasources = useDatasourcesPageMapInCurrentApplication();
|
||||
return useMemo(() => {
|
||||
if (searchKeyword) {
|
||||
const start = performance.now();
|
||||
const filteredDatasources = produce(datasources, (draft) => {
|
||||
for (const [key, value] of Object.entries(draft)) {
|
||||
if (pageIds.includes(key)) {
|
||||
draft[key] = value;
|
||||
} else {
|
||||
draft[key] = findDataSources(value, searchKeyword);
|
||||
}
|
||||
}
|
||||
});
|
||||
log.debug("Filtered datasources in:", performance.now() - start, "ms");
|
||||
return filteredDatasources;
|
||||
}
|
||||
|
||||
return datasources;
|
||||
}, [searchKeyword, datasources]);
|
||||
};
|
||||
|
||||
export const useJSCollections = (searchKeyword?: string) => {
|
||||
const reducerActions = useSelector(
|
||||
(state: AppState) => state.entities.jsActions,
|
||||
);
|
||||
const pageIds = usePageIds(searchKeyword);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
return groupBy(reducerActions, "config.pageId");
|
||||
}, [reducerActions]);
|
||||
|
||||
return useMemo(() => {
|
||||
if (searchKeyword) {
|
||||
const start = performance.now();
|
||||
const filteredActions = produce(actions, (draft) => {
|
||||
for (const [key, value] of Object.entries(draft)) {
|
||||
if (pageIds.includes(key)) {
|
||||
draft[key] = value;
|
||||
} else {
|
||||
value.forEach((action, index) => {
|
||||
const searchMatches =
|
||||
action.config.name
|
||||
.toLowerCase()
|
||||
.indexOf(searchKeyword.toLowerCase()) > -1;
|
||||
if (searchMatches) {
|
||||
draft[key][index] = action;
|
||||
} else {
|
||||
delete draft[key][index];
|
||||
}
|
||||
});
|
||||
}
|
||||
draft[key] = draft[key].filter(Boolean);
|
||||
}
|
||||
});
|
||||
log.debug("Filtered actions in:", performance.now() - start, "ms");
|
||||
return filteredActions;
|
||||
}
|
||||
return actions;
|
||||
}, [searchKeyword, actions]);
|
||||
};
|
||||
|
||||
export const useActions = (searchKeyword?: string) => {
|
||||
const reducerActions = useSelector(
|
||||
(state: AppState) => state.entities.actions,
|
||||
|
|
@ -243,7 +187,7 @@ export const useActions = (searchKeyword?: string) => {
|
|||
return filteredActions;
|
||||
}
|
||||
return actions;
|
||||
}, [searchKeyword, actions]);
|
||||
}, [searchKeyword, actions, pageIds]);
|
||||
};
|
||||
|
||||
export const useWidgets = (searchKeyword?: string) => {
|
||||
|
|
@ -272,7 +216,7 @@ export const useWidgets = (searchKeyword?: string) => {
|
|||
return filteredDSLs;
|
||||
}
|
||||
return pageCanvasStructures;
|
||||
}, [searchKeyword, pageCanvasStructures]);
|
||||
}, [searchKeyword, pageCanvasStructures, pageIds]);
|
||||
};
|
||||
|
||||
export const usePageIds = (searchKeyword?: string) => {
|
||||
|
|
@ -300,50 +244,6 @@ export const usePageIds = (searchKeyword?: string) => {
|
|||
}, [searchKeyword, pages]);
|
||||
};
|
||||
|
||||
export const useFilteredEntities = (
|
||||
ref: MutableRefObject<HTMLInputElement | null>,
|
||||
) => {
|
||||
const start = performance.now();
|
||||
const [searchKeyword, setSearchKeyword] = useState<string | null>(null);
|
||||
|
||||
const search = debounce((e: any) => {
|
||||
const keyword = e.target.value;
|
||||
if (keyword.trim().length > 0) {
|
||||
setSearchKeyword(keyword);
|
||||
} else {
|
||||
setSearchKeyword(null);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const event = new Event("cleared");
|
||||
useEffect(() => {
|
||||
const el: HTMLInputElement | null = ref.current;
|
||||
|
||||
el?.addEventListener("keydown", search);
|
||||
el?.addEventListener("cleared", search);
|
||||
return () => {
|
||||
el?.removeEventListener("keydown", search);
|
||||
el?.removeEventListener("cleared", search);
|
||||
};
|
||||
}, [ref, search]);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
const el: HTMLInputElement | null = ref.current;
|
||||
|
||||
if (el && el.value.trim().length > 0) {
|
||||
el.value = "";
|
||||
el?.dispatchEvent(event);
|
||||
}
|
||||
}, [ref, event]);
|
||||
|
||||
const stop = performance.now();
|
||||
log.debug("Explorer hook props calculations took", stop - start, "ms");
|
||||
return {
|
||||
searchKeyword: searchKeyword ?? undefined,
|
||||
clearSearch,
|
||||
};
|
||||
};
|
||||
|
||||
export const useEntityUpdateState = (entityId: string) => {
|
||||
return useSelector((state: AppState) =>
|
||||
get(state, "ui.explorer.entity.updatingEntity")?.includes(entityId),
|
||||
|
|
|
|||
|
|
@ -108,31 +108,40 @@ const useDependencyList = (name: string) => {
|
|||
);
|
||||
const guidedTour = useSelector(inGuidedTour);
|
||||
|
||||
const getEntityId = useCallback((name) => {
|
||||
const entity = dataTree[name];
|
||||
const getEntityId = useCallback(
|
||||
(name) => {
|
||||
const entity = dataTree[name];
|
||||
|
||||
if (isWidget(entity)) {
|
||||
return entity.widgetId;
|
||||
} else if (isAction(entity)) {
|
||||
return entity.actionId;
|
||||
}
|
||||
}, []);
|
||||
if (isWidget(entity)) {
|
||||
return entity.widgetId;
|
||||
} else if (isAction(entity)) {
|
||||
return entity.actionId;
|
||||
}
|
||||
},
|
||||
[dataTree],
|
||||
);
|
||||
|
||||
const entityDependencies = useMemo(() => {
|
||||
if (guidedTour) return null;
|
||||
return getDependenciesFromInverseDependencies(inverseDependencyMap, name);
|
||||
}, [name, inverseDependencyMap, guidedTour]);
|
||||
|
||||
const dependencyOptions =
|
||||
entityDependencies?.directDependencies.map((e) => ({
|
||||
label: e,
|
||||
value: getEntityId(e) ?? e,
|
||||
})) ?? [];
|
||||
const inverseDependencyOptions =
|
||||
entityDependencies?.inverseDependencies.map((e) => ({
|
||||
label: e,
|
||||
value: getEntityId(e),
|
||||
})) ?? [];
|
||||
const dependencyOptions = useMemo(
|
||||
() =>
|
||||
entityDependencies?.directDependencies.map((e) => ({
|
||||
label: e,
|
||||
value: getEntityId(e) ?? e,
|
||||
})) ?? [],
|
||||
[entityDependencies?.directDependencies, getEntityId],
|
||||
);
|
||||
const inverseDependencyOptions = useMemo(
|
||||
() =>
|
||||
entityDependencies?.inverseDependencies.map((e) => ({
|
||||
label: e,
|
||||
value: getEntityId(e),
|
||||
})) ?? [],
|
||||
[entityDependencies?.inverseDependencies, getEntityId],
|
||||
);
|
||||
|
||||
return {
|
||||
dependencyOptions,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
|
@ -37,17 +37,20 @@ export function PropertyPaneTab(props: PropertyPaneTabProps) {
|
|||
getSelectedPropertyTabIndex(state, props.panelPropertyPath),
|
||||
);
|
||||
|
||||
const setSelectedIndex = (index: number) => {
|
||||
dispatch(setSelectedPropertyTabIndex(index, props.panelPropertyPath));
|
||||
};
|
||||
|
||||
const setSelectedIndex = useCallback(
|
||||
(index: number) => {
|
||||
dispatch(setSelectedPropertyTabIndex(index, props.panelPropertyPath));
|
||||
},
|
||||
[dispatch, props.panelPropertyPath],
|
||||
);
|
||||
const onValueChange = useCallback(
|
||||
(value) => {
|
||||
setSelectedIndex(tabs.indexOf(value) || 0);
|
||||
},
|
||||
[setSelectedIndex],
|
||||
);
|
||||
return (
|
||||
<StyledTabs
|
||||
onValueChange={(value) => {
|
||||
setSelectedIndex(tabs.indexOf(value) || 0);
|
||||
}}
|
||||
value={tabs[selectedIndex]}
|
||||
>
|
||||
<StyledTabs onValueChange={onValueChange} value={tabs[selectedIndex]}>
|
||||
<TabsList>
|
||||
{props.contentComponent && <Tab value={tabs[0]}>Content</Tab>}
|
||||
{props.styleComponent && <Tab value={tabs[1]}>Style</Tab>}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import type { WidgetType } from "constants/WidgetConstants";
|
|||
import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants";
|
||||
import type { InteractionAnalyticsEventDetail } from "utils/AppsmithUtils";
|
||||
import { INTERACTION_ANALYTICS_EVENT } from "utils/AppsmithUtils";
|
||||
import { emitInteractionAnalyticsEvent } from "utils/AppsmithUtils";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { buildDeprecationWidgetMessage, isWidgetDeprecated } from "../utils";
|
||||
import { Button, Callout } from "design-system";
|
||||
|
|
@ -63,7 +62,7 @@ function PropertyPaneView(
|
|||
} & IPanelProps,
|
||||
) {
|
||||
const dispatch = useDispatch();
|
||||
const { ...panel } = props;
|
||||
const panel = props;
|
||||
const widgetProperties = useSelector(
|
||||
getWidgetPropsForPropertyPaneView,
|
||||
equal,
|
||||
|
|
@ -77,7 +76,7 @@ function PropertyPaneView(
|
|||
}
|
||||
|
||||
return true;
|
||||
}, [widgetProperties?.type, excludeList]);
|
||||
}, [widgetProperties]);
|
||||
const { searchText, setSearchText } = useSearchText("");
|
||||
const { pushFeature } = useContext(WalkthroughContext) || {};
|
||||
const widgets = useSelector(getWidgets);
|
||||
|
|
@ -174,19 +173,6 @@ function PropertyPaneView(
|
|||
*/
|
||||
const onCopy = useCallback(() => dispatch(copyWidget(false)), [dispatch]);
|
||||
|
||||
const handleTabKeyDownForButton = useCallback(
|
||||
(propertyName: string) => (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Tab")
|
||||
emitInteractionAnalyticsEvent(containerRef?.current, {
|
||||
key: e.key,
|
||||
propertyName,
|
||||
propertyType: "BUTTON",
|
||||
widgetType: widgetProperties?.type,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* actions shown on the right of title
|
||||
*/
|
||||
|
|
@ -220,7 +206,7 @@ function PropertyPaneView(
|
|||
),
|
||||
},
|
||||
];
|
||||
}, [onCopy, onDelete, handleTabKeyDownForButton]);
|
||||
}, [onCopy, onDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchText("");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { applyChange } from "deep-diff";
|
|||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import { createImmerReducer } from "utils/ReducerUtils";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { get } from "lodash";
|
||||
import type { DiffWithReferenceState } from "workers/Evaluation/helpers";
|
||||
|
||||
export type EvaluatedTreeState = DataTree;
|
||||
|
||||
|
|
@ -15,7 +17,7 @@ const evaluatedTreeReducer = createImmerReducer(initialState, {
|
|||
state: EvaluatedTreeState,
|
||||
action: ReduxAction<{
|
||||
dataTree: DataTree;
|
||||
updates: Diff<DataTree, DataTree>[];
|
||||
updates: DiffWithReferenceState[];
|
||||
removedPaths: [string];
|
||||
}>,
|
||||
) => {
|
||||
|
|
@ -29,7 +31,20 @@ const evaluatedTreeReducer = createImmerReducer(initialState, {
|
|||
continue;
|
||||
}
|
||||
try {
|
||||
applyChange(state, undefined, update);
|
||||
//these are the decompression updates, there are cases where identical values are present in the state
|
||||
//over here we have the path which has the identical value and apply as an update
|
||||
if (update.kind === "referenceState") {
|
||||
const { path, referencePath } = update;
|
||||
|
||||
const patch = {
|
||||
kind: "N",
|
||||
path,
|
||||
rhs: get(state, referencePath),
|
||||
} as Diff<DataTree, DataTree>;
|
||||
applyChange(state, undefined, patch);
|
||||
} else {
|
||||
applyChange(state, undefined, update);
|
||||
}
|
||||
} catch (e) {
|
||||
Sentry.captureException(e, {
|
||||
extra: {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ import {
|
|||
TriggerKind,
|
||||
} from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import { validate } from "workers/Evaluation/validations";
|
||||
import { diff } from "deep-diff";
|
||||
import { REPLAY_DELAY } from "entities/Replay/replayUtils";
|
||||
import type { EvaluationVersion } from "@appsmith/api/ApplicationApi";
|
||||
|
||||
|
|
@ -103,7 +102,6 @@ import { waitForWidgetConfigBuild } from "./InitSagas";
|
|||
import { logDynamicTriggerExecution } from "@appsmith/sagas/analyticsSaga";
|
||||
|
||||
const APPSMITH_CONFIGS = getAppsmithConfigs();
|
||||
|
||||
export const evalWorker = new GracefulWorkerService(
|
||||
new Worker(
|
||||
new URL("../workers/Evaluation/evaluation.worker.ts", import.meta.url),
|
||||
|
|
@ -132,7 +130,6 @@ export function* updateDataTreeHandler(
|
|||
|
||||
const {
|
||||
configTree,
|
||||
dataTree,
|
||||
dependencies,
|
||||
errors,
|
||||
evalMetaUpdates = [],
|
||||
|
|
@ -147,6 +144,7 @@ export function* updateDataTreeHandler(
|
|||
undefinedEvalValuesMap,
|
||||
unEvalUpdates,
|
||||
jsVarsCreatedEvent,
|
||||
updates,
|
||||
} = evalTreeResponse;
|
||||
|
||||
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
|
||||
|
|
@ -157,14 +155,13 @@ export function* updateDataTreeHandler(
|
|||
PerformanceTracker.startAsyncTracking(
|
||||
PerformanceTransactionName.SET_EVALUATED_TREE,
|
||||
);
|
||||
const oldDataTree: ReturnType<typeof getDataTree> = yield select(getDataTree);
|
||||
|
||||
const updates = diff(oldDataTree, dataTree) || [];
|
||||
|
||||
if (!isEmpty(staleMetaIds)) {
|
||||
yield put(resetWidgetsMetaState(staleMetaIds));
|
||||
}
|
||||
|
||||
yield put(setEvaluatedTree(updates));
|
||||
|
||||
ConfigTreeActions.setConfigTree(configTree);
|
||||
|
||||
PerformanceTracker.stopAsyncTracking(
|
||||
|
|
|
|||
|
|
@ -72,22 +72,28 @@ const getCurrentWidgetName = createSelector(
|
|||
export const getWidgetPropsForPropertyPane = createSelector(
|
||||
getCurrentWidgetProperties,
|
||||
getCurrentAppPositioningType,
|
||||
getDataTree,
|
||||
(state) => {
|
||||
const currentWidget = getCurrentWidgetProperties(state);
|
||||
if (!currentWidget) return;
|
||||
const evaluatedWidget = find(getDataTree(state), {
|
||||
widgetId: currentWidget.widgetId,
|
||||
}) as WidgetEntity;
|
||||
if (!evaluatedWidget) return;
|
||||
return evaluatedWidget[EVALUATION_PATH];
|
||||
},
|
||||
(
|
||||
widget: WidgetProps | undefined,
|
||||
appPositioningType,
|
||||
evaluatedTree: DataTree,
|
||||
evaluatedValue: any,
|
||||
): WidgetProps | undefined => {
|
||||
if (!widget) return undefined;
|
||||
const evaluatedWidget = find(evaluatedTree, {
|
||||
widgetId: widget.widgetId,
|
||||
}) as WidgetEntity;
|
||||
|
||||
const widgetProperties = {
|
||||
...widget,
|
||||
appPositioningType,
|
||||
};
|
||||
if (evaluatedWidget) {
|
||||
widgetProperties[EVALUATION_PATH] = evaluatedWidget[EVALUATION_PATH];
|
||||
if (evaluatedValue) {
|
||||
widgetProperties[EVALUATION_PATH] = evaluatedValue;
|
||||
}
|
||||
return widgetProperties;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function withWidgetProps(WrappedWidget: typeof BaseWidget) {
|
|||
type,
|
||||
widgetId,
|
||||
} = props;
|
||||
|
||||
const isPreviewMode = useSelector(previewModeSelector);
|
||||
const canvasWidget = useSelector((state: AppState) =>
|
||||
getWidget(state, widgetId),
|
||||
|
|
@ -82,6 +83,7 @@ function withWidgetProps(WrappedWidget: typeof BaseWidget) {
|
|||
const evaluatedWidget = useSelector((state: AppState) =>
|
||||
getWidgetEvalValues(state, widgetName),
|
||||
);
|
||||
|
||||
const isLoading = useSelector((state: AppState) =>
|
||||
getIsWidgetLoading(state, widgetName),
|
||||
);
|
||||
|
|
@ -316,7 +318,6 @@ function withWidgetProps(WrappedWidget: typeof BaseWidget) {
|
|||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return <WrappedWidget {...widgetProps} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
import produce from "immer";
|
||||
import { range } from "lodash";
|
||||
import { generateOptimisedUpdates } from "../helpers";
|
||||
|
||||
export const smallDataSet = [
|
||||
{
|
||||
address: "Paseo de Soledad Tur 245 Puerta 4 \nValencia, 50285",
|
||||
company: "Gonzalez Inc",
|
||||
},
|
||||
{
|
||||
address: "Ronda Rosalina Menéndez 72\nCuenca, 38057",
|
||||
company: "Gallego, Pedrosa and Conesa",
|
||||
},
|
||||
{
|
||||
address: "833 جزيني Gateway\nمشفقland, AL 63852",
|
||||
company: "اهرام-الحويطات",
|
||||
},
|
||||
];
|
||||
//size of about 300 elements
|
||||
const largeDataSet = range(100).flatMap(() => smallDataSet) as any;
|
||||
|
||||
const oldState = {
|
||||
Table1: {
|
||||
ENTITY_TYPE: "WIDGET",
|
||||
primaryColumns: {
|
||||
customColumn2: {
|
||||
isDisabled: false,
|
||||
},
|
||||
},
|
||||
tableData: [],
|
||||
filteredTableData: smallDataSet,
|
||||
selectedRows: [],
|
||||
pageSize: 0,
|
||||
triggerRowSelection: false,
|
||||
type: "TABLE_WIDGET_V2",
|
||||
__evaluation__: {
|
||||
errors: {
|
||||
transientTableData: [],
|
||||
tableData: [],
|
||||
processedTableData: [],
|
||||
},
|
||||
evaluatedValues: {
|
||||
filteredTableData: smallDataSet,
|
||||
transientTableData: {},
|
||||
tableData: [],
|
||||
processedTableData: [],
|
||||
"primaryColumns.customColumn2.isDisabled": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Select1: {
|
||||
value: "",
|
||||
ENTITY_TYPE: "WIDGET",
|
||||
options: [
|
||||
{
|
||||
label: "courtney68@example.net",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "osamusato@example.com",
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
type: "SELECT_WIDGET",
|
||||
__evaluation__: {
|
||||
errors: {
|
||||
options: [],
|
||||
},
|
||||
evaluatedValues: {
|
||||
"meta.value": "",
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("optimised diff updates", () => {
|
||||
describe("regular diff", () => {
|
||||
test("should generate regular diff updates when a simple property changes in the widget property segment", () => {
|
||||
const newState = produce(oldState, (draft) => {
|
||||
draft.Table1.pageSize = 17;
|
||||
});
|
||||
const updates = generateOptimisedUpdates(oldState, newState);
|
||||
expect(updates).toEqual([
|
||||
{ kind: "E", path: ["Table1", "pageSize"], lhs: 0, rhs: 17 },
|
||||
]);
|
||||
});
|
||||
test("should generate regular diff updates when a simple property changes in the __evaluation__ segment ", () => {
|
||||
const validationError = "Some validation error";
|
||||
const newState = produce(oldState, (draft) => {
|
||||
draft.Table1.__evaluation__.evaluatedValues.tableData =
|
||||
validationError as any;
|
||||
});
|
||||
const updates = generateOptimisedUpdates(oldState, newState);
|
||||
expect(updates).toEqual([
|
||||
{
|
||||
kind: "E",
|
||||
path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"],
|
||||
lhs: [],
|
||||
rhs: validationError,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("diffs with identicalEvalPathsPatches", () => {
|
||||
test("should not generate any updates when both the states are the same", () => {
|
||||
const updates = generateOptimisedUpdates(oldState, oldState);
|
||||
expect(updates).toEqual([]);
|
||||
const identicalEvalPathsPatches = {
|
||||
"Table1.__evaluation__.evaluatedValues.['tableData']":
|
||||
"Table1.tableData",
|
||||
};
|
||||
const updatesWithCompressionMap = generateOptimisedUpdates(
|
||||
oldState,
|
||||
oldState,
|
||||
identicalEvalPathsPatches,
|
||||
);
|
||||
expect(updatesWithCompressionMap).toEqual([]);
|
||||
});
|
||||
test("should generate the correct table data updates and reference state patches", () => {
|
||||
const identicalEvalPathsPatches = {
|
||||
"Table1.__evaluation__.evaluatedValues.['tableData']":
|
||||
"Table1.tableData",
|
||||
};
|
||||
const newState = produce(oldState, (draft) => {
|
||||
draft.Table1.tableData = largeDataSet;
|
||||
});
|
||||
const updates = generateOptimisedUpdates(
|
||||
oldState,
|
||||
newState,
|
||||
identicalEvalPathsPatches,
|
||||
);
|
||||
expect(updates).toEqual([
|
||||
{ kind: "N", path: ["Table1", "tableData"], rhs: largeDataSet },
|
||||
{
|
||||
kind: "referenceState",
|
||||
path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"],
|
||||
referencePath: "Table1.tableData",
|
||||
},
|
||||
]);
|
||||
});
|
||||
test("should not generate granular updates and generate a patch which replaces the complete collection when any change is made to the large collection ", () => {
|
||||
const largeDataSetWithSomeSimpleChange = produce(
|
||||
largeDataSet,
|
||||
(draft: any) => {
|
||||
//making a change to the first row of the collection
|
||||
draft[0].address = "some new address";
|
||||
//making a change to the second row of the collection
|
||||
draft[1].address = "some other new address";
|
||||
},
|
||||
) as any;
|
||||
const identicalEvalPathsPatches = {
|
||||
"Table1.__evaluation__.evaluatedValues.['tableData']":
|
||||
"Table1.tableData",
|
||||
};
|
||||
const evalVal = "some eval value" as any;
|
||||
|
||||
const newState = produce(oldState, (draft) => {
|
||||
//this value eval value should be ignores since we have provided identicalEvalPathsPatches which takes precedence
|
||||
draft.Table1.__evaluation__.evaluatedValues.tableData = evalVal as any;
|
||||
draft.Table1.tableData = largeDataSetWithSomeSimpleChange;
|
||||
});
|
||||
|
||||
const updates = generateOptimisedUpdates(
|
||||
oldState,
|
||||
newState,
|
||||
identicalEvalPathsPatches,
|
||||
);
|
||||
//should not see evalVal since identical eval path patches takes precedence
|
||||
expect(JSON.stringify(updates)).not.toContain(evalVal);
|
||||
expect(updates).toEqual([
|
||||
{
|
||||
kind: "N",
|
||||
path: ["Table1", "tableData"],
|
||||
//we should not see granular updates but complete replacement
|
||||
rhs: largeDataSetWithSomeSimpleChange,
|
||||
},
|
||||
{
|
||||
//compression patch
|
||||
kind: "referenceState",
|
||||
path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"],
|
||||
referencePath: "Table1.tableData",
|
||||
},
|
||||
]);
|
||||
});
|
||||
test("should not generate compression patches when there are no identical eval paths provided ", () => {
|
||||
const tableDataEvaluationValue = "someValidation error";
|
||||
const largeDataSetWithSomeSimpleChange = produce(
|
||||
largeDataSet,
|
||||
(draft: any) => {
|
||||
//making a change to the first row of the collection
|
||||
draft[0].address = "some new address";
|
||||
//making a change to the second row of the collection
|
||||
draft[1].address = "some other new address";
|
||||
},
|
||||
) as any;
|
||||
// empty indentical eval paths
|
||||
const identicalEvalPathsPatches = {};
|
||||
const newState = produce(oldState, (draft) => {
|
||||
draft.Table1.tableData = largeDataSetWithSomeSimpleChange;
|
||||
draft.Table1.__evaluation__.evaluatedValues.tableData =
|
||||
tableDataEvaluationValue as any;
|
||||
});
|
||||
const updates = generateOptimisedUpdates(
|
||||
oldState,
|
||||
newState,
|
||||
identicalEvalPathsPatches,
|
||||
);
|
||||
expect(updates).toEqual([
|
||||
{
|
||||
//considered as regular diff since there are no identical eval paths provided
|
||||
kind: "E",
|
||||
path: ["Table1", "__evaluation__", "evaluatedValues", "tableData"],
|
||||
lhs: [],
|
||||
rhs: tableDataEvaluationValue,
|
||||
},
|
||||
{
|
||||
kind: "N",
|
||||
path: ["Table1", "tableData"],
|
||||
//we should not see granular updates but complete replacement
|
||||
rhs: largeDataSetWithSomeSimpleChange,
|
||||
},
|
||||
]);
|
||||
});
|
||||
test("should not generate any update when the new state has the same value as the old state", () => {
|
||||
const oldStateSetWithSomeData = produce(oldState, (draft) => {
|
||||
draft.Table1.tableData = largeDataSet;
|
||||
draft.Table1.__evaluation__.evaluatedValues.tableData = largeDataSet;
|
||||
});
|
||||
const identicalEvalPathsPatches = {
|
||||
"Table1.__evaluation__.evaluatedValues.['tableData']":
|
||||
"Table1.tableData",
|
||||
};
|
||||
const newStateSetWithTheSameData = produce(oldState, (draft) => {
|
||||
//deliberating making a new instance of largeDataSet
|
||||
draft.Table1.tableData = [...largeDataSet] as any;
|
||||
});
|
||||
//since the old state has the same value as the new value..we wont generate a patch unnecessarily...
|
||||
const updates = generateOptimisedUpdates(
|
||||
oldStateSetWithSomeData,
|
||||
newStateSetWithTheSameData,
|
||||
identicalEvalPathsPatches,
|
||||
);
|
||||
expect(updates).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -14,6 +14,7 @@ import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActio
|
|||
import type { UpdateDataTreeMessageData } from "sagas/EvalWorkerActionSagas";
|
||||
import type { JSUpdate } from "utils/JSPaneUtils";
|
||||
import { setEvalContext } from "./evaluate";
|
||||
import { generateOptimisedUpdatesAndSetPrevState } from "./helpers";
|
||||
|
||||
export function evalTreeWithChanges(
|
||||
updatedValuePaths: string[][],
|
||||
|
|
@ -63,6 +64,8 @@ export function evalTreeWithChanges(
|
|||
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, {
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
identicalEvalPathsPatches:
|
||||
dataTreeEvaluator.getEvalPathsIdenticalToState(),
|
||||
});
|
||||
|
||||
/** Make sure evalMetaUpdates is sanitized to prevent postMessage failure */
|
||||
|
|
@ -75,8 +78,12 @@ export function evalTreeWithChanges(
|
|||
configTree = dataTreeEvaluator.oldConfigTree;
|
||||
}
|
||||
|
||||
const evalTreeResponse: EvalTreeResponseData = {
|
||||
const updates = generateOptimisedUpdatesAndSetPrevState(
|
||||
dataTree,
|
||||
dataTreeEvaluator,
|
||||
);
|
||||
const evalTreeResponse: EvalTreeResponseData = {
|
||||
updates,
|
||||
dependencies,
|
||||
errors,
|
||||
evalMetaUpdates,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import JSObjectCollection from "workers/Evaluation/JSObject/Collection";
|
|||
import { setEvalContext } from "../evaluate";
|
||||
import { getJSVariableCreatedEvents } from "../JSObject/JSVariableEvents";
|
||||
import { errorModifier } from "../errorModifier";
|
||||
import { generateOptimisedUpdatesAndSetPrevState } from "../helpers";
|
||||
|
||||
export let replayMap: Record<string, ReplayEntity<any>> | undefined;
|
||||
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
|
||||
|
|
@ -82,6 +83,8 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
identicalEvalPathsPatches:
|
||||
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
|
||||
});
|
||||
staleMetaIds = dataTreeResponse.staleMetaIds;
|
||||
} else if (dataTreeEvaluator.hasCyclicalDependency || forceEvaluation) {
|
||||
|
|
@ -122,6 +125,8 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
identicalEvalPathsPatches:
|
||||
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
|
||||
});
|
||||
staleMetaIds = dataTreeResponse.staleMetaIds;
|
||||
} else {
|
||||
|
|
@ -167,6 +172,8 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, {
|
||||
evalProps: dataTreeEvaluator.evalProps,
|
||||
identicalEvalPathsPatches:
|
||||
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
|
||||
});
|
||||
|
||||
evalMetaUpdates = JSON.parse(
|
||||
|
|
@ -201,6 +208,8 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
makeEntityConfigsAsObjProperties(unevalTree, {
|
||||
sanitizeDataTree: false,
|
||||
evalProps: dataTreeEvaluator?.evalProps,
|
||||
identicalEvalPathsPatches:
|
||||
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
|
||||
}),
|
||||
widgetTypeConfigMap,
|
||||
configTree,
|
||||
|
|
@ -210,8 +219,13 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
|
||||
const jsVarsCreatedEvent = getJSVariableCreatedEvents(jsUpdates);
|
||||
|
||||
const evalTreeResponse: EvalTreeResponseData = {
|
||||
const updates = generateOptimisedUpdatesAndSetPrevState(
|
||||
dataTree,
|
||||
dataTreeEvaluator,
|
||||
);
|
||||
|
||||
const evalTreeResponse: EvalTreeResponseData = {
|
||||
updates,
|
||||
dependencies,
|
||||
errors,
|
||||
evalMetaUpdates,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,18 @@
|
|||
import type { Diff } from "deep-diff";
|
||||
import { diff } from "deep-diff";
|
||||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import equal from "fast-deep-equal";
|
||||
import produce from "immer";
|
||||
import { get, isNumber, isObject, set } from "lodash";
|
||||
|
||||
export interface DiffReferenceState {
|
||||
kind: "referenceState";
|
||||
path: any[];
|
||||
referencePath: string;
|
||||
}
|
||||
export type DiffWithReferenceState =
|
||||
| Diff<DataTree, DataTree>
|
||||
| DiffReferenceState;
|
||||
// Finds the first index which is a duplicate value
|
||||
// Returns -1 if there are no duplicates
|
||||
// Returns the index of the first duplicate entry it finds
|
||||
|
|
@ -51,3 +66,210 @@ export const countOccurrences = (
|
|||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
const LARGE_COLLECTION_SIZE = 100;
|
||||
// for object paths which have a "." in the object key like "a.['b.c']"
|
||||
const REGEX_NESTED_OBJECT_PATH = /(.+)\.\[\'(.*)\'\]/;
|
||||
|
||||
const generateWithKey = (basePath: any, key: any) => {
|
||||
const segmentedPath = [...basePath, key];
|
||||
|
||||
if (isNumber(key)) {
|
||||
return {
|
||||
path: basePath.join(".") + ".[" + key + "]",
|
||||
segmentedPath,
|
||||
};
|
||||
}
|
||||
if (key.includes(".")) {
|
||||
return {
|
||||
path: basePath.join(".") + ".['" + key + "']",
|
||||
segmentedPath,
|
||||
};
|
||||
}
|
||||
return {
|
||||
path: basePath.join(".") + "." + key,
|
||||
segmentedPath,
|
||||
};
|
||||
};
|
||||
|
||||
const isLargeCollection = (val: any) => {
|
||||
if (!Array.isArray(val)) return false;
|
||||
const rowSize = !isObject(val[0]) ? 1 : Object.keys(val[0]).length;
|
||||
|
||||
const size = val.length * rowSize;
|
||||
|
||||
return size > LARGE_COLLECTION_SIZE;
|
||||
};
|
||||
|
||||
const normaliseEvalPath = (identicalEvalPathsPatches: any) =>
|
||||
Object.keys(identicalEvalPathsPatches || {}).reduce(
|
||||
(acc: any, evalPath: string) => {
|
||||
//for object paths which have a "." in the object key like "a.['b.c']", we need to extract these
|
||||
// paths and break them to appropriate patch paths
|
||||
|
||||
const matches = evalPath.match(REGEX_NESTED_OBJECT_PATH);
|
||||
if (!matches || !matches.length) {
|
||||
//regular paths like "a.b.c"
|
||||
acc[evalPath] = identicalEvalPathsPatches[evalPath];
|
||||
return acc;
|
||||
}
|
||||
|
||||
const [, firstSeg, nestedPathSeg] = matches;
|
||||
// normalise non nested paths like "a.['b']"
|
||||
if (!nestedPathSeg.includes(".")) {
|
||||
const key = [firstSeg, nestedPathSeg].join(".");
|
||||
acc[key] = identicalEvalPathsPatches[evalPath];
|
||||
return acc;
|
||||
}
|
||||
// object paths which have a "." like "a.['b.c']"
|
||||
const key = [firstSeg, `['${nestedPathSeg}']`].join(".");
|
||||
acc[key] = identicalEvalPathsPatches[evalPath];
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
//completely new updates which the diff will not traverse through needs to be attached
|
||||
const generateMissingSetPathsUpdates = (
|
||||
ignoreLargeKeys: any,
|
||||
ignoreLargeKeysHasBeenAttached: any,
|
||||
dataTree: any,
|
||||
): DiffWithReferenceState[] =>
|
||||
Object.keys(ignoreLargeKeys)
|
||||
.filter((evalPath) => !ignoreLargeKeysHasBeenAttached.has(evalPath))
|
||||
.map((evalPath) => {
|
||||
const statePath = ignoreLargeKeys[evalPath];
|
||||
//for object paths which have a "." in the object key like "a.['b.c']", we need to extract these
|
||||
// paths and break them to appropriate patch paths
|
||||
|
||||
//get the matching value from the widget properies in the data tree
|
||||
const val = get(dataTree, statePath);
|
||||
|
||||
const matches = evalPath.match(REGEX_NESTED_OBJECT_PATH);
|
||||
if (!matches || !matches.length) {
|
||||
//regular paths like "a.b.c"
|
||||
|
||||
return {
|
||||
kind: "N",
|
||||
path: evalPath.split("."),
|
||||
rhs: val,
|
||||
};
|
||||
}
|
||||
// object paths which have a "." like "a.['b.c']"
|
||||
const [, firstSeg, nestedPathSeg] = matches;
|
||||
const segmentedPath = [...firstSeg.split("."), nestedPathSeg];
|
||||
|
||||
return {
|
||||
kind: "N",
|
||||
path: segmentedPath,
|
||||
rhs: val,
|
||||
};
|
||||
});
|
||||
|
||||
const generateDiffUpdates = (
|
||||
oldDataTree: any,
|
||||
dataTree: any,
|
||||
ignoreLargeKeys: any,
|
||||
): DiffWithReferenceState[] => {
|
||||
const attachDirectly: DiffWithReferenceState[] = [];
|
||||
const ignoreLargeKeysHasBeenAttached = new Set();
|
||||
const attachLater: DiffWithReferenceState[] = [];
|
||||
const updates =
|
||||
diff(oldDataTree, dataTree, (path, key) => {
|
||||
if (!path.length || key === "__evaluation__") return false;
|
||||
|
||||
const { path: setPath, segmentedPath } = generateWithKey(path, key);
|
||||
|
||||
// if ignore path is present...this segment of code generates the data compression patches
|
||||
if (!!ignoreLargeKeys[setPath]) {
|
||||
const originalStateVal = get(oldDataTree, segmentedPath);
|
||||
const correspondingStatePath = ignoreLargeKeys[setPath];
|
||||
const statePathValue = get(dataTree, correspondingStatePath);
|
||||
if (!equal(originalStateVal, statePathValue)) {
|
||||
//reference state patches are a patch that does not have a patch value but it provides a path which contains the same value
|
||||
//this is helpful in making the payload sent to the main thread small
|
||||
attachLater.push({
|
||||
kind: "referenceState",
|
||||
path: segmentedPath,
|
||||
referencePath: correspondingStatePath,
|
||||
});
|
||||
}
|
||||
ignoreLargeKeysHasBeenAttached.add(setPath);
|
||||
return true;
|
||||
}
|
||||
const rhs = get(dataTree, segmentedPath);
|
||||
|
||||
const lhs = get(oldDataTree, segmentedPath);
|
||||
|
||||
const isLhsLarge = isLargeCollection(lhs);
|
||||
const isRhsLarge = isLargeCollection(rhs);
|
||||
if (!isLhsLarge && !isRhsLarge) {
|
||||
//perform diff on this node
|
||||
return false;
|
||||
}
|
||||
|
||||
//if either of values are large just directly attach it don't have to generate very granular updates
|
||||
|
||||
if ((!isLhsLarge && isRhsLarge) || (isLhsLarge && !isRhsLarge)) {
|
||||
attachDirectly.push({ kind: "N", path: segmentedPath, rhs });
|
||||
return true;
|
||||
}
|
||||
|
||||
//if the values are different attach the update directly
|
||||
!equal(lhs, rhs) &&
|
||||
attachDirectly.push({ kind: "N", path: segmentedPath, rhs });
|
||||
|
||||
//ignore diff on this node
|
||||
return true;
|
||||
}) || [];
|
||||
|
||||
const missingSetPaths = generateMissingSetPathsUpdates(
|
||||
ignoreLargeKeys,
|
||||
ignoreLargeKeysHasBeenAttached,
|
||||
dataTree,
|
||||
);
|
||||
|
||||
const largeDataSetUpdates = [
|
||||
...attachDirectly,
|
||||
...missingSetPaths,
|
||||
...attachLater,
|
||||
];
|
||||
return [...updates, ...largeDataSetUpdates];
|
||||
};
|
||||
|
||||
export const generateOptimisedUpdates = (
|
||||
oldDataTree: any,
|
||||
dataTree: any,
|
||||
identicalEvalPathsPatches?: Record<string, string>,
|
||||
): DiffWithReferenceState[] => {
|
||||
const ignoreLargeKeys = normaliseEvalPath(identicalEvalPathsPatches);
|
||||
const updates = generateDiffUpdates(oldDataTree, dataTree, ignoreLargeKeys);
|
||||
return updates;
|
||||
};
|
||||
|
||||
export const decompressIdenticalEvalPaths = (
|
||||
dataTree: any,
|
||||
identicalEvalPathsPatches: Record<string, string>,
|
||||
) =>
|
||||
produce(dataTree, (draft: any) =>
|
||||
Object.entries(identicalEvalPathsPatches || {}).forEach(([key, value]) => {
|
||||
const referencePathValue = get(dataTree, value);
|
||||
set(draft, key, referencePathValue);
|
||||
}),
|
||||
);
|
||||
|
||||
export const generateOptimisedUpdatesAndSetPrevState = (
|
||||
dataTree: any,
|
||||
dataTreeEvaluator: any,
|
||||
) => {
|
||||
const identicalEvalPathsPatches =
|
||||
dataTreeEvaluator?.getEvalPathsIdenticalToState();
|
||||
|
||||
const updates = generateOptimisedUpdates(
|
||||
dataTreeEvaluator?.getPrevState(),
|
||||
dataTree,
|
||||
identicalEvalPathsPatches,
|
||||
);
|
||||
|
||||
dataTreeEvaluator?.setPrevState(dataTree);
|
||||
return updates;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type {
|
||||
ConfigTree,
|
||||
DataTree,
|
||||
unEvalAndConfigTree,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import type { ActionValidationConfigMap } from "constants/PropertyControlConstants";
|
||||
|
|
@ -19,6 +18,7 @@ import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator
|
|||
import type { WorkerRequest } from "@appsmith/workers/common/types";
|
||||
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||
import type { APP_MODE } from "entities/App";
|
||||
import type { DiffWithReferenceState } from "./helpers";
|
||||
|
||||
export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>;
|
||||
export type EvalWorkerASyncRequest = WorkerRequest<
|
||||
|
|
@ -42,7 +42,6 @@ export interface EvalTreeRequestData {
|
|||
}
|
||||
|
||||
export interface EvalTreeResponseData {
|
||||
dataTree: DataTree;
|
||||
dependencies: DependencyMap;
|
||||
errors: EvalError[];
|
||||
evalMetaUpdates: EvalMetaUpdates;
|
||||
|
|
@ -58,6 +57,7 @@ export interface EvalTreeResponseData {
|
|||
isNewWidgetAdded: boolean;
|
||||
undefinedEvalValuesMap: Record<string, boolean>;
|
||||
jsVarsCreatedEvent?: { path: string; type: string }[];
|
||||
updates: DiffWithReferenceState[];
|
||||
}
|
||||
|
||||
export type JSVarMutatedEvents = Record<string, { path: string; type: string }>;
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ import {
|
|||
import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils";
|
||||
import {
|
||||
getValidatedTree,
|
||||
setToEvalPathsIdenticalToState,
|
||||
validateActionProperty,
|
||||
validateAndParseWidgetProperty,
|
||||
} from "./validationUtils";
|
||||
|
|
@ -164,8 +165,18 @@ export default class DataTreeEvaluator {
|
|||
* Sanitized eval values and errors
|
||||
*/
|
||||
evalProps: EvalProps = {};
|
||||
//when attaching values to __evaluations__ segment of the state there are cases where this value is identical to the widget property
|
||||
//in those cases do not it to the dataTree and update this map. The main thread can decompress these updates and we can minimise the data transfer
|
||||
evalPathsIdenticalToState: any = {};
|
||||
undefinedEvalValuesMap: Record<string, boolean> = {};
|
||||
|
||||
prevState = {};
|
||||
setPrevState(state: any) {
|
||||
this.prevState = state;
|
||||
}
|
||||
getPrevState() {
|
||||
return this.prevState;
|
||||
}
|
||||
public hasCyclicalDependency = false;
|
||||
constructor(
|
||||
widgetConfigMap: WidgetTypeConfigMap,
|
||||
|
|
@ -177,6 +188,9 @@ export default class DataTreeEvaluator {
|
|||
this.widgetConfigMap = widgetConfigMap;
|
||||
}
|
||||
|
||||
getEvalPathsIdenticalToState(): Record<string, string> {
|
||||
return this.evalPathsIdenticalToState || {};
|
||||
}
|
||||
getEvalTree() {
|
||||
return this.evalTree;
|
||||
}
|
||||
|
|
@ -344,6 +358,7 @@ export default class DataTreeEvaluator {
|
|||
evaluatedTree,
|
||||
{
|
||||
evalProps: this.evalProps,
|
||||
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
|
||||
},
|
||||
this.oldConfigTree,
|
||||
),
|
||||
|
|
@ -1028,6 +1043,7 @@ export default class DataTreeEvaluator {
|
|||
evalPropertyValue,
|
||||
unEvalPropertyValue,
|
||||
evalProps: this.evalProps,
|
||||
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
|
||||
});
|
||||
|
||||
this.setParsedValue({
|
||||
|
|
@ -1078,13 +1094,19 @@ export default class DataTreeEvaluator {
|
|||
}
|
||||
}
|
||||
|
||||
set(
|
||||
this.evalProps,
|
||||
getEvalValuePath(fullPropertyPath),
|
||||
evalPropertyValue,
|
||||
);
|
||||
set(currentTree, fullPropertyPath, evalPropertyValue);
|
||||
if (!propertyPath) return currentTree;
|
||||
|
||||
const evalPath = getEvalValuePath(fullPropertyPath);
|
||||
setToEvalPathsIdenticalToState({
|
||||
evalPath,
|
||||
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
|
||||
evalProps: this.evalProps,
|
||||
isParsedValueTheSame: true,
|
||||
statePath: fullPropertyPath,
|
||||
value: evalPropertyValue,
|
||||
});
|
||||
|
||||
set(currentTree, fullPropertyPath, evalPropertyValue);
|
||||
return currentTree;
|
||||
}
|
||||
case ENTITY_TYPE.JSACTION: {
|
||||
|
|
@ -1113,14 +1135,22 @@ export default class DataTreeEvaluator {
|
|||
!hasUnEvalValueModified && prevEvaluatedValue
|
||||
? prevEvaluatedValue
|
||||
: evalPropertyValue;
|
||||
set(
|
||||
this.evalProps,
|
||||
getEvalValuePath(fullPropertyPath, {
|
||||
isPopulated: true,
|
||||
fullPath: true,
|
||||
}),
|
||||
evalValue,
|
||||
);
|
||||
|
||||
const evalPath = getEvalValuePath(fullPropertyPath, {
|
||||
isPopulated: true,
|
||||
//what is the purpose of this argument
|
||||
fullPath: true,
|
||||
});
|
||||
|
||||
setToEvalPathsIdenticalToState({
|
||||
evalPath,
|
||||
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
|
||||
evalProps: this.evalProps,
|
||||
isParsedValueTheSame: true,
|
||||
statePath: fullPropertyPath,
|
||||
value: evalValue,
|
||||
});
|
||||
|
||||
set(currentTree, fullPropertyPath, evalValue);
|
||||
JSObjectCollection.setVariableValue(
|
||||
evalValue,
|
||||
|
|
@ -1461,6 +1491,7 @@ export default class DataTreeEvaluator {
|
|||
fullPath,
|
||||
) as unknown as string,
|
||||
evalProps: this.evalProps,
|
||||
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type {
|
|||
WidgetEntity,
|
||||
WidgetEntityConfig,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import { get, isUndefined, set } from "lodash";
|
||||
import { get, isObject, isUndefined, set } from "lodash";
|
||||
import type { EvaluationError } from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
getEvalValuePath,
|
||||
|
|
@ -21,10 +21,40 @@ import {
|
|||
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||
import { validate } from "workers/Evaluation/validations";
|
||||
import type { EvalProps } from ".";
|
||||
import type { ValidationResponse } from "constants/WidgetValidation";
|
||||
|
||||
const LARGE_COLLECTION_SIZE = 100;
|
||||
|
||||
const getIsLargeCollection = (val: any) => {
|
||||
if (!Array.isArray(val)) return false;
|
||||
const rowSize = !isObject(val[0]) ? 1 : Object.keys(val[0]).length;
|
||||
|
||||
const size = val.length * rowSize;
|
||||
|
||||
return size > LARGE_COLLECTION_SIZE;
|
||||
};
|
||||
export function setToEvalPathsIdenticalToState({
|
||||
evalPath,
|
||||
evalPathsIdenticalToState,
|
||||
evalProps,
|
||||
isParsedValueTheSame,
|
||||
statePath,
|
||||
value,
|
||||
}: any) {
|
||||
const isLargeCollection = getIsLargeCollection(value);
|
||||
|
||||
if (isParsedValueTheSame && isLargeCollection) {
|
||||
evalPathsIdenticalToState[evalPath] = statePath;
|
||||
} else {
|
||||
delete evalPathsIdenticalToState[evalPath];
|
||||
|
||||
set(evalProps, evalPath, value);
|
||||
}
|
||||
}
|
||||
export function validateAndParseWidgetProperty({
|
||||
configTree,
|
||||
currentTree,
|
||||
evalPathsIdenticalToState,
|
||||
evalPropertyValue,
|
||||
evalProps,
|
||||
fullPropertyPath,
|
||||
|
|
@ -38,6 +68,7 @@ export function validateAndParseWidgetProperty({
|
|||
evalPropertyValue: unknown;
|
||||
unEvalPropertyValue: string;
|
||||
evalProps: EvalProps;
|
||||
evalPathsIdenticalToState: any;
|
||||
}): unknown {
|
||||
const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath);
|
||||
if (isPathDynamicTrigger(widget, propertyPath)) {
|
||||
|
|
@ -85,14 +116,21 @@ export function validateAndParseWidgetProperty({
|
|||
configTree,
|
||||
});
|
||||
}
|
||||
set(
|
||||
|
||||
const evalPath = getEvalValuePath(fullPropertyPath, {
|
||||
isPopulated: false,
|
||||
fullPath: true,
|
||||
});
|
||||
const isParsedValueTheSame = parsed === evaluatedValue;
|
||||
|
||||
setToEvalPathsIdenticalToState({
|
||||
evalPath,
|
||||
evalPathsIdenticalToState,
|
||||
evalProps,
|
||||
getEvalValuePath(fullPropertyPath, {
|
||||
isPopulated: false,
|
||||
fullPath: true,
|
||||
}),
|
||||
evaluatedValue,
|
||||
);
|
||||
isParsedValueTheSame,
|
||||
statePath: fullPropertyPath,
|
||||
value: evaluatedValue,
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
|
@ -115,7 +153,7 @@ export function validateWidgetProperty(
|
|||
export function validateActionProperty(
|
||||
config: ValidationConfig,
|
||||
value: unknown,
|
||||
) {
|
||||
): ValidationResponse {
|
||||
if (!config) {
|
||||
return {
|
||||
isValid: true,
|
||||
|
|
@ -127,10 +165,10 @@ export function validateActionProperty(
|
|||
|
||||
export function getValidatedTree(
|
||||
tree: DataTree,
|
||||
option: { evalProps: EvalProps },
|
||||
option: { evalProps: EvalProps; evalPathsIdenticalToState: any },
|
||||
configTree: ConfigTree,
|
||||
) {
|
||||
const { evalProps } = option;
|
||||
const { evalPathsIdenticalToState, evalProps } = option;
|
||||
return Object.keys(tree).reduce((tree, entityKey: string) => {
|
||||
const entity = tree[entityKey];
|
||||
if (!isWidget(entity)) {
|
||||
|
|
@ -152,15 +190,21 @@ export function getValidatedTree(
|
|||
? value
|
||||
: transformed;
|
||||
|
||||
const isParsedValueTheSame = parsed === evaluatedValue;
|
||||
const fullPropertyPath = `${entityKey}.${property}`;
|
||||
set(
|
||||
const evalPath = getEvalValuePath(fullPropertyPath, {
|
||||
isPopulated: false,
|
||||
fullPath: true,
|
||||
});
|
||||
|
||||
setToEvalPathsIdenticalToState({
|
||||
evalPath,
|
||||
evalPathsIdenticalToState,
|
||||
evalProps,
|
||||
getEvalValuePath(fullPropertyPath, {
|
||||
isPopulated: false,
|
||||
fullPath: true,
|
||||
}),
|
||||
evaluatedValue,
|
||||
);
|
||||
isParsedValueTheSame,
|
||||
statePath: fullPropertyPath,
|
||||
value: evaluatedValue,
|
||||
});
|
||||
|
||||
resetValidationErrorsForEntityProperty({
|
||||
evalProps,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import forge from "node-forge";
|
|||
import { defaultLibraries } from "./index";
|
||||
import { JSLibraries, libraryReservedIdentifiers } from "./index";
|
||||
import { invalidEntityIdentifiers } from "../DependencyMap/utils";
|
||||
|
||||
const defaultLibImplementations = {
|
||||
lodash: _,
|
||||
moment: moment,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user