feat: create api (#1079)

* feat: initial create obj api

* chore: split create and generate

* chore: remove duplicate selector

* chore: set props to null at failed creation
This commit is contained in:
Tobias Åström
2023-04-05 20:35:13 +02:00
committed by GitHub
parent ff8d6e4a9f
commit b3315d0e6b
9 changed files with 333 additions and 82 deletions

View File

@@ -11,7 +11,8 @@ import ListBoxPopoverWrapper, {
getOptions as getListboxPopoverOptions,
} from './components/listbox/ListBoxPopoverWrapper';
import create from './object/create-session-object';
import createSessionObject from './object/create-session-object';
import createObject from './object/create-object';
import get from './object/get-generic-object';
import flagsFn from './flags/flags';
import { create as typesFn } from './sn/types';
@@ -251,8 +252,9 @@ function nuked(configuration = {}) {
const api = /** @lends Embed# */ {
/**
* Renders a visualization or sheet into an HTMLElement.
* Visualizations can either be existing objects or created on the fly.
* Support for sense sheets is experimental.
* @param {CreateConfig | GetConfig} cfg - The render configuration.
* @param {RenderConfig} cfg The render configuration.
* @returns {Promise<Viz|Sheet>} A controller to the rendered visualization or sheet.
* @example
* // render from existing object
@@ -273,8 +275,39 @@ function nuked(configuration = {}) {
if (cfg.id) {
return get(cfg, halo);
}
return create(cfg, halo);
return createSessionObject(cfg, halo);
},
/**
* Creates a visualization model
* @param {CreateConfig} cfg The create configuration.
* @experimental
* @returns {Promise<EngineAPI.IGenericObject>} An engima model
* @example
* // create a barchart in the app and return the model
* const model = await n.create({
* type: 'barchart',
* fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],
* properties: { showTitle: true }
* }
* );
*/
create: async (cfg) => createObject(cfg, halo, false),
/**
* Generates properties for a visualization object
* @param {CreateConfig} cfg The create configuration.
* @experimental
* @returns {Promise<object>} The objects properties
* @example
* // generate properties for a barchart
* const properties = await n.create({
* type: 'barchart',
* fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],
* properties: { showTitle: true }
* },
* true
* );
*/
generateProperties: async (cfg) => createObject(cfg, halo, true),
/**
* Updates the current context of this embed instance.
* Use this when you want to change some part of the current context, like theme.

View File

@@ -0,0 +1,94 @@
import * as populatorModule from '../populator';
import create from '../create-object';
describe('create-object', () => {
let halo = {};
let types;
let sn;
let merged;
let populator;
let init;
let objectModel;
beforeEach(() => {
populator = jest.fn();
init = jest.fn();
jest.spyOn(populatorModule, 'default').mockImplementation(populator);
objectModel = { id: 'id', on: () => {}, once: () => {} };
types = {
get: jest.fn(),
};
halo = {
app: {
createObject: jest.fn().mockResolvedValue(objectModel),
},
types,
};
init.mockReturnValue('api');
sn = { qae: { properties: { onChange: jest.fn() } } };
merged = { m: 'true' };
const t = {
initialProperties: jest.fn().mockResolvedValue(merged),
supernova: jest.fn().mockResolvedValue(sn),
};
types.get.mockReturnValue(t);
});
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});
test('should call types.get with name and version', () => {
create({ type: 't', version: 'v', fields: 'f' }, halo);
expect(types.get).toHaveBeenCalledWith({ name: 't', version: 'v' });
});
test('should call initialProperties on returned type', () => {
const t = { initialProperties: jest.fn() };
t.initialProperties.mockReturnValue({ then: () => {} });
types.get.mockReturnValue(t);
create({ type: 't', version: 'v', fields: 'f', properties: 'props', extendProperties: false }, halo);
expect(t.initialProperties).toHaveBeenCalledWith('props', false);
});
test('should populate fields', async () => {
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
expect(populator).toHaveBeenCalledWith({ sn, properties: merged, fields: 'f' }, halo);
});
test('should call properties onChange handler when optional props are provided', async () => {
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
expect(sn.qae.properties.onChange).toHaveBeenCalledWith(merged);
});
test('should not call onChange handler when optional props are not provided', async () => {
await create({ type: 't', version: 'v', fields: 'f' }, halo);
expect(sn.qae.properties.onChange).toHaveBeenCalledTimes(0);
});
test('should create a object with merged props', async () => {
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
expect(halo.app.createObject).toHaveBeenCalledWith(merged);
});
test('should create a dummy object when error is thrown', async () => {
types.get.mockImplementation(() => {
throw new Error('oops');
});
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
expect(halo.app.createObject).toHaveBeenCalledWith({
qInfo: { qType: 't' },
visualization: 't',
});
});
test('should return props only', async () => {
const props = await create({ type: 't', version: 'v', fields: 'f' }, halo, true);
expect(halo.app.createObject).not.toHaveBeenCalledWith();
expect(props).toEqual({ m: 'true' });
});
});

View File

@@ -0,0 +1,63 @@
import populateData from './populator';
import { modelStore } from '../stores/model-store';
/**
* @typedef {string | EngineAPI.INxDimension | EngineAPI.INxMeasure | LibraryField} Field
*/
/**
* @interface CreateConfig
* @description Rendering configuration for creating and rendering a new object
* @property {string} type
* @property {string=} version
* @property {(Field[])=} fields
* @property {EngineAPI.IGenericObjectProperties=} properties
*/
export default async function createObject(
{ type, version, fields, properties, extendProperties /* , options, plugins, element */ },
halo,
generateOnly
) {
let mergedProps = {};
// let error;
try {
const t = halo.types.get({ name: type, version });
mergedProps = await t.initialProperties(properties, extendProperties);
const sn = await t.supernova();
if (fields) {
populateData(
{
sn,
properties: mergedProps,
fields,
},
halo
);
}
if (properties && sn && sn.qae.properties.onChange) {
sn.qae.properties.onChange.call({}, mergedProps);
}
} catch (e) {
// error = e;
// minimal dummy object properties to allow it to be created
// and rendered with the error
if (!generateOnly) {
mergedProps = {
qInfo: {
qType: type,
},
visualization: type,
};
} else {
mergedProps = null;
}
// console.error(e); // eslint-disable-line
}
if (!generateOnly) {
const model = await halo.app.createObject(mergedProps);
modelStore.set(model.id, model);
return model;
}
return mergedProps;
}

View File

@@ -7,14 +7,17 @@ import { subscribe, modelStore } from '../stores/model-store';
*/
/**
* @interface CreateConfig
* @description Rendering configuration for creating and rendering a new object
* @extends BaseConfig
* @property {string} type
* @property {string=} version
* @property {(Field[])=} fields
* @property {boolean} [extendProperties=false] Whether to deeply extend properties or not. If false then subtrees will be overwritten.
* @property {EngineAPI.IGenericObjectProperties=} properties
* @interface RenderConfig
* @description Configuration for rendering a visualisation, either creating or fetching an existing object.
* @property {HTMLElement} element Target html element to render in to
* @property {object=} options Options passed into the visualisation
* @property {Plugin[]} [plugins] plugins passed into the visualisation
* @property {string=} id For existing objects: Engine identifier of object to render
* @property {string=} type For creating objects: Type of visualisation to render
* @property {string=} version For creating objects: Version of visualization to render
* @property {(Field[])=} fields For creating objects: Data fields to use
* @property {boolean=} [extendProperties=false] For creating objects: Whether to deeply extend properties or not. If false then subtrees will be overwritten.
* @property {EngineAPI.IGenericObjectProperties=} properties For creating objects: Explicit properties to set
* @example
* // A config for Creating objects:
* const createConfig = {
@@ -29,6 +32,12 @@ import { subscribe, modelStore } from '../stores/model-store';
* }
* };
* nebbie.render(createConfig);
* // A config for rendering an existing object:
* const createConfig = {
* id: 'jG5LP',
* element: document.querySelector('.line'),
* };
* nebbie.render(createConfig);
*/
export default async function createSessionObject(
{ type, version, fields, properties, options, plugins, element, extendProperties },

View File

@@ -2,21 +2,6 @@ import init from './initiate';
import initSheet from './initiate-sheet';
import { modelStore, rpcRequestModelStore } from '../stores/model-store';
/**
* @interface BaseConfig
* @description Basic rendering configuration for rendering an object
* @property {HTMLElement} element
* @property {object=} options
* @property {Plugin[]} [plugins]
*/
/**
* @interface GetConfig
* @description Rendering configuration for rendering an existing object
* @extends BaseConfig
* @property {string} id
*/
export default async function getObject({ id, options, plugins, element }, halo) {
const key = `${id}`;
let rpc = rpcRequestModelStore.get(key);

View File

@@ -71,7 +71,7 @@ export default function viz({ model, halo, initialError, onDestroy = async () =>
id: model.id,
/**
* This visualizations Enigma model, a representation of the generic object.
* @type {string}
* @type {EngineAPI.IGenericObject}
*/
model,
/**

View File

@@ -692,21 +692,13 @@
},
"entries": {
"render": {
"description": "Renders a visualization or sheet into an HTMLElement.\nSupport for sense sheets is experimental.",
"description": "Renders a visualization or sheet into an HTMLElement.\nVisualizations can either be existing objects or created on the fly.\nSupport for sense sheets is experimental.",
"kind": "function",
"params": [
{
"name": "cfg",
"description": "The render configuration.",
"kind": "union",
"items": [
{
"type": "#/definitions/CreateConfig"
},
{
"type": "#/definitions/GetConfig"
}
]
"type": "#/definitions/RenderConfig"
}
],
"returns": {
@@ -731,6 +723,54 @@
"// render on the fly\nn.render({\n element: el,\n type: 'barchart',\n fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }]\n});"
]
},
"create": {
"description": "Creates a visualization model",
"stability": "experimental",
"kind": "function",
"params": [
{
"name": "cfg",
"description": "The create configuration.",
"type": "#/definitions/CreateConfig"
}
],
"returns": {
"description": "An engima model",
"type": "Promise",
"generics": [
{
"type": "EngineAPI.IGenericObject"
}
]
},
"examples": [
"// create a barchart in the app and return the model\nconst model = await n.create({\n type: 'barchart',\n fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],\n properties: { showTitle: true }\n }\n);"
]
},
"generateProperties": {
"description": "Generates properties for a visualization object",
"stability": "experimental",
"kind": "function",
"params": [
{
"name": "cfg",
"description": "The create configuration.",
"type": "#/definitions/CreateConfig"
}
],
"returns": {
"description": "The objects properties",
"type": "Promise",
"generics": [
{
"type": "object"
}
]
},
"examples": [
"// generate properties for a barchart\nconst properties = await n.create({\n type: 'barchart',\n fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],\n properties: { showTitle: true }\n },\n true\n);"
]
},
"context": {
"description": "Updates the current context of this embed instance.\nUse this when you want to change some part of the current context, like theme.",
"kind": "function",
@@ -1152,7 +1192,7 @@
},
"model": {
"description": "This visualizations Enigma model, a representation of the generic object.",
"type": "string"
"type": "EngineAPI.IGenericObject"
},
"destroy": {
"description": "Destroys the visualization and removes it from the the DOM.",
@@ -1474,11 +1514,6 @@
},
"CreateConfig": {
"description": "Rendering configuration for creating and rendering a new object",
"extends": [
{
"type": "#/definitions/BaseConfig"
}
],
"kind": "interface",
"entries": {
"type": {
@@ -1500,54 +1535,76 @@
}
]
},
"extendProperties": {
"description": "Whether to deeply extend properties or not. If false then subtrees will be overwritten.",
"optional": true,
"defaultValue": false,
"type": "boolean"
},
"properties": {
"optional": true,
"type": "EngineAPI.IGenericObjectProperties"
}
},
"examples": [
"// A config for Creating objects:\nconst createConfig = {\n type: 'bar',\n element: document.querySelector('.bar'),\n extendProperties: true,\n fields: ['[Country names]', '=Sum(Sales)'],\n properties: {\n legend: {\n show: false,\n },\n }\n};\nnebbie.render(createConfig);"
]
}
},
"BaseConfig": {
"description": "Basic rendering configuration for rendering an object",
"RenderConfig": {
"description": "Configuration for rendering a visualisation, either creating or fetching an existing object.",
"kind": "interface",
"entries": {
"element": {
"description": "Target html element to render in to",
"type": "HTMLElement"
},
"options": {
"description": "Options passed into the visualisation",
"optional": true,
"type": "object"
},
"plugins": {
"description": "plugins passed into the visualisation",
"optional": true,
"kind": "array",
"items": {
"type": "#/definitions/Plugin"
}
}
}
},
"GetConfig": {
"description": "Rendering configuration for rendering an existing object",
"extends": [
{
"type": "#/definitions/BaseConfig"
}
],
"kind": "interface",
"entries": {
},
"id": {
"description": "For existing objects: Engine identifier of object to render",
"optional": true,
"type": "string"
},
"type": {
"description": "For creating objects: Type of visualisation to render",
"optional": true,
"type": "string"
},
"version": {
"description": "For creating objects: Version of visualization to render",
"optional": true,
"type": "string"
},
"fields": {
"description": "For creating objects: Data fields to use",
"optional": true,
"kind": "union",
"items": [
{
"kind": "array",
"items": {
"type": "#/definitions/Field"
}
}
]
},
"extendProperties": {
"description": "For creating objects: Whether to deeply extend properties or not. If false then subtrees will be overwritten.",
"optional": true,
"defaultValue": false,
"type": "boolean"
},
"properties": {
"description": "For creating objects: Explicit properties to set",
"optional": true,
"type": "EngineAPI.IGenericObjectProperties"
}
}
},
"examples": [
"// A config for Creating objects:\nconst createConfig = {\n type: 'bar',\n element: document.querySelector('.bar'),\n extendProperties: true,\n fields: ['[Country names]', '=Sum(Sales)'],\n properties: {\n legend: {\n show: false,\n },\n }\n};\nnebbie.render(createConfig);\n// A config for rendering an existing object:\nconst createConfig = {\n id: 'jG5LP',\n element: document.querySelector('.line'),\n};\nnebbie.render(createConfig);"
]
},
"LibraryField": {
"kind": "interface",

View File

@@ -235,10 +235,23 @@ declare namespace stardust {
/**
* Renders a visualization or sheet into an HTMLElement.
* Visualizations can either be existing objects or created on the fly.
* Support for sense sheets is experimental.
* @param cfg The render configuration.
*/
render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise<stardust.Viz | stardust.Sheet>;
render(cfg: stardust.RenderConfig): Promise<stardust.Viz | stardust.Sheet>;
/**
* Creates a visualization model
* @param cfg The create configuration.
*/
create(cfg: stardust.CreateConfig): Promise<EngineAPI.IGenericObject>;
/**
* Generates properties for a visualization object
* @param cfg The create configuration.
*/
generateProperties(cfg: stardust.CreateConfig): Promise<object>;
/**
* Updates the current context of this embed instance.
@@ -357,7 +370,7 @@ declare namespace stardust {
id: string;
model: string;
model: EngineAPI.IGenericObject;
/**
* Destroys the visualization and removes it from the the DOM.
@@ -460,28 +473,26 @@ declare namespace stardust {
/**
* Rendering configuration for creating and rendering a new object
*/
interface CreateConfig extends stardust.BaseConfig{
interface CreateConfig {
type: string;
version?: string;
fields?: stardust.Field[];
extendProperties?: boolean;
properties?: EngineAPI.IGenericObjectProperties;
}
/**
* Basic rendering configuration for rendering an object
* Configuration for rendering a visualisation, either creating or fetching an existing object.
*/
interface BaseConfig {
interface RenderConfig {
element: HTMLElement;
options?: object;
plugins?: stardust.Plugin[];
}
/**
* Rendering configuration for rendering an existing object
*/
interface GetConfig extends stardust.BaseConfig{
id: string;
id?: string;
type?: string;
version?: string;
fields?: stardust.Field[];
extendProperties?: boolean;
properties?: EngineAPI.IGenericObjectProperties;
}
interface LibraryField {

View File

@@ -1010,7 +1010,6 @@ export function onTakeSnapshot(cb) {
* @ignore
* @example
* import { onContextMenu } from '@nebula.js/stardust';
* onContextMenu((menu, event) => {
* menu.addItem(item, index);
* });