Skip to content

Commit

Permalink
feat: sheet embed support (#1013)
Browse files Browse the repository at this point in the history
* feat: add basic sheet rendering support

* chore: add missing file

* fix: correct bg colors for none support

* chore: fix test that relied on dark bg

* chore: fix ref

* chore: api spec update

* chore: add todo comments

* chore: use memo

* chore: a bit less verbose

* chore: list

* chore: cleaning

* chore: add rendering test

* chore: enable rendering test

* chore: settings

* chore: settings

* chore: disable rendering tests

* chore: revert test tests
  • Loading branch information
Caele committed Dec 12, 2022
1 parent f644c35 commit 3dacac5
Show file tree
Hide file tree
Showing 26 changed files with 689 additions and 36 deletions.
4 changes: 4 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ jobs:
fi
- store_test_results:
path: coverage/junit
# - run:
# name: Test rendering
# command: yarn run test:rendering
- run:
name: Test component
command: yarn run test:component --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch
Expand All @@ -119,6 +122,7 @@ jobs:
- run:
name: Test integration
command: yarn run test:integration --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch

- nebula_create:
project_name: 'generated/hello'
picasso_template: 'none'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.rej
*.tmp
*.log
*.pem
.cache/
.DS_Store
.idea/
Expand Down
1 change: 1 addition & 0 deletions apis/enigma-mocker/src/mocks/get-object-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ function createMock(genericObject, options) {
}),
{}
),
genericType: genericObject.type,
};
return { [qId]: mock };
}
Expand Down
9 changes: 4 additions & 5 deletions apis/nucleus/src/components/Cell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ const Cell = forwardRef(
const { nebbie } = halo.public;
const { disableCellPadding = false } = halo.context || {};

const { translator, language, keyboardNavigation } = useContext(InstanceContext);
const { theme: themeName, translator, language, keyboardNavigation } = useContext(InstanceContext);
const theme = useTheme();
const [cellRef, cellRect, cellNode] = useRect();
const [state, dispatch] = useReducer(contentReducer, initialState(initialError));
Expand All @@ -289,8 +289,7 @@ const Cell = forwardRef(
const [snOptions, setSnOptions] = useState(initialSnOptions);
const [snPlugins, setSnPlugins] = useState(initialSnPlugins);
const cellElementId = `njs-cell-${currentId}`;
const clickOutElements = [`#${cellElementId}`, '.njs-action-toolbar-popover']; // elements which will not trigger the click out listener
const [selections] = useObjectSelections(app, model, clickOutElements);
const [selections] = useObjectSelections(app, model, [`#${cellElementId}`, '.njs-action-toolbar-popover']); // elements which will not trigger the click out listener
const [hovering, setHover] = useState(false);
const hoveringDebouncer = useRef({ enter: null, leave: null });
const [bgColor, setBgColor] = useState(undefined);
Expand All @@ -311,7 +310,7 @@ const Cell = forwardRef(
const bgComp = layout?.components ? layout.components.find((comp) => comp.key === 'general') : null;
setBgColor(resolveBgColor(bgComp, halo.public.theme));
setBgImage(resolveBgImage(bgComp, halo.app));
}, [layout, halo.public.theme, halo.app]);
}, [layout, halo.public.theme, halo.app, themeName]);

focusHandler.current.blurCallback = (resetFocus) => {
halo.root.toggleFocusOfCells();
Expand Down Expand Up @@ -495,7 +494,7 @@ const Cell = forwardRef(
width: '100%',
height: '100%',
overflow: 'hidden',
backgroundColor: bgColor,
backgroundColor: bgColor || 'unset',
backgroundImage: bgImage && bgImage.url ? `url(${bgImage.url})` : undefined,
backgroundRepeat: 'no-repeat',
backgroundSize: bgImage && bgImage.size,
Expand Down
3 changes: 3 additions & 0 deletions apis/nucleus/src/components/NebulaApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export default function boot({ app, context }) {
addCell(id, cell) {
cells[id] = cell;
},
removeCell(id) {
delete cells[id];
},
add(component) {
(async () => {
await rendered;
Expand Down
148 changes: 148 additions & 0 deletions apis/nucleus/src/components/Sheet.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React, { useEffect, useState, useContext, useMemo } from 'react';
import useLayout from '../hooks/useLayout';
import getObject from '../object/get-object';
import Cell from './Cell';
import uid from '../object/uid';
import { resolveBgColor, resolveBgImage } from '../utils/background-props';
import InstanceContext from '../contexts/InstanceContext';

/**
* @interface
* @extends HTMLElement
* @experimental
* @since 3.1.0
*/
const SheetElement = {
/** @type {'njs-sheet'} */
className: 'njs-sheet',
};

function getCellRenderer(cell, halo, initialSnOptions, initialSnPlugins, initialError, onMount) {
const { x, y, width, height } = cell.bounds;
return (
<div style={{ left: `${x}%`, top: `${y}%`, width: `${width}%`, height: `${height}%`, position: 'absolute' }}>
<Cell
ref={cell.cellRef}
halo={halo}
model={cell.model}
currentId={cell.currentId}
initialSnOptions={initialSnOptions}
initialSnPlugins={initialSnPlugins}
initialError={initialError}
onMount={onMount}
/>
</div>
);
}

function Sheet({ model, halo, initialSnOptions, initialSnPlugins, initialError, onMount }) {
const { root } = halo;
const [layout] = useLayout(model);
const { theme: themeName } = useContext(InstanceContext);
const [cells, setCells] = useState([]);
const [bgColor, setBgColor] = useState(undefined);
const [bgImage, setBgImage] = useState(undefined);
const [deepHash, setDeepHash] = useState('');

/// For each object
useEffect(() => {
if (layout) {
const hash = JSON.stringify(layout.cells);
if (hash === deepHash) {
return;
}
setDeepHash(hash);
const fetchObjects = async () => {
/*
Need to always fetch and evaluate everything as the sheet need to support multiple instances of the same object?
No, there is no way to add the same chart twice, so the optimization should be worth it.
*/

// Clear the cell list
cells.forEach((c) => {
root.removeCell(c.currentId);
});

const lCells = layout.cells;
// TODO - should try reuse existing objects on subsequent renders
// Non-id updates should only change the "css"
const cs = await Promise.all(
lCells.map(async (c) => {
let mounted;
const mountedPromise = new Promise((resolve) => {
mounted = resolve;
});

const cell = cells.find((ce) => ce.id === c.name);
if (cell) {
cell.bounds = c.bounds;
delete cell.mountedPromise;
return cell;
}
const vs = await getObject({ id: c.name }, halo);
return {
model: vs.model,
id: c.name,
bounds: c.bounds,
cellRef: React.createRef(),
currentId: uid(),
mounted,
mountedPromise,
};
})
);
cs.forEach((c) => root.addCell(c.currentId, c.cellRef));
setCells(cs);
};
fetchObjects();
}
}, [layout]);

const cellRenderers = useMemo(
() =>
cells
? cells.map((c) => getCellRenderer(c, halo, initialSnOptions, initialSnPlugins, initialError, c.mounted))
: [],
[cells]
);

useEffect(() => {
const bgComp = layout?.components ? layout.components.find((comp) => comp.key === 'general') : null;
setBgColor(resolveBgColor(bgComp, halo.public.theme));
setBgImage(resolveBgImage(bgComp, halo.app));
}, [layout, halo.public.theme, halo.app, themeName]);

/* TODO
- sheet title + bg + logo etc + as option
- sheet exposed classnames for theming
*/

const height = !layout || Number.isNaN(layout.height) ? '100%' : `${Number(layout.height)}%`;
const promises = cells.map((c) => c.mountedPromise);
const ps = promises.filter((p) => !!p);
if (ps.length) {
Promise.all(promises).then(() => {
// TODO - correct? Currently called each time a new cell is mounted?
onMount();
});
}
return (
<div
className={SheetElement.className}
style={{
width: `100%`,
height,
position: 'relative',
backgroundColor: bgColor,
backgroundImage: bgImage && bgImage.url ? `url(${bgImage.url})` : undefined,
backgroundRepeat: 'no-repeat',
backgroundSize: bgImage && bgImage.size,
backgroundPosition: bgImage && bgImage.pos,
}}
>
{cellRenderers}
</div>
);
}

export default Sheet;
31 changes: 31 additions & 0 deletions apis/nucleus/src/components/sheetGlue.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Sheet from './Sheet';

export default function glue({ halo, element, model, initialSnOptions, initialSnPlugins, onMount, initialError }) {
const { root } = halo;
const sheetRef = React.createRef();
const portal = ReactDOM.createPortal(
<Sheet
ref={sheetRef}
halo={halo}
model={model}
initialSnOptions={initialSnOptions}
initialSnPlugins={initialSnPlugins}
initialError={initialError}
onMount={onMount}
/>,
element,
model.id
);

const unmount = () => {
root.remove(portal);
model.removeListener('closed', unmount);
};

model.on('closed', unmount);

root.add(portal);
return [unmount, sheetRef];
}
7 changes: 4 additions & 3 deletions apis/nucleus/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import AppSelectionsPortal from './components/selections/AppSelections';
import ListBoxPortal from './components/listbox/ListBoxPortal';

import create from './object/create-session-object';
import get from './object/get-object';
import get from './object/get-generic-object';
import flagsFn from './flags/flags';
import { create as typesFn } from './sn/types';

Expand Down Expand Up @@ -255,9 +255,10 @@ function nuked(configuration = {}) {
*/
const api = /** @lends Embed# */ {
/**
* Renders a visualization into an HTMLElement.
* Renders a visualization or sheet into an HTMLElement.
* Support for sense sheets is experimental.
* @param {CreateConfig | GetConfig} cfg - The render configuration.
* @returns {Promise<Viz>} A controller to the rendered visualization.
* @returns {Promise<Viz|Sheet>} A controller to the rendered visualization or sheet.
* @example
* // render from existing object
* n.render({
Expand Down
35 changes: 35 additions & 0 deletions apis/nucleus/src/object/get-generic-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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);
if (!rpc) {
rpc = halo.app.getObject(id);
rpcRequestModelStore.set(key, rpc);
}
const model = await rpc;
modelStore.set(key, model);

if (model.genericType === 'sheet') {
return initSheet(model, { options, plugins, element }, halo);
}

return init(model, { options, plugins, element }, halo);
}
16 changes: 1 addition & 15 deletions apis/nucleus/src/object/get-object.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
import init from './initiate';
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);
Expand All @@ -25,5 +10,6 @@ export default async function getObject({ id, options, plugins, element }, halo)
}
const model = await rpc;
modelStore.set(key, model);

return init(model, { options, plugins, element }, halo);
}
23 changes: 23 additions & 0 deletions apis/nucleus/src/object/initiate-sheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* eslint no-underscore-dangle:0 */
import sheetAPI from '../sheet';

export default async function initSheet(model, optional, halo, initialError, onDestroy = async () => {}) {
const api = sheetAPI({
model,
halo,
initialError,
onDestroy,
});

if (optional.options) {
api.__DO_NOT_USE__.options(optional.options);
}
if (optional.plugins) {
api.__DO_NOT_USE__.plugins(optional.plugins);
}
if (optional.element) {
await api.__DO_NOT_USE__.mount(optional.element);
}

return api;
}
Loading

0 comments on commit 3dacac5

Please sign in to comment.