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: add variable and expression support to serve props #895

Merged
merged 5 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions commands/serve/lib/init-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ const options = {
type: 'array',
description: 'Array of scripts to inject',
},
flags: {
type: 'array',
description: 'Array of flags to enable',
},
stylesheets: {
type: 'array',
description: 'Array of stylesheets to inject',
Expand Down
2 changes: 1 addition & 1 deletion commands/serve/lib/webpack.build.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const cfg = ({ srcDir, distDir, dev = false, serveConfig = {} }) => {
eHub: [path.resolve(srcDir, 'eHub')],
fixtures: [path.resolve(__dirname, './fixtures.js')],
},
devtool: dev ? 'eval-cheap-module-source-map' : false,
devtool: dev ? 'source-map' : false,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source maps wheren't working correctly.

output: {
path: distDir,
filename: '[name].js',
Expand Down
2 changes: 1 addition & 1 deletion commands/serve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
],
"scripts": {
"build": "cross-env NODE_ENV=production DEFAULTS=true webpack --config ./lib/webpack.build.js",
"build:dev": "cross-env DEFAULTS=true webpack --config ./lib/webpack.build.js",
"build:dev": "cross-env NODE_ENV=development DEFAULTS=true webpack --config ./lib/webpack.build.js",
"lint": "eslint web",
"prepublishOnly": "rm -rf dist && yarn run build"
},
Expand Down
8 changes: 7 additions & 1 deletion commands/serve/web/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,13 @@ export default function App({ app, info }) {
padding: 0,
}}
>
<Properties sn={sn} viz={activeViz} isTemp={!objectListMode} storage={storage} />
<Properties
sn={sn}
viz={activeViz}
isTemp={!objectListMode}
storage={storage}
flags={info.flags}
/>
</Grid>
)}
</Grid>
Expand Down
74 changes: 66 additions & 8 deletions commands/serve/web/components/AutoComponents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
InputAdornment,
} from '@mui/material';

import { ExpandMore } from '@nebula.js/ui/icons';
import Variable from './property-panel/Variable';

const PREFIX = 'AutoComponents';

Expand Down Expand Up @@ -50,10 +52,19 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
},
}));

const getType = (value) => {
const getType = (value, key) => {
if (Array.isArray(value)) {
return 'array';
}

if (key === 'variable' && enableExpressions) {
return 'variable';
}

if (value && typeof value === 'object' && 'qStringExpression' in value && enableExpressions) {
return 'expression';
}

if (typeof value === 'boolean') {
return 'boolean';
}
Expand Down Expand Up @@ -113,13 +124,41 @@ function Num({ property, value, target, changed }) {
return <TextField variant="standard" fullWidth onChange={handleChange} onBlur={onBlur} label={property} value={s} />;
}

function Obj({ property, value, changed }) {
function Expression({ property, value, target, changed }) {
const initial = value.qStringExpression && value.qStringExpression.qExpr ? value.qStringExpression.qExpr : '';
const [s, setS] = useState(initial);
const handleChange = (e) => {
setS(e.target.value);
};
const onBlur = () => {
if (s !== value) {
target[property].qStringExpression = { qExpr: `=${s}` };
changed();
}
};

return (
<TextField
variant="standard"
InputProps={{
startAdornment: <InputAdornment position="start">=</InputAdornment>,
}}
fullWidth
onChange={handleChange}
onBlur={onBlur}
label={property}
value={s}
/>
);
}

function Obj({ property, value, changed, app }) {
return (
<StyledAccordion square className={classes.root}>
<AccordionSummary expandIcon={<ExpandMore />} className={classes.summary}>
<Typography>{property}</Typography>
</AccordionSummary>
<AccordionDetails className={classes.details}>{generateComponents(value, changed)}</AccordionDetails>
<AccordionDetails className={classes.details}>{generateComponents(value, changed, app)}</AccordionDetails>
</StyledAccordion>
);
}
Expand All @@ -129,24 +168,36 @@ const registeredComponents = {
string: Str,
object: Obj,
number: Num,
expression: Expression,
variable: Variable,
};

const QRX = /^q[A-Z]/;

export default function generateComponents(properties, changed) {
const whiteListedQ = {};

let enableExpressions = false;

export default function generateComponents(properties, changed, app, flags) {
if (flags && flags.PP_EXPRESSIONS) {
enableExpressions = true;
whiteListedQ.qStringExpression = true;
}

const components = Object.keys(properties)
.map((key) => {
if (['visualization', 'version'].indexOf(key) !== -1) {
return false;
}
if (QRX.test(key)) {
// skip q properties for now
if (!whiteListedQ[key] && QRX.test(key)) {
// skip q properties for now, but allow qStringExpression
Caele marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
const type = getType(properties[key]);
const type = getType(properties[key], key);
if (!registeredComponents[type]) {
return false;
}

return {
Component: registeredComponents[type],
property: key,
Expand All @@ -161,7 +212,14 @@ export default function generateComponents(properties, changed) {
<StyledGrid container direction="column" gap={0} alignItems="stretch">
{components.map((c) => (
<Grid item xs key={c.key} style={{ width: '100%' }}>
<c.Component key={c.key} property={c.property} value={c.value} target={properties} changed={changed} />
<c.Component
key={c.key}
app={app}
property={c.property}
value={c.value}
target={properties}
changed={changed}
/>
</Grid>
))}
</StyledGrid>
Expand Down
2 changes: 1 addition & 1 deletion commands/serve/web/components/FieldsPopover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export default function FieldsPopover({ alignTo, show, close, onSelected, type }
open={show}
onClose={close}
anchorEl={alignTo.current}
marginThreshold={theme.spacing(1)}
marginThreshold={16} // theme.spacing(1)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a MUI warning

elevation={3}
anchorOrigin={{
vertical: 'bottom',
Expand Down
8 changes: 5 additions & 3 deletions commands/serve/web/components/Properties.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, useContext } from 'react';

import { Divider, Grid, Checkbox, FormControlLabel } from '@mui/material';

import usePropertiesById from '@nebula.js/nucleus/src/hooks/usePropertiesById';

import AppContext from '../contexts/AppContext';
import Data from './property-panel/Data';
import generateComponents from './AutoComponents';

export default function Properties({ viz, sn, isTemp, storage }) {
export default function Properties({ viz, sn, isTemp, storage, flags }) {
const [properties, setProperties] = usePropertiesById(viz.id);
const app = useContext(AppContext);

const [isReadCacheEnabled, setReadCacheEnabled] = useState(storage.get('readFromCache') !== false);

Expand Down Expand Up @@ -51,7 +53,7 @@ export default function Properties({ viz, sn, isTemp, storage }) {
</>
)}
<Data properties={properties} setProperties={setProperties} sn={sn} />
{generateComponents(properties, changed)}
{generateComponents(properties, changed, app, flags)}
</div>
);
}
73 changes: 73 additions & 0 deletions commands/serve/web/components/property-panel/Variable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint no-param-reassign:0 */
import React, { useState, useEffect } from 'react';

import { Select, FormControl, MenuItem } from '@mui/material';

let variableList = null;

async function getVariables(app) {
if (!variableList) {
variableList = await app.createSessionObject({
qInfo: {
qType: 'VariableList',
},
qVariableListDef: {
qType: 'variable',
qShowReserved: false,
qShowConfig: false,
},
});
}
const reply = await variableList.getLayout();

const list = reply.qVariableList.qItems.map((item) => ({
value: item.qName,
label: item.qName.length > 50 ? `${item.qName.slice(0, 50)}...` : item.qName,
}));

return list;
}
// variable, {name, value}, properties
export default function Variable({ property, value, target, changed, app }) {
const [s, setS] = useState(value.name);
const [l, setL] = useState([]);

useEffect(() => {
async function fetchData() {
const list = await getVariables(app);
setL(list);
}
fetchData();
}, []);

const handleChange = (e) => {
setS(e.target.value);
target[property].name = e.target.value;
target[property].value = { qStringExpression: { qExpr: `$(${e.target.value})` } };
changed();
};
return l.length === 0 ? (
<em>No variables in app</em>
) : (
<FormControl size="small" fullWidth>
<Select
onChange={handleChange}
displayEmpty
fullWidth
value={s}
renderValue={(selected) => {
if (selected.length === 0) {
return <em>Select a variable</em>;
}
return selected;
}}
>
{l.map((c) => (
<MenuItem key={c.value} value={c.value}>
{c.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}