mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
feat: long running queries (#194)
* feat: long running queries * fix: snapshotting and exporting * chore: treat console as error We default to error for console now Added overrides for current use cases This ensure not getting console logs in without overriding
This commit is contained in:
committed by
GitHub
parent
397cbc4294
commit
7f45fbc6c5
@@ -21,6 +21,12 @@
|
|||||||
"import/no-dynamic-require": 0
|
"import/no-dynamic-require": 0
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["apis/**/*"],
|
||||||
|
"rules": {
|
||||||
|
"no-console": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"files": ["**/*.{int,spec}.{js,jsx}"],
|
"files": ["**/*.{int,spec}.{js,jsx}"],
|
||||||
"env": {
|
"env": {
|
||||||
|
|||||||
@@ -10,24 +10,18 @@ describe('viz', () => {
|
|||||||
],
|
],
|
||||||
['../viz.js']
|
['../viz.js']
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('api', () => {
|
describe('api', () => {
|
||||||
let api;
|
let api;
|
||||||
before(() => {
|
before(() => {
|
||||||
const [{ default: create }] = doMock();
|
const [{ default: create }] = doMock();
|
||||||
|
|
||||||
const { api: foo } = create({
|
const [foo] = create({
|
||||||
model: 'a',
|
model: 'a',
|
||||||
config: {},
|
|
||||||
context: {},
|
context: {},
|
||||||
});
|
});
|
||||||
api = foo;
|
api = foo;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a reference to the model', () => {
|
|
||||||
expect(api.model).to.equal('a');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have a mount method', () => {
|
it('should have a mount method', () => {
|
||||||
expect(api.mount).to.be.a('function');
|
expect(api.mount).to.be.a('function');
|
||||||
});
|
});
|
||||||
@@ -100,8 +94,10 @@ describe('viz', () => {
|
|||||||
const [{ default: create }] = doMock({ getter, getPatches });
|
const [{ default: create }] = doMock({ getter, getPatches });
|
||||||
const model = {
|
const model = {
|
||||||
applyPatches: sinon.spy(),
|
applyPatches: sinon.spy(),
|
||||||
|
on: sinon.spy(),
|
||||||
|
once: sinon.spy(),
|
||||||
};
|
};
|
||||||
const { api } = create({
|
const [api] = create({
|
||||||
model,
|
model,
|
||||||
context: {},
|
context: {},
|
||||||
});
|
});
|
||||||
@@ -117,8 +113,10 @@ describe('viz', () => {
|
|||||||
const [{ default: create }] = doMock({ getter, getPatches });
|
const [{ default: create }] = doMock({ getter, getPatches });
|
||||||
const model = {
|
const model = {
|
||||||
applyPatches: sinon.spy(),
|
applyPatches: sinon.spy(),
|
||||||
|
on: sinon.spy(),
|
||||||
|
once: sinon.spy(),
|
||||||
};
|
};
|
||||||
const { api } = create({
|
const [api] = create({
|
||||||
model,
|
model,
|
||||||
context: {},
|
context: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,75 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import React, { useEffect, useState, useContext } from 'react';
|
import React, { forwardRef, useImperativeHandle, useEffect, useState, useContext, useReducer } from 'react';
|
||||||
|
|
||||||
import { Grid, Paper } from '@material-ui/core';
|
import { Grid, Paper } from '@material-ui/core';
|
||||||
import { useTheme } from '@nebula.js/ui/theme';
|
import { useTheme } from '@nebula.js/ui/theme';
|
||||||
|
|
||||||
import CError from './Error';
|
import CError from './Error';
|
||||||
|
import LongRunningQuery from './LongRunningQuery';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import Supernova from './Supernova';
|
import Supernova from './Supernova';
|
||||||
|
|
||||||
import useRect from '../hooks/useRect';
|
import useRect from '../hooks/useRect';
|
||||||
import useLayout from '../hooks/useLayout';
|
import useLayout from '../hooks/useLayout';
|
||||||
import useSupernova from '../hooks/useSupernova';
|
|
||||||
import useSelectionsModal from '../hooks/useSelectionsModal';
|
|
||||||
import useLayoutError from '../hooks/useLayoutError';
|
|
||||||
import LocaleContext from '../contexts/LocaleContext';
|
import LocaleContext from '../contexts/LocaleContext';
|
||||||
|
import { createObjectSelectionAPI } from '../selections';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
loading: false,
|
||||||
|
loaded: false,
|
||||||
|
longRunningQuery: false,
|
||||||
|
error: null,
|
||||||
|
sn: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentReducer = (state, action) => {
|
||||||
|
// console.log(action.type);
|
||||||
|
switch (action.type) {
|
||||||
|
case 'LOADING': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'LOADED': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
longRunningQuery: false,
|
||||||
|
error: null,
|
||||||
|
sn: action.sn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'RENDER': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
longRunningQuery: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'LONG_RUNNING_QUERY': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
longRunningQuery: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'ERROR': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
longRunningQuery: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unhandled type: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Loading = ({ delay = 750 }) => {
|
const Loading = ({ delay = 750 }) => {
|
||||||
const [showLoading, setShowLoading] = useState(false);
|
const [showLoading, setShowLoading] = useState(false);
|
||||||
@@ -28,41 +83,194 @@ const Loading = ({ delay = 750 }) => {
|
|||||||
return showLoading && <div>loading...</div>;
|
return showLoading && <div>loading...</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Cell({ nebulaContext, model, snContext, snOptions, onMount }) {
|
const handleModal = ({ sn, layout, model }) => {
|
||||||
const translator = useContext(LocaleContext);
|
const selections = sn && sn.component && sn.component.selections;
|
||||||
const [err, setErr] = useState(null);
|
if (!selections || !selections.id || !model.id) {
|
||||||
|
return;
|
||||||
const [layout] = useLayout(model);
|
}
|
||||||
const [sn, snErr] = useSupernova({
|
if (selections.id === model.id) {
|
||||||
model,
|
selections.setLayout(layout);
|
||||||
nebulaContext,
|
if (layout && layout.qSelectionInfo && layout.qSelectionInfo.qInSelections && !selections.isModal()) {
|
||||||
genericObjectType: layout && layout.visualization,
|
sn.selections.goModal('/qHyperCubeDef'); // TODO - use path from data targets
|
||||||
genericObjectVersion: layout && layout.version,
|
|
||||||
});
|
|
||||||
const [layoutError, requirementsError] = useLayoutError({ sn, layout });
|
|
||||||
const [contentRef /* contentRect */, , contentNode] = useRect();
|
|
||||||
const theme = useTheme();
|
|
||||||
useSelectionsModal({ sn, model, layout });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onMount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (snErr) {
|
|
||||||
setErr(snErr);
|
|
||||||
} else if (layoutError.length) {
|
|
||||||
setErr({ message: '', data: layoutError });
|
|
||||||
} else if (requirementsError.length) {
|
|
||||||
setErr({ message: translator.get('Supernova.Incomplete'), data: [] });
|
|
||||||
} else {
|
|
||||||
setErr(null);
|
|
||||||
}
|
}
|
||||||
}, [snErr, layoutError, requirementsError]);
|
if (!layout.qSelectionInfo || !layout.qSelectionInfo.qInSelections) {
|
||||||
|
if (selections.isModal()) {
|
||||||
|
selections.noModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterData = d => (d.qError ? d.qError.qErrorCode === 7005 : true);
|
||||||
|
|
||||||
|
const validateTargets = (translator, layout, { targets }) => {
|
||||||
|
const layoutErrors = [];
|
||||||
|
const requirementsError = [];
|
||||||
|
targets.forEach(def => {
|
||||||
|
const minD = def.dimensions.min();
|
||||||
|
const minM = def.measures.min();
|
||||||
|
const hc = def.resolveLayout(layout);
|
||||||
|
const d = (hc.qDimensionInfo || []).filter(filterData);
|
||||||
|
const m = (hc.qMeasureInfo || []).filter(filterData);
|
||||||
|
const path = def.layoutPath;
|
||||||
|
if (hc.qError) {
|
||||||
|
layoutErrors.push({ path, error: hc.qError });
|
||||||
|
}
|
||||||
|
if (d.length < minD || m.length < minM) {
|
||||||
|
requirementsError.push(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const showError = !!(layoutErrors.length || requirementsError.length);
|
||||||
|
const title = requirementsError.length ? translator.get('Supernova.Incomplete') : 'Error';
|
||||||
|
const data = requirementsError.length ? requirementsError : layoutErrors;
|
||||||
|
return [showError, { title, data }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getType = async ({ types, name, version }) => {
|
||||||
|
const SN = await types
|
||||||
|
.get({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
})
|
||||||
|
.supernova();
|
||||||
|
return SN;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadType = async ({ dispatch, types, name, version, layout, model, app }) => {
|
||||||
|
try {
|
||||||
|
const snType = await getType({ types, name, version });
|
||||||
|
// Layout might have changed since we requested the new type -> quick return
|
||||||
|
if (layout.visualization !== name) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const selections = createObjectSelectionAPI(model, app);
|
||||||
|
const sn = snType.create({
|
||||||
|
model,
|
||||||
|
app,
|
||||||
|
selections,
|
||||||
|
});
|
||||||
|
return sn;
|
||||||
|
} catch (err) {
|
||||||
|
dispatch({ type: 'ERROR', error: { title: err.message } });
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Cell = forwardRef(({ nebulaContext, model, initialSnContext, initialSnOptions, onMount }, ref) => {
|
||||||
|
const {
|
||||||
|
app,
|
||||||
|
nebbie: { types },
|
||||||
|
} = nebulaContext;
|
||||||
|
|
||||||
|
const translator = useContext(LocaleContext);
|
||||||
|
const theme = useTheme();
|
||||||
|
const [state, dispatch] = useReducer(contentReducer, initialState);
|
||||||
|
const [layout, validating, cancel, retry] = useLayout({ app, model });
|
||||||
|
const [contentRef, contentRect, , contentNode] = useRect();
|
||||||
|
const [snContext, setSnContext] = useState(initialSnContext);
|
||||||
|
const [snOptions, setSnOptions] = useState(initialSnOptions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const validate = sn => {
|
||||||
|
const [showError, error] = validateTargets(translator, layout, sn.generator.qae.data);
|
||||||
|
if (showError) {
|
||||||
|
dispatch({ type: 'ERROR', error });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'RENDER' });
|
||||||
|
}
|
||||||
|
handleModal({ sn: state.sn, layout, model });
|
||||||
|
};
|
||||||
|
const load = async (withLayout, version) => {
|
||||||
|
const sn = await loadType({ dispatch, types, name: withLayout.visualization, version, layout, model, app });
|
||||||
|
if (sn) {
|
||||||
|
dispatch({ type: 'LOADED', sn });
|
||||||
|
onMount();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!layout) {
|
||||||
|
dispatch({ type: 'LOADING' });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (state.sn) {
|
||||||
|
validate(state.sn);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load supernova h
|
||||||
|
const withVersion = types.getSupportedVersion(layout.visualization, layout.version);
|
||||||
|
if (!withVersion) {
|
||||||
|
dispatch({
|
||||||
|
type: 'ERROR',
|
||||||
|
error: {
|
||||||
|
title: `Could not find a version of '${layout.visualization}' that supports current object version. Did you forget to register ${layout.visualization}?`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
load(layout, withVersion);
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}, [types, state.sn, model, layout]);
|
||||||
|
|
||||||
|
// Long running query
|
||||||
|
useEffect(() => {
|
||||||
|
if (!validating) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const handle = setTimeout(() => dispatch({ type: 'LONG_RUNNING_QUERY' }), 2000);
|
||||||
|
return () => clearTimeout(handle);
|
||||||
|
}, [validating]);
|
||||||
|
|
||||||
|
// Expose cell ref api
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
setSnContext,
|
||||||
|
setSnOptions,
|
||||||
|
takeSnapshot: async () => {
|
||||||
|
const snapshot = {
|
||||||
|
...layout,
|
||||||
|
snapshotData: {
|
||||||
|
object: {
|
||||||
|
size: {
|
||||||
|
w: contentRect.width,
|
||||||
|
h: contentRect.height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (typeof state.sn.component.setSnapshotData === 'function') {
|
||||||
|
return (await state.sn.component.setSnapshotData(snapshot)) || snapshot;
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[state.sn, contentRect, layout]
|
||||||
|
);
|
||||||
|
|
||||||
|
let Content = null;
|
||||||
|
if (state.loading) {
|
||||||
|
Content = <Loading />;
|
||||||
|
} else if (state.error) {
|
||||||
|
Content = <CError {...state.error} />;
|
||||||
|
} else if (state.loaded) {
|
||||||
|
Content = (
|
||||||
|
<Supernova
|
||||||
|
key={layout.visualization}
|
||||||
|
sn={state.sn}
|
||||||
|
snContext={snContext}
|
||||||
|
snOptions={snOptions}
|
||||||
|
layout={layout}
|
||||||
|
parentNode={contentNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
style={{ position: 'relative', width: '100%', height: '100%' }}
|
style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}
|
||||||
elevation={0}
|
elevation={0}
|
||||||
square
|
square
|
||||||
className="nebulajs-cell"
|
className="nebulajs-cell"
|
||||||
@@ -71,9 +279,15 @@ export default function Cell({ nebulaContext, model, snContext, snOptions, onMou
|
|||||||
container
|
container
|
||||||
direction="column"
|
direction="column"
|
||||||
spacing={0}
|
spacing={0}
|
||||||
style={{ position: 'relative', width: '100%', height: '100%', padding: theme.spacing(1) }}
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
...(state.longRunningQuery ? { opacity: '0.3' } : {}),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Header layout={layout} sn={sn}>
|
<Header layout={layout} sn={state.sn}>
|
||||||
|
|
||||||
</Header>
|
</Header>
|
||||||
<Grid
|
<Grid
|
||||||
@@ -84,21 +298,12 @@ export default function Cell({ nebulaContext, model, snContext, snOptions, onMou
|
|||||||
}}
|
}}
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
>
|
>
|
||||||
{sn === null && err === null && <Loading />}
|
{Content}
|
||||||
{err && <CError {...err} />}
|
|
||||||
{sn && !err && (
|
|
||||||
<Supernova
|
|
||||||
key={layout.visualization}
|
|
||||||
sn={sn}
|
|
||||||
snContext={snContext}
|
|
||||||
snOptions={snOptions}
|
|
||||||
layout={layout}
|
|
||||||
parentNode={contentNode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Footer layout={layout} />
|
<Footer layout={layout} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{state.longRunningQuery && <LongRunningQuery onCancel={cancel} onRetry={retry} />}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
export default Cell;
|
||||||
|
|||||||
@@ -32,23 +32,23 @@ export default function Error({ title = 'Error', message = '', data = [] }) {
|
|||||||
spacing={1}
|
spacing={1}
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<WarningTriangle style={{ fontSize: '48px' }} />
|
<WarningTriangle style={{ fontSize: '38px' }} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h4" align="center">
|
<Typography variant="h6" align="center">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" align="center">
|
<Typography variant="subtitle1" align="center">
|
||||||
{message}
|
{message}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
{data.map((d, ix) => (
|
{data.map((d, ix) => (
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<Typography key={ix} variant="h6" align="center">
|
<Typography key={ix} variant="subtitle2" align="center">
|
||||||
{d.path} - {d.error.qErrorCode}
|
{d.path} {d.error && '-'} {d.error && d.error.qErrorCode}
|
||||||
</Typography>
|
</Typography>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
95
apis/nucleus/src/components/LongRunningQuery.jsx
Normal file
95
apis/nucleus/src/components/LongRunningQuery.jsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { makeStyles, Grid, Typography, Button } from '@material-ui/core';
|
||||||
|
import WarningTriangle from '@nebula.js/ui/icons/warning-triangle-2';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
stripes: {
|
||||||
|
'&::before': {
|
||||||
|
position: 'absolute',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
content: '""',
|
||||||
|
backgroundSize: '14.14px 14.14px',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(135deg, currentColor 10%, rgba(0,0,0,0) 10%, rgba(0,0,0,0) 50%, currentColor 50%, currentColor 59%, rgba(0,0,0,0) 60%, rgba(0,0,0,0) 103%)',
|
||||||
|
opacity: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Cancel = ({ cancel, ...props }) => (
|
||||||
|
<>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h4" align="center">
|
||||||
|
Long running query...
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item {...props}>
|
||||||
|
<Button variant="outlined" onClick={cancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Retry = ({ retry, ...props }) => (
|
||||||
|
<>
|
||||||
|
<Grid item>
|
||||||
|
<WarningTriangle style={{ fontSize: '38px' }} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h6" align="center">
|
||||||
|
Data update was cancelled
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="subtitle1" align="center">
|
||||||
|
Visualization not updated. Please try again.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button variant="contained" onClick={retry} {...props}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function LongRunningQuery({ onCancel, onRetry }) {
|
||||||
|
const { stripes, cancel, retry } = useStyles();
|
||||||
|
const [canCancel, setCanCancel] = useState(!!onCancel);
|
||||||
|
const [canRetry, setCanRetry] = useState(!!onRetry);
|
||||||
|
const handleCancel = () => {
|
||||||
|
setCanCancel(false);
|
||||||
|
setCanRetry(true);
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
const handleRetry = () => {
|
||||||
|
setCanRetry(false);
|
||||||
|
setCanCancel(true);
|
||||||
|
onRetry();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justify="center"
|
||||||
|
className={stripes}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
|
spacing={1}
|
||||||
|
>
|
||||||
|
{canCancel && <Cancel cancel={handleCancel} className={cancel} />}
|
||||||
|
{canRetry && <Retry retry={handleRetry} className={retry} />}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ const Supernova = ({ sn, snOptions: options, snContext, layout }) => {
|
|||||||
|
|
||||||
// Mount / Unmount / ThemeChanged
|
// Mount / Unmount / ThemeChanged
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!snNode || !parentNode) return undefined;
|
if (!snNode || !parentNode || !snContext) return undefined;
|
||||||
setLogicalSize(constrainElement({ snNode, parentNode, sn, snContext, layout }));
|
setLogicalSize(constrainElement({ snNode, parentNode, sn, snContext, layout }));
|
||||||
component.created({ options, snContext });
|
component.created({ options, snContext });
|
||||||
component.mounted(snNode);
|
component.mounted(snNode);
|
||||||
@@ -87,7 +87,7 @@ const Supernova = ({ sn, snOptions: options, snContext, layout }) => {
|
|||||||
component.willUnmount();
|
component.willUnmount();
|
||||||
snContext.theme.removeListener('changed', render);
|
snContext.theme.removeListener('changed', render);
|
||||||
};
|
};
|
||||||
}, [snNode, parentNode]);
|
}, [snNode, parentNode, snContext]);
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Cell from './Cell';
|
import Cell from './Cell';
|
||||||
|
|
||||||
export default function glue({ nebulaContext, element, model, snContext, snOptions, onMount }) {
|
export default function glue({ nebulaContext, element, model, initialSnContext, initialSnOptions, onMount }) {
|
||||||
const { root } = nebulaContext;
|
const { root } = nebulaContext;
|
||||||
|
const cellRef = React.createRef();
|
||||||
const portal = ReactDOM.createPortal(
|
const portal = ReactDOM.createPortal(
|
||||||
<Cell nebulaContext={nebulaContext} model={model} snContext={snContext} snOptions={snOptions} onMount={onMount} />,
|
<Cell
|
||||||
|
ref={cellRef}
|
||||||
|
nebulaContext={nebulaContext}
|
||||||
|
model={model}
|
||||||
|
initialSnContext={initialSnContext}
|
||||||
|
initialSnOptions={initialSnOptions}
|
||||||
|
onMount={onMount}
|
||||||
|
/>,
|
||||||
element,
|
element,
|
||||||
model.id
|
model.id
|
||||||
);
|
);
|
||||||
@@ -18,7 +26,10 @@ export default function glue({ nebulaContext, element, model, snContext, snOptio
|
|||||||
|
|
||||||
root.add(portal);
|
root.add(portal);
|
||||||
|
|
||||||
return () => {
|
return [
|
||||||
unmount();
|
() => {
|
||||||
};
|
unmount();
|
||||||
|
},
|
||||||
|
cellRef,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import useLayout from '../../hooks/useLayout';
|
|||||||
import Row from './ListBoxRow';
|
import Row from './ListBoxRow';
|
||||||
|
|
||||||
export default function ListBox({ model, selections, direction }) {
|
export default function ListBox({ model, selections, direction }) {
|
||||||
const [layout] = useLayout(model);
|
const [layout] = useLayout({ model });
|
||||||
const [pages, setPages] = useState(null);
|
const [pages, setPages] = useState(null);
|
||||||
const loaderRef = useRef(null);
|
const loaderRef = useRef(null);
|
||||||
const local = useRef({
|
const local = useRef({
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function ListBoxPopover({ alignTo, show, close, app, fieldName, s
|
|||||||
model.unlock('/qListObjectDef');
|
model.unlock('/qListObjectDef');
|
||||||
}, [model]);
|
}, [model]);
|
||||||
|
|
||||||
const [layout] = useLayout(model);
|
const [layout] = useLayout({ model });
|
||||||
|
|
||||||
const translator = useContext(LocaleContext);
|
const translator = useContext(LocaleContext);
|
||||||
const direction = useContext(DirectionContext);
|
const direction = useContext(DirectionContext);
|
||||||
|
|||||||
@@ -1,20 +1,96 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useReducer, useEffect } from 'react';
|
||||||
|
|
||||||
import { observe, unObserve } from '../object/observer';
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const sleep = delay => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, delay);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function useLayout(model) {
|
const layoutReducer = (state, action) => {
|
||||||
const [layout, setLayout] = useState(null);
|
// console.log(action.type);
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INVALID': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
validating: true,
|
||||||
|
cancel: action.cancel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'VALID': {
|
||||||
|
return {
|
||||||
|
validating: false,
|
||||||
|
layout: action.layout,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'CANCELLED': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
validating: false,
|
||||||
|
cancelled: true,
|
||||||
|
retry: action.retry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unhandled type: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getLayout = ({ dispatch, app, model }) => {
|
||||||
|
let canCancel = true;
|
||||||
|
const rpc = model.getLayout();
|
||||||
|
canCancel = false;
|
||||||
|
dispatch({
|
||||||
|
type: 'INVALID',
|
||||||
|
cancel: async () => {
|
||||||
|
if (canCancel) {
|
||||||
|
const global = app.session.getObjectApi({ handle: -1 });
|
||||||
|
await global.cancelRequest(rpc.requestId);
|
||||||
|
dispatch({
|
||||||
|
type: 'CANCELLED',
|
||||||
|
retry: () => getLayout({ dispatch, app, model })(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
canCancel = true;
|
||||||
|
try {
|
||||||
|
const layout = await rpc;
|
||||||
|
// await sleep(15000);
|
||||||
|
canCancel = false;
|
||||||
|
dispatch({ type: 'VALID', layout });
|
||||||
|
} catch (err) {
|
||||||
|
// TODO - this can happen for requested aborted
|
||||||
|
// console.info(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
validating: false,
|
||||||
|
cancelled: false,
|
||||||
|
cancel: null,
|
||||||
|
retry: null,
|
||||||
|
layout: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useLayout({ app, model }) {
|
||||||
|
const [state, dispatch] = useReducer(layoutReducer, initialState);
|
||||||
|
const onChanged = () => {
|
||||||
|
getLayout({ dispatch, app, model })();
|
||||||
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
observe(model, setLayout);
|
model.on('changed', onChanged);
|
||||||
|
onChanged({ dispatch, app, model });
|
||||||
return () => {
|
return () => {
|
||||||
unObserve(model, setLayout);
|
model.removeListener('changed', onChanged);
|
||||||
};
|
};
|
||||||
}, [model && model.id]);
|
}, [model && model.id]);
|
||||||
|
|
||||||
return [layout];
|
return [state.layout, state.validating, state.cancel, state.retry];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
const filterData = d => (d.qError ? d.qError.qErrorCode === 7005 : true);
|
|
||||||
|
|
||||||
export default function useLayoutError({ sn, layout }, deps = []) {
|
|
||||||
const [layoutError, setLayoutError] = useState([]);
|
|
||||||
const [requirementError, setRequirementError] = useState([]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sn || !sn.generator || !sn.generator.qae || !layout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { targets } = sn.generator.qae.data;
|
|
||||||
const layoutErrors = [];
|
|
||||||
const requirementsError = [];
|
|
||||||
targets.forEach(def => {
|
|
||||||
const minD = def.dimensions.min();
|
|
||||||
const minM = def.measures.min();
|
|
||||||
const hc = def.resolveLayout(layout);
|
|
||||||
const d = (hc.qDimensionInfo || []).filter(filterData);
|
|
||||||
const m = (hc.qMeasureInfo || []).filter(filterData);
|
|
||||||
const path = def.layoutPath;
|
|
||||||
if (hc.qError) {
|
|
||||||
layoutErrors.push({ path, error: hc.qError });
|
|
||||||
}
|
|
||||||
if (d.length < minD || m.length < minM) {
|
|
||||||
requirementsError.push(path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setLayoutError(layoutErrors);
|
|
||||||
setRequirementError(requirementsError);
|
|
||||||
}, [sn, layout, ...deps]);
|
|
||||||
|
|
||||||
return [layoutError, requirementError];
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export default function useSelectionModal({ sn, layout, model }, deps = []) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (sn && sn.component.selections && sn.component.selections.id === model.id) {
|
|
||||||
sn.component.selections.setLayout(layout);
|
|
||||||
if (layout.qSelectionInfo && layout.qSelectionInfo.qInSelections && !sn.component.selections.isModal()) {
|
|
||||||
sn.selections.goModal('/qHyperCubeDef'); // TODO - use path from data targets
|
|
||||||
}
|
|
||||||
if (!layout.qSelectionInfo || !layout.qSelectionInfo.qInSelections) {
|
|
||||||
if (sn.component.selections.isModal()) {
|
|
||||||
sn.component.selections.noModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [sn && sn.component.selections, layout, model, ...deps]);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import useSupernovaType from './userSupernovaType';
|
|
||||||
import { createObjectSelectionAPI } from '../selections';
|
|
||||||
|
|
||||||
export default function useSupernova({ model, nebulaContext, genericObjectType, genericObjectVersion }, deps = []) {
|
|
||||||
const [supernova, setSupernova] = useState(null);
|
|
||||||
const [snErr, setSnErr] = useState(null);
|
|
||||||
const [snType, snTypeErr] = useSupernovaType({ nebulaContext, genericObjectType, genericObjectVersion });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!snType || snTypeErr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { app } = nebulaContext;
|
|
||||||
const selections = createObjectSelectionAPI(model, app);
|
|
||||||
const sn = snType.create({
|
|
||||||
model,
|
|
||||||
app,
|
|
||||||
selections,
|
|
||||||
});
|
|
||||||
setSupernova(sn);
|
|
||||||
} catch (err) {
|
|
||||||
setSnErr(err);
|
|
||||||
}
|
|
||||||
}, [snType, snTypeErr, ...deps]);
|
|
||||||
|
|
||||||
return [supernova, snTypeErr, snErr];
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export default function useSupernovaType({ nebulaContext, genericObjectType, genericObjectVersion }, deps = []) {
|
|
||||||
const [snType, setSnType] = useState(null);
|
|
||||||
const [err, setErr] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!genericObjectType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const withVersion = nebulaContext.nebbie.types.getSupportedVersion(genericObjectType, genericObjectVersion);
|
|
||||||
if (!withVersion) {
|
|
||||||
setErr({
|
|
||||||
message: `Could not find a version of '${genericObjectType}' that supports current object version. Did you forget to register ${genericObjectType}?`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setErr(null);
|
|
||||||
|
|
||||||
const get = async () => {
|
|
||||||
const SN = await nebulaContext.nebbie.types
|
|
||||||
.get({
|
|
||||||
name: genericObjectType,
|
|
||||||
version: withVersion,
|
|
||||||
})
|
|
||||||
.supernova();
|
|
||||||
setSnType(SN);
|
|
||||||
};
|
|
||||||
get();
|
|
||||||
}, [genericObjectType, genericObjectVersion, ...deps]);
|
|
||||||
|
|
||||||
return [snType, err];
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import vizualizationAPI from '../viz';
|
import vizualizationAPI from '../viz';
|
||||||
|
|
||||||
|
const cache = {};
|
||||||
/**
|
/**
|
||||||
* @typedef {object} GetObjectConfig
|
* @typedef {object} GetObjectConfig
|
||||||
* @property {string} id
|
* @property {string} id
|
||||||
@@ -13,24 +14,22 @@ import vizualizationAPI from '../viz';
|
|||||||
* @property {Array<'passive'|'select'|'interact'|'fetch'>} [context.permissions]
|
* @property {Array<'passive'|'select'|'interact'|'fetch'>} [context.permissions]
|
||||||
* @property {object=} properties
|
* @property {object=} properties
|
||||||
*/
|
*/
|
||||||
|
export default async function initiate({ id }, optional, context) {
|
||||||
export default function initiate({ id }, optional, context) {
|
const cacheKey = `${context.app.id}/${id}`;
|
||||||
return context.app.getObject(id).then(model => {
|
const model = cache[cacheKey] || (await context.app.getObject(id));
|
||||||
const viz = vizualizationAPI({
|
const [viz] = vizualizationAPI({
|
||||||
model,
|
model,
|
||||||
context,
|
context,
|
||||||
});
|
|
||||||
|
|
||||||
if (optional.options) {
|
|
||||||
viz.api.options(optional.options);
|
|
||||||
}
|
|
||||||
if (optional.context) {
|
|
||||||
viz.api.context(optional.context);
|
|
||||||
}
|
|
||||||
if (optional.element) {
|
|
||||||
return viz.api.mount(optional.element).then(() => viz.api);
|
|
||||||
}
|
|
||||||
|
|
||||||
return viz.api;
|
|
||||||
});
|
});
|
||||||
|
if (optional.element) {
|
||||||
|
await viz.mount(optional.element);
|
||||||
|
}
|
||||||
|
if (optional.options) {
|
||||||
|
viz.options(optional.options);
|
||||||
|
}
|
||||||
|
if (optional.context) {
|
||||||
|
viz.context(optional.context);
|
||||||
|
}
|
||||||
|
cache[cacheKey] = model;
|
||||||
|
return viz;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export default function hcHandler({ hc, def }) {
|
|||||||
// TODO - rename 'add' to 'added' since the callback is run after the dimension has been added
|
// TODO - rename 'add' to 'added' since the callback is run after the dimension has been added
|
||||||
def.dimensions.add(dimension, objectProperties);
|
def.dimensions.add(dimension, objectProperties);
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log('Should add dimension to layout exclude');
|
console.log('Should add dimension to layout exclude');
|
||||||
// add to layout exclude
|
// add to layout exclude
|
||||||
}
|
}
|
||||||
@@ -124,6 +125,7 @@ export default function hcHandler({ hc, def }) {
|
|||||||
|
|
||||||
def.measures.add(measure, objectProperties);
|
def.measures.add(measure, objectProperties);
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log('Should add measure to layout exclude');
|
console.log('Should add measure to layout exclude');
|
||||||
// add to layout exclude
|
// add to layout exclude
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function load(name, version, config, loader) {
|
|||||||
return sn;
|
return sn;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new Error(`Failed to load supernova: ${name}`);
|
throw new Error(`Failed to load supernova: ${name}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,68 +3,58 @@ import glueCell from './components/glue';
|
|||||||
import { get } from './object/observer';
|
import { get } from './object/observer';
|
||||||
import getPatches from './utils/patcher';
|
import getPatches from './utils/patcher';
|
||||||
|
|
||||||
import eventMixin from './selections/event-mixin';
|
|
||||||
|
|
||||||
const noopi = () => {};
|
const noopi = () => {};
|
||||||
|
|
||||||
export default function viz({ model, context: nebulaContext, initialUserProps = {} } = {}) {
|
export default function viz({ model, context: nebulaContext } = {}) {
|
||||||
let unmountCell = noopi;
|
let unmountCell = noopi;
|
||||||
|
let cellRef = null;
|
||||||
let mountedReference = null;
|
let mountedReference = null;
|
||||||
let onMount = null;
|
let onMount = null;
|
||||||
const mounted = new Promise(resolve => {
|
const mounted = new Promise(resolve => {
|
||||||
onMount = resolve;
|
onMount = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
let userProps = {
|
let initialSnContext = {
|
||||||
options: {},
|
theme: nebulaContext.theme,
|
||||||
context: {
|
permissions: [],
|
||||||
theme: nebulaContext.theme,
|
};
|
||||||
permissions: [],
|
let initialSnOptions = {};
|
||||||
},
|
|
||||||
...initialUserProps,
|
const setSnOptions = async opts => {
|
||||||
|
if (mountedReference) {
|
||||||
|
(async () => {
|
||||||
|
await mounted;
|
||||||
|
cellRef.current.setSnOptions({
|
||||||
|
...initialSnOptions,
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
// Handle setting options before mount
|
||||||
|
initialSnOptions = {
|
||||||
|
...initialSnOptions,
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let objectProps = {
|
const setSnContext = async ctx => {
|
||||||
layout: null,
|
if (mountedReference) {
|
||||||
sn: null,
|
(async () => {
|
||||||
error: null,
|
await mounted;
|
||||||
};
|
cellRef.current.setSnContext({
|
||||||
|
...initialSnContext,
|
||||||
const vizGlue = {
|
...ctx,
|
||||||
userProps() {
|
theme: nebulaContext.theme,
|
||||||
return userProps;
|
});
|
||||||
},
|
})();
|
||||||
objectProps() {
|
} else {
|
||||||
return objectProps;
|
// Handle setting context before mount
|
||||||
},
|
initialSnContext = {
|
||||||
};
|
...initialSnContext,
|
||||||
|
...ctx,
|
||||||
eventMixin(vizGlue);
|
};
|
||||||
|
}
|
||||||
const update = () => {
|
|
||||||
vizGlue.emit('changed');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUserProps = up => {
|
|
||||||
userProps = {
|
|
||||||
...userProps,
|
|
||||||
...up,
|
|
||||||
context: {
|
|
||||||
// DO NOT MAKE A DEEP COPY OF THEME AS IT WOULD MESS UP THE INSTANCE
|
|
||||||
...(userProps || {}).context,
|
|
||||||
...(up || {}).context,
|
|
||||||
theme: nebulaContext.theme,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
update();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setObjectProps = p => {
|
|
||||||
objectProps = {
|
|
||||||
...objectProps,
|
|
||||||
...p,
|
|
||||||
};
|
|
||||||
update();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,12 +76,12 @@ export default function viz({ model, context: nebulaContext, initialUserProps =
|
|||||||
throw new Error('Already mounted');
|
throw new Error('Already mounted');
|
||||||
}
|
}
|
||||||
mountedReference = element;
|
mountedReference = element;
|
||||||
unmountCell = glueCell({
|
[unmountCell, cellRef] = glueCell({
|
||||||
nebulaContext,
|
nebulaContext,
|
||||||
element,
|
element,
|
||||||
model,
|
model,
|
||||||
snContext: userProps.context,
|
initialSnContext,
|
||||||
snOptions: userProps.options,
|
initialSnOptions,
|
||||||
onMount,
|
onMount,
|
||||||
});
|
});
|
||||||
return mounted;
|
return mounted;
|
||||||
@@ -111,48 +101,19 @@ export default function viz({ model, context: nebulaContext, initialUserProps =
|
|||||||
if (patches.length) {
|
if (patches.length) {
|
||||||
return model.applyPatches(patches, true);
|
return model.applyPatches(patches, true);
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return undefined;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
options(opts) {
|
options(opts) {
|
||||||
setUserProps({
|
setSnOptions(opts);
|
||||||
options: opts,
|
|
||||||
});
|
|
||||||
return api;
|
return api;
|
||||||
},
|
},
|
||||||
context(ctx) {
|
context(ctx) {
|
||||||
setUserProps({
|
setSnContext(ctx);
|
||||||
context: ctx,
|
|
||||||
});
|
|
||||||
return api;
|
return api;
|
||||||
},
|
},
|
||||||
takeSnapshot() {
|
takeSnapshot() {
|
||||||
if (mountedReference) {
|
return cellRef.current.takeSnapshot();
|
||||||
const content = mountedReference.querySelector('.nebulajs-sn');
|
|
||||||
if (content) {
|
|
||||||
const rect = content.getBoundingClientRect();
|
|
||||||
if (objectProps.sn) {
|
|
||||||
const snapshot = {
|
|
||||||
...objectProps.layout,
|
|
||||||
snapshotData: {
|
|
||||||
object: {
|
|
||||||
size: {
|
|
||||||
w: rect.width,
|
|
||||||
h: rect.height,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof objectProps.sn.component.setSnapshotData === 'function') {
|
|
||||||
return Promise.resolve(objectProps.sn.component.setSnapshotData(snapshot)).then(v => v || snapshot);
|
|
||||||
}
|
|
||||||
return Promise.resolve(snapshot);
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error('No content'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error('Not mounted yet'));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// QVisualization API
|
// QVisualization API
|
||||||
@@ -166,8 +127,5 @@ export default function viz({ model, context: nebulaContext, initialUserProps =
|
|||||||
// toggleDataView() {},
|
// toggleDataView() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return [api, mounted];
|
||||||
setObjectProps,
|
|
||||||
api,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function snapshotter({ host, port }) {
|
|||||||
await page.waitFor(
|
await page.waitFor(
|
||||||
() =>
|
() =>
|
||||||
document.querySelector('.nebulajs-sn') &&
|
document.querySelector('.nebulajs-sn') &&
|
||||||
document.querySelector('.nebulajs-sn').getAttribute('data-rendered') === '1'
|
+document.querySelector('.nebulajs-sn').getAttribute('data-render-count') > 0
|
||||||
);
|
);
|
||||||
const image = await page.screenshot();
|
const image = await page.screenshot();
|
||||||
images[snap.key] = image;
|
images[snap.key] = image;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Cell from './Cell';
|
|||||||
|
|
||||||
export default function Collection({ types, cache }) {
|
export default function Collection({ types, cache }) {
|
||||||
const app = useContext(AppContext);
|
const app = useContext(AppContext);
|
||||||
const [layout] = useLayout(app);
|
const [layout] = useLayout({ app });
|
||||||
const [objects, setObjects] = useState(null);
|
const [objects, setObjects] = useState(null);
|
||||||
|
|
||||||
const { expandedObject } = useContext(VizContext);
|
const { expandedObject } = useContext(VizContext);
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function FieldsPopover({ alignTo, show, close, onSelected, type }
|
|||||||
app
|
app
|
||||||
);
|
);
|
||||||
|
|
||||||
const [layout] = useLayout(model, app);
|
const [layout] = useLayout({ model, app });
|
||||||
|
|
||||||
const fields = useMemo(
|
const fields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ export default function list(app, type = 'dimension') {
|
|||||||
const def = type === 'dimension' ? D : M;
|
const def = type === 'dimension' ? D : M;
|
||||||
|
|
||||||
const [model] = useModel(def, app);
|
const [model] = useModel(def, app);
|
||||||
const [layout] = useLayout(model, app);
|
const [layout] = useLayout({ model, app });
|
||||||
return [layout ? (layout.qDimensionList || layout.qMeasureList).qItems || [] : []];
|
return [layout ? (layout.qDimensionList || layout.qMeasureList).qItems || [] : []];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ const config = isEsm => {
|
|||||||
'useEffect',
|
'useEffect',
|
||||||
'useLayoutEffect',
|
'useLayoutEffect',
|
||||||
'useRef',
|
'useRef',
|
||||||
|
'useReducer',
|
||||||
|
'useImperativeHandle',
|
||||||
|
'forwardRef',
|
||||||
'useContext',
|
'useContext',
|
||||||
'useCallback',
|
'useCallback',
|
||||||
'useMemo',
|
'useMemo',
|
||||||
|
|||||||
Reference in New Issue
Block a user