chore: Expose widget configuration to define properties that can set … (#16573)

This commit is contained in:
balajisoundar 2022-09-13 11:10:08 +05:30 committed by GitHub
parent 0970389143
commit 9bc5a5e076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 166 additions and 51 deletions

View File

@ -48,6 +48,7 @@ class WidgetFactory {
WidgetType, WidgetType,
readonly PropertyPaneConfig[] readonly PropertyPaneConfig[]
> = new Map(); > = new Map();
static loadingProperties: Map<WidgetType, Array<RegExp>> = new Map();
static widgetConfigMap: Map< static widgetConfigMap: Map<
WidgetType, WidgetType,
@ -64,6 +65,7 @@ class WidgetFactory {
propertyPaneContentConfig?: PropertyPaneConfig[], propertyPaneContentConfig?: PropertyPaneConfig[],
propertyPaneStyleConfig?: PropertyPaneConfig[], propertyPaneStyleConfig?: PropertyPaneConfig[],
features?: WidgetFeatures, features?: WidgetFeatures,
loadingProperties?: Array<RegExp>,
) { ) {
if (!this.widgetTypes[widgetType]) { if (!this.widgetTypes[widgetType]) {
this.widgetTypes[widgetType] = widgetType; this.widgetTypes[widgetType] = widgetType;
@ -71,6 +73,8 @@ class WidgetFactory {
this.derivedPropertiesMap.set(widgetType, derivedPropertiesMap); this.derivedPropertiesMap.set(widgetType, derivedPropertiesMap);
this.defaultPropertiesMap.set(widgetType, defaultPropertiesMap); this.defaultPropertiesMap.set(widgetType, defaultPropertiesMap);
this.metaPropertiesMap.set(widgetType, metaPropertiesMap); this.metaPropertiesMap.set(widgetType, metaPropertiesMap);
loadingProperties &&
this.loadingProperties.set(widgetType, loadingProperties);
if (propertyPaneConfig) { if (propertyPaneConfig) {
const enhancedPropertyPaneConfig = enhancePropertyPaneConfig( const enhancedPropertyPaneConfig = enhancePropertyPaneConfig(
@ -244,6 +248,10 @@ class WidgetFactory {
}); });
return typeConfigMap; return typeConfigMap;
} }
static getLoadingProperties(type: WidgetType): Array<RegExp> | undefined {
return this.loadingProperties.get(type);
}
} }
export type WidgetTypeConfigMap = Record< export type WidgetTypeConfigMap = Record<

View File

@ -7,9 +7,10 @@ import {
} from "entities/DataTree/dataTreeFactory"; } from "entities/DataTree/dataTreeFactory";
import { import {
findLoadingEntities, findLoadingEntities,
getEntityDependants, getEntityDependantPaths,
groupAndFilterDependantsMap, groupAndFilterDependantsMap,
} from "utils/WidgetLoadingStateUtils"; } from "utils/WidgetLoadingStateUtils";
import WidgetFactory from "./WidgetFactory";
const JS_object_tree: DataTreeJSAction = { const JS_object_tree: DataTreeJSAction = {
pluginType: PluginType.JS, pluginType: PluginType.JS,
@ -72,14 +73,64 @@ const Query_tree: DataTreeAction = {
isLoading: false, isLoading: false,
}; };
const Api_tree: DataTreeAction = {
data: {},
actionId: "",
config: {},
pluginType: PluginType.API,
pluginId: "",
name: "",
run: {},
clear: {},
dynamicBindingPathList: [],
bindingPaths: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
dependencyMap: {},
logBlackList: {},
datasourceUrl: "",
responseMeta: {
isExecutionSuccess: true,
},
isLoading: false,
reactivePaths: {},
};
const Table_tree: DataTreeWidget = {
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
bindingPaths: {},
triggerPaths: {},
validationPaths: {},
logBlackList: {},
propertyOverrideDependency: {},
overridingPropertyPaths: {},
privateWidgets: {},
widgetId: "",
type: "TABLE_WIDGET",
widgetName: "",
renderMode: "CANVAS",
version: 0,
parentColumnSpace: 0,
parentRowSpace: 0,
leftColumn: 0,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
isLoading: false,
animateLoading: true,
reactivePaths: {},
meta: {},
};
const baseDataTree = { const baseDataTree = {
JS_file: { ...JS_object_tree, name: "JS_file" }, JS_file: { ...JS_object_tree, name: "JS_file" },
Select1: { ...Select_tree, name: "Select1" }, Select1: { ...Select_tree, name: "Select1" },
Select2: { ...Select_tree, name: "Select2" }, Select2: { ...Select_tree, name: "Select2" },
Select3: { ...Select_tree, name: "Select3" }, Select3: { ...Select_tree, name: "Select3" },
Table1: { ...Table_tree, name: "Table1" },
Query1: { ...Query_tree, name: "Query1" }, Query1: { ...Query_tree, name: "Query1" },
Query2: { ...Query_tree, name: "Query2" }, Query2: { ...Query_tree, name: "Query2" },
Query3: { ...Query_tree, name: "Query3" }, Query3: { ...Query_tree, name: "Query3" },
Api1: { ...Api_tree, name: "Api1" },
}; };
describe("Widget loading state utils", () => { describe("Widget loading state utils", () => {
@ -120,6 +171,22 @@ describe("Widget loading state utils", () => {
], ],
}; };
beforeAll(() => {
// mock WidgetFactory.getLoadingProperties
const loadingPropertiesMap = new Map<string, RegExp[]>();
loadingPropertiesMap.set("TABLE_WIDGET", [/.tableData$/]);
jest
.spyOn(WidgetFactory, "getLoadingProperties")
.mockImplementation((widgetType) =>
loadingPropertiesMap.get(widgetType),
);
});
afterAll(() => {
jest.restoreAllMocks();
});
// Select1.options -> JS_file.func1 -> Query1.data // Select1.options -> JS_file.func1 -> Query1.data
it("handles linear dependencies", () => { it("handles linear dependencies", () => {
const loadingEntites = findLoadingEntities( const loadingEntites = findLoadingEntities(
@ -251,6 +318,20 @@ describe("Widget loading state utils", () => {
}); });
expect(loadingEntites).toStrictEqual(new Set(["Select2"])); expect(loadingEntites).toStrictEqual(new Set(["Select2"]));
}); });
it("includes loading properties", () => {
const loadingEntites = findLoadingEntities(["Api1"], baseDataTree, {
"Api1.data": ["Table1.tableData"],
});
expect(loadingEntites).toStrictEqual(new Set(["Table1"]));
});
it("ignores non-loading properties", () => {
const loadingEntites = findLoadingEntities(["Api1"], baseDataTree, {
"Api1.run": ["Table1.primaryColumns.action.onClick"],
});
expect(loadingEntites).toStrictEqual(new Set());
});
}); });
describe("groupAndFilterDependantsMap", () => { describe("groupAndFilterDependantsMap", () => {
@ -331,10 +412,10 @@ describe("Widget loading state utils", () => {
}); });
}); });
describe("getEntityDependants", () => { describe("getEntityDependantPaths", () => {
// Select1.options -> JS_file.func1 -> Query1.data // Select1.options -> JS_file.func1 -> Query1.data
it("handles simple dependency", () => { it("handles simple dependency", () => {
const dependants = getEntityDependants( const dependants = getEntityDependantPaths(
["Query1"], ["Query1"],
{ {
Query1: { Query1: {
@ -346,16 +427,15 @@ describe("Widget loading state utils", () => {
}, },
new Set<string>(), new Set<string>(),
); );
expect(dependants).toStrictEqual({ expect(dependants).toStrictEqual(
names: new Set(["JS_file", "Select1"]), new Set(["JS_file.func1", "Select1.options"]),
fullPaths: new Set(["JS_file.func1", "Select1.options"]), );
});
}); });
// Select1.options -> JS_file.func1 -> Query1.data // Select1.options -> JS_file.func1 -> Query1.data
// Select2.options -> JS_file.func2 -> Query1.data // Select2.options -> JS_file.func2 -> Query1.data
it("handles multiple dependencies", () => { it("handles multiple dependencies", () => {
const dependants = getEntityDependants( const dependants = getEntityDependantPaths(
["Query1"], ["Query1"],
{ {
Query1: { Query1: {
@ -368,19 +448,18 @@ describe("Widget loading state utils", () => {
}, },
new Set<string>(), new Set<string>(),
); );
expect(dependants).toStrictEqual({ expect(dependants).toStrictEqual(
names: new Set(["JS_file", "Select1", "Select2"]), new Set([
fullPaths: new Set([
"JS_file.func1", "JS_file.func1",
"Select1.options", "Select1.options",
"JS_file.func2", "JS_file.func2",
"Select2.options", "Select2.options",
]), ]),
}); );
}); });
it("handles specific entity paths", () => { it("handles specific entity paths", () => {
const dependants = getEntityDependants( const dependants = getEntityDependantPaths(
["JS_file.func2"], // specific path ["JS_file.func2"], // specific path
{ {
Query1: { Query1: {
@ -396,15 +475,12 @@ describe("Widget loading state utils", () => {
}, },
new Set<string>(), new Set<string>(),
); );
expect(dependants).toStrictEqual({ expect(dependants).toStrictEqual(new Set(["Select2.options"]));
names: new Set(["Select2"]),
fullPaths: new Set(["Select2.options"]),
});
}); });
// Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data // Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data
it("handles JS self-dependencies", () => { it("handles JS self-dependencies", () => {
const dependants = getEntityDependants( const dependants = getEntityDependantPaths(
["Query1"], ["Query1"],
{ {
Query1: { Query1: {
@ -417,19 +493,14 @@ describe("Widget loading state utils", () => {
}, },
new Set<string>(), new Set<string>(),
); );
expect(dependants).toStrictEqual({ expect(dependants).toStrictEqual(
names: new Set(["JS_file", "Select1"]), new Set(["JS_file.internalFunc", "JS_file.func1", "Select1.options"]),
fullPaths: new Set([ );
"JS_file.internalFunc",
"JS_file.func1",
"Select1.options",
]),
});
}); });
// Select1.options -> JS_file.func -> JS_file.internalFunc1 -> JS_file.internalFunc2 -> Query1.data // Select1.options -> JS_file.func -> JS_file.internalFunc1 -> JS_file.internalFunc2 -> Query1.data
it("handles nested JS self-dependencies", () => { it("handles nested JS self-dependencies", () => {
const dependants = getEntityDependants( const dependants = getEntityDependantPaths(
["Query1"], ["Query1"],
{ {
Query1: { Query1: {
@ -443,15 +514,14 @@ describe("Widget loading state utils", () => {
}, },
new Set<string>(), new Set<string>(),
); );
expect(dependants).toStrictEqual({ expect(dependants).toStrictEqual(
names: new Set(["JS_file", "Select1"]), new Set([
fullPaths: new Set([
"JS_file.internalFunc1", "JS_file.internalFunc1",
"JS_file.internalFunc2", "JS_file.internalFunc2",
"JS_file.func", "JS_file.func",
"Select1.options", "Select1.options",
]), ]),
}); );
}); });
/* Select1.options -> JS.func1 -> Query1.data, /* Select1.options -> JS.func1 -> Query1.data,
@ -462,7 +532,7 @@ describe("Widget loading state utils", () => {
Only Select2 should be listed, not Select1. Only Select2 should be listed, not Select1.
*/ */
it("handles selective dependencies in same JS file", () => { it("handles selective dependencies in same JS file", () => {
const dependants = getEntityDependants( const dependants = getEntityDependantPaths(
["Query2"], ["Query2"],
{ {
Query1: { Query1: {
@ -478,10 +548,9 @@ describe("Widget loading state utils", () => {
}, },
new Set<string>(), new Set<string>(),
); );
expect(dependants).toStrictEqual({ expect(dependants).toStrictEqual(
names: new Set(["JS_file", "Select2"]), new Set(["JS_file.func2", "Select2.options"]),
fullPaths: new Set(["JS_file.func2", "Select2.options"]), );
});
}); });
}); });
}); });

View File

@ -1,7 +1,8 @@
import { DataTree } from "entities/DataTree/dataTreeFactory"; import { DataTree } from "entities/DataTree/dataTreeFactory";
import { get, set } from "lodash"; import { get, set } from "lodash";
import { isJSObject } from "workers/evaluationUtils"; import { isJSObject, isWidget } from "workers/evaluationUtils";
import { DependencyMap } from "./DynamicBindingUtils"; import { DependencyMap } from "./DynamicBindingUtils";
import WidgetFactory from "./WidgetFactory";
type GroupedDependencyMap = Record<string, DependencyMap>; type GroupedDependencyMap = Record<string, DependencyMap>;
@ -57,13 +58,13 @@ export const groupAndFilterDependantsMap = (
return entitiesDepMap; return entitiesDepMap;
}; };
// get entities that depend on a given list of entites // get entity paths that depend on a given list of entites
// e.g. widgets that depend on a list of actions // e.g. widget paths that depend on a list of actions
export const getEntityDependants = ( export const getEntityDependantPaths = (
fullEntityPaths: string[], fullEntityPaths: string[],
allEntitiesDependantsmap: GroupedDependencyMap, allEntitiesDependantsmap: GroupedDependencyMap,
visitedPaths: Set<string>, visitedPaths: Set<string>,
): { names: Set<string>; fullPaths: Set<string> } => { ): Set<string> => {
const dependantEntityNames = new Set<string>(); const dependantEntityNames = new Set<string>();
const dependantEntityFullPaths = new Set<string>(); const dependantEntityFullPaths = new Set<string>();
@ -97,15 +98,13 @@ export const getEntityDependants = (
dependantEntityNames.add(dependantEntityName); dependantEntityNames.add(dependantEntityName);
dependantEntityFullPaths.add(dependantPath); dependantEntityFullPaths.add(dependantPath);
const childDependants = getEntityDependants( const childDependants = getEntityDependantPaths(
[dependantPath], [dependantPath],
allEntitiesDependantsmap, allEntitiesDependantsmap,
visitedPaths, visitedPaths,
); );
childDependants.names.forEach((childDependantName) => {
dependantEntityNames.add(childDependantName); childDependants.forEach((childDependantPath) => {
});
childDependants.fullPaths.forEach((childDependantPath) => {
dependantEntityFullPaths.add(childDependantPath); dependantEntityFullPaths.add(childDependantPath);
}); });
}); });
@ -113,11 +112,11 @@ export const getEntityDependants = (
); );
}); });
return { names: dependantEntityNames, fullPaths: dependantEntityFullPaths }; return dependantEntityFullPaths;
}; };
export const findLoadingEntities = ( export const findLoadingEntities = (
isLoadingActions: string[], loadingActions: string[],
dataTree: DataTree, dataTree: DataTree,
inverseMap: DependencyMap, inverseMap: DependencyMap,
): Set<string> => { ): Set<string> => {
@ -125,15 +124,33 @@ export const findLoadingEntities = (
inverseMap, inverseMap,
dataTree, dataTree,
); );
const loadingEntitiesDetails = getEntityDependants( const loadingEntityPaths = getEntityDependantPaths(
isLoadingActions, loadingActions,
entitiesDependantsMap, entitiesDependantsMap,
new Set<string>(), new Set<string>(),
); );
// check animateLoading is active on current widgets and set // check animateLoading is active on current widgets and set
const filteredLoadingEntityNames = new Set<string>(); const filteredLoadingEntityNames = new Set<string>();
loadingEntitiesDetails.names.forEach((entityName) => {
loadingEntityPaths.forEach((entityPath) => {
const entityPathArray = entityPath.split(".");
const entityName = entityPathArray[0];
const widget = get(dataTree, [entityName]);
if (isWidget(widget)) {
const loadingProperties = WidgetFactory.getLoadingProperties(widget.type);
// check if propertyPath is listed in widgetConfig
if (
entityPathArray.length > 1 &&
loadingProperties &&
!loadingProperties.some((propRegExp) => propRegExp.test(entityPath))
) {
return;
}
}
// check animateLoading is active on current widgets and set
get(dataTree, [entityName, "animateLoading"]) === true && get(dataTree, [entityName, "animateLoading"]) === true &&
filteredLoadingEntityNames.add(entityName); filteredLoadingEntityNames.add(entityName);
}); });

View File

@ -44,6 +44,7 @@ export const registerWidget = (Widget: any, config: WidgetConfiguration) => {
config.properties.contentConfig, config.properties.contentConfig,
config.properties.styleConfig, config.properties.styleConfig,
config.features, config.features,
config.properties.loadingProperties,
); );
configureWidget(config); configureWidget(config);
}; };

View File

@ -77,6 +77,20 @@ abstract class BaseWidget<
return {}; return {};
} }
/**
* getLoadingProperties returns a list of regexp's used to specify bindingPaths,
* which can set the isLoading prop of the widget.
* When:
* 1. the path is bound to an action (API/Query)
* 2. the action is currently in-progress
*
* if undefined, all paths can set the isLoading state
* if empty array, no paths can set the isLoading state
*/
static getLoadingProperties(): Array<RegExp> | undefined {
return;
}
/** /**
* Widget abstraction to register the widget type * Widget abstraction to register the widget type
* ```javascript * ```javascript

View File

@ -229,6 +229,7 @@ export const CONFIG = {
config: Widget.getPropertyPaneConfig(), config: Widget.getPropertyPaneConfig(),
contentConfig: Widget.getPropertyPaneContentConfig(), contentConfig: Widget.getPropertyPaneContentConfig(),
styleConfig: Widget.getPropertyPaneStyleConfig(), styleConfig: Widget.getPropertyPaneStyleConfig(),
loadingProperties: Widget.getLoadingProperties(),
}, },
}; };

View File

@ -140,6 +140,10 @@ class TableWidgetV2 extends BaseWidget<TableWidgetProps, WidgetState> {
}; };
} }
static getLoadingProperties(): Array<RegExp> | undefined {
return [/\.tableData$/];
}
/* /*
* Function to get the table columns with appropriate render functions * Function to get the table columns with appropriate render functions
* based on columnType * based on columnType

View File

@ -26,6 +26,7 @@ export interface WidgetConfiguration {
default: Record<string, string>; default: Record<string, string>;
meta: Record<string, any>; meta: Record<string, any>;
derived: DerivedPropertiesMap; derived: DerivedPropertiesMap;
loadingProperties?: Array<RegExp>;
}; };
} }