Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sheet embed support #1013

Merged
merged 17 commits into from
Dec 12, 2022
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
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
136 changes: 136 additions & 0 deletions apis/nucleus/src/components/Sheet.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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';

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
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;
37 changes: 37 additions & 0 deletions apis/nucleus/src/components/sheetGlue.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Sheet from './Sheet';
import uid from '../object/uid';

export default function glue({ halo, element, model, initialSnOptions, initialSnPlugins, onMount, initialError }) {
const { root } = halo;
const sheetRef = React.createRef();
const currentId = uid();
const portal = ReactDOM.createPortal(
<Sheet
ref={sheetRef}
halo={halo}
model={model}
currentId={currentId}
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);
// Cannot use model.id as it is not unique in a given mashup
// root.addCell(currentId, sheetRef); // this is not needed, sheet is not part of the focus stuff

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