mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-22 11:17:18 -05:00
feat(cli-serve): improve remote configs (#245)
This commit is contained in:
@@ -2,7 +2,7 @@ describe('sn', () => {
|
||||
const content = '.nebulajs-sn';
|
||||
it('should say hello', async () => {
|
||||
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
||||
await page.goto(`${process.env.BASE_URL}/render/app/${app}`);
|
||||
await page.goto(`${process.env.BASE_URL}/render/?app=${app}`);
|
||||
await page.waitForFunction(`!!document.querySelector('${content}')`);
|
||||
const text = await page.$eval(content, el => el.textContent);
|
||||
expect(text).to.equal('Hello!');
|
||||
|
||||
@@ -3,7 +3,7 @@ describe('interaction', () => {
|
||||
it('should select two bars', async () => {
|
||||
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
||||
await page.goto(
|
||||
`${process.env.BASE_URL}/render/app/${app}?cols=Alpha,=5+avg(Expression1)&&permissions=interact,select`
|
||||
`${process.env.BASE_URL}/render/?app=${app}&cols=Alpha,=5+avg(Expression1)&permissions=interact,select`
|
||||
);
|
||||
await page.waitForSelector(content, { visible: true });
|
||||
|
||||
|
||||
@@ -90,7 +90,11 @@ module.exports = async argv => {
|
||||
let snPath;
|
||||
let snName;
|
||||
let watcher;
|
||||
if (serveConfig.entry) {
|
||||
let snUrl;
|
||||
if (/^https?/.test(serveConfig.entry)) {
|
||||
snName = 'remote';
|
||||
snUrl = serveConfig.entry;
|
||||
} else if (serveConfig.entry) {
|
||||
snPath = path.resolve(context, serveConfig.entry);
|
||||
const parsed = path.parse(snPath);
|
||||
snName = parsed.name;
|
||||
@@ -111,7 +115,7 @@ module.exports = async argv => {
|
||||
}
|
||||
}
|
||||
|
||||
const ww = await initiateWatch({ snPath, snName: serveConfig.type || snName, host });
|
||||
const ww = snUrl ? null : await initiateWatch({ snPath, snName: serveConfig.type || snName, host });
|
||||
|
||||
const server = await webpackServe({
|
||||
host,
|
||||
@@ -119,7 +123,7 @@ module.exports = async argv => {
|
||||
enigmaConfig,
|
||||
webIntegrationId: serveConfig.webIntegrationId,
|
||||
snName: serveConfig.type || snName,
|
||||
snPath,
|
||||
snUrl,
|
||||
dev: process.env.MONO === 'true',
|
||||
open: serveConfig.open !== false,
|
||||
entryWatcher: ww,
|
||||
|
||||
@@ -15,6 +15,7 @@ module.exports = async ({
|
||||
enigmaConfig,
|
||||
webIntegrationId,
|
||||
snName,
|
||||
snUrl,
|
||||
dev = false,
|
||||
open = true,
|
||||
entryWatcher,
|
||||
@@ -74,7 +75,9 @@ module.exports = async ({
|
||||
before(app) {
|
||||
app.use(snapshotRoute, snapRouter);
|
||||
|
||||
entryWatcher.addRoutes(app);
|
||||
if (entryWatcher) {
|
||||
entryWatcher.addRoutes(app);
|
||||
}
|
||||
|
||||
app.get('/themes', (req, res) => {
|
||||
const arr = themes.map(theme => theme.key);
|
||||
@@ -96,9 +99,10 @@ module.exports = async ({
|
||||
webIntegrationId,
|
||||
supernova: {
|
||||
name: snName,
|
||||
url: snUrl,
|
||||
},
|
||||
sock: {
|
||||
port: entryWatcher.port,
|
||||
port: entryWatcher && entryWatcher.port,
|
||||
},
|
||||
themes: themes.map(theme => theme.key),
|
||||
});
|
||||
|
||||
@@ -246,7 +246,12 @@ export default function App({ app, info }) {
|
||||
<Toolbar variant="dense" style={{ background: theme.palette.background.paper }}>
|
||||
<Grid container>
|
||||
<Grid item container alignItems="center" style={{ width: 'auto' }}>
|
||||
<Button variant="contained" href={window.location.origin}>
|
||||
<Button
|
||||
variant="contained"
|
||||
href={`${window.location.origin}?engine_url=${info.engineUrl || ''}${
|
||||
info.webIntegrationId ? `&web-integration-id=${info.webIntegrationId}` : ''
|
||||
}`}
|
||||
>
|
||||
{/* <IconButton style={{ padding: '0px' }}>
|
||||
<ChevronLeft style={{ verticalAlign: 'middle' }} />
|
||||
</IconButton> */}
|
||||
|
||||
@@ -121,7 +121,9 @@ export default function({ id, expandable, minHeight }) {
|
||||
title="Open in single render view"
|
||||
href={
|
||||
model
|
||||
? `${document.location.href.replace(/\/dev\//, '/render/')}?object=${
|
||||
? `${document.location.href.replace(/\/dev\//, '/render/')}${
|
||||
window.location.search ? '&' : '?'
|
||||
}object=${
|
||||
model.id
|
||||
}&permissions=passive,interact,select&theme=${currentThemeName}&language=${language}`
|
||||
: ''
|
||||
|
||||
150
commands/serve/web/components/Hub.jsx
Normal file
150
commands/serve/web/components/Hub.jsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { createTheme, ThemeProvider } from '@nebula.js/ui/theme';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import OutlinedInput from '@material-ui/core/OutlinedInput';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
|
||||
import { info as connectionInfo, connect } from '../connect';
|
||||
|
||||
const theme = createTheme('light');
|
||||
|
||||
function URLInput({ info }) {
|
||||
const onKeyDown = e => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
window.location.href = `${window.location.origin}?engine_url=${e.target.value}`;
|
||||
break;
|
||||
case 'Escape':
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Box style={{ height: '30vh' }} />
|
||||
<Box boxShadow={24}>
|
||||
<OutlinedInput
|
||||
autoFocus
|
||||
fullWidth
|
||||
placeholder="Engine WebSocket URL"
|
||||
onKeyDown={onKeyDown}
|
||||
defaultValue={info.engineUrl}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AppList({ info }) {
|
||||
const [items, setItems] = useState();
|
||||
const [err, setError] = useState();
|
||||
useEffect(() => {
|
||||
connect()
|
||||
.then(g => g.getDocList().then(setItems))
|
||||
.catch(e => {
|
||||
const oops = {
|
||||
message: 'Something went wrong, check the console',
|
||||
hints: [],
|
||||
};
|
||||
if (e.target instanceof WebSocket) {
|
||||
oops.message = `Connection failed to ${info.engineUrl}`;
|
||||
if (!info.webIntegrationId) {
|
||||
oops.hints.push('If you are connecting to QCS/QSEoK, make sure to provide a web-integration-id');
|
||||
}
|
||||
setError(oops);
|
||||
return;
|
||||
}
|
||||
setError(oops);
|
||||
console.error(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<Paper elevation={24}>
|
||||
<Box p={2}>
|
||||
<Typography variant="h6" color="error" gutterBottom>
|
||||
Error
|
||||
</Typography>
|
||||
<Typography gutterBottom>{err.message} </Typography>
|
||||
{err.hints.map(hint => (
|
||||
<Typography key={hint} variant="body2" color="textSecondary">
|
||||
{hint}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!items) {
|
||||
return (
|
||||
<Grid container align="center" direction="column" spacing={2}>
|
||||
<Grid item>
|
||||
<CircularProgress size={48} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography>Connecting</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper elevation={24}>
|
||||
{items.length ? (
|
||||
<List>
|
||||
{items.map(li => (
|
||||
<ListItem
|
||||
button
|
||||
key={li.qDocId}
|
||||
component="a"
|
||||
href={`/dev/${window.location.search.replace(
|
||||
info.engineUrl,
|
||||
`${info.engineUrl}/app/${encodeURIComponent(li.qDocId)}`
|
||||
)}`}
|
||||
>
|
||||
<ListItemText primary={li.qTitle} secondary={li.qDocId} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Box p={2}>
|
||||
<Typography component="span">No apps found</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Hub() {
|
||||
const [n, setInfo] = useState();
|
||||
useEffect(() => {
|
||||
connectionInfo.then(setInfo);
|
||||
}, []);
|
||||
if (!n) {
|
||||
return null;
|
||||
}
|
||||
if (n.enigma.appId) {
|
||||
window.location.href = `/dev/${window.location.search}`;
|
||||
}
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Box p={[2, 4]}>
|
||||
<Grid container justify="center">
|
||||
<Grid item xs style={{ maxWidth: '800px' }}>
|
||||
{!n.invalid && n.engineUrl ? <AppList info={n} /> : <URLInput info={n} />}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -2,15 +2,8 @@ import enigma from 'enigma.js';
|
||||
import qixSchema from 'enigma.js/schemas/12.34.11.json';
|
||||
import SenseUtilities from 'enigma.js/sense-utilities';
|
||||
|
||||
import { requireFrom } from 'd3-require';
|
||||
|
||||
const params = (() => {
|
||||
const opts = {};
|
||||
const { pathname } = window.location;
|
||||
const am = pathname.match(/\/app\/([^/?&]+)/);
|
||||
if (am) {
|
||||
opts.app = decodeURIComponent(am[1]);
|
||||
}
|
||||
window.location.search
|
||||
.substring(1)
|
||||
.split('&')
|
||||
@@ -27,96 +20,112 @@ const params = (() => {
|
||||
return opts;
|
||||
})();
|
||||
|
||||
const getModule = name => requireFrom(async n => `/pkg/${encodeURIComponent(n)}`)(name);
|
||||
// Qlik Core: ws://<host>:<port>/app/<data-folder>/<app-name>
|
||||
// QCS: wss://<tenant-url>.<region>.qlikcloud.com/app/<app-GUID>
|
||||
// QSEoK: wss://<host>/app/<app-GUID>
|
||||
// QSEoW: wss://<host>/<virtual-proxy-prefix>/app/<app-GUID>
|
||||
const RX = /(wss?):\/\/([^/:?&]+)(?::(\d+))?/;
|
||||
const parseEngineURL = url => {
|
||||
const m = RX.exec(url);
|
||||
|
||||
const hotListeners = {};
|
||||
|
||||
const lightItUp = name => {
|
||||
if (!hotListeners[name]) {
|
||||
return;
|
||||
}
|
||||
hotListeners[name].forEach(fn => fn());
|
||||
};
|
||||
|
||||
const onHotChange = (name, fn) => {
|
||||
if (!hotListeners[name]) {
|
||||
hotListeners[name] = [];
|
||||
if (!m) {
|
||||
return {
|
||||
engineUrl: url,
|
||||
invalid: true,
|
||||
};
|
||||
}
|
||||
|
||||
hotListeners[name].push(fn);
|
||||
if (window[name]) {
|
||||
fn();
|
||||
let appId;
|
||||
let engineUrl = url;
|
||||
let appUrl;
|
||||
|
||||
const rxApp = /\/app\/([^?&#:]+)/.exec(url);
|
||||
|
||||
if (rxApp) {
|
||||
[, appId] = rxApp;
|
||||
engineUrl = url.substring(0, rxApp.index);
|
||||
appUrl = url;
|
||||
}
|
||||
return () => {
|
||||
// removeListener
|
||||
const idx = hotListeners[name].indexOf(fn);
|
||||
hotListeners[name].splice(idx, 1);
|
||||
|
||||
return {
|
||||
enigma: {
|
||||
secure: m[1] === 'wss',
|
||||
host: m[2],
|
||||
port: m[3] || undefined,
|
||||
appId,
|
||||
},
|
||||
engineUrl,
|
||||
appUrl,
|
||||
};
|
||||
};
|
||||
|
||||
window.onHotChange = onHotChange;
|
||||
|
||||
const initiateWatch = info => {
|
||||
const ws = new WebSocket(`ws://localhost:${info.sock.port}`);
|
||||
|
||||
const update = () => {
|
||||
getModule(info.supernova.name).then(mo => {
|
||||
window[info.supernova.name] = mo;
|
||||
lightItUp(info.supernova.name);
|
||||
});
|
||||
};
|
||||
|
||||
ws.onmessage = () => {
|
||||
update();
|
||||
};
|
||||
|
||||
update();
|
||||
};
|
||||
|
||||
const requestInfo = fetch('/info')
|
||||
const connectionInfo = fetch('/info')
|
||||
.then(response => response.json())
|
||||
.then(async info => {
|
||||
initiateWatch(info);
|
||||
const { webIntegrationId } = info;
|
||||
const rootPath = `${info.enigma.secure ? 'https' : 'http'}://${info.enigma.host}`;
|
||||
let headers = {};
|
||||
if (webIntegrationId) {
|
||||
const response = await fetch(`${rootPath}/api/v1/csrf-token`, {
|
||||
credentials: 'include',
|
||||
headers: { 'qlik-web-integration-id': webIntegrationId },
|
||||
});
|
||||
if (response.status === 401) {
|
||||
const loginUrl = new URL(`${rootPath}/login`);
|
||||
loginUrl.searchParams.append('returnto', window.location.href);
|
||||
loginUrl.searchParams.append('qlik-web-integration-id', webIntegrationId);
|
||||
window.location.href = loginUrl;
|
||||
return undefined;
|
||||
}
|
||||
const csrfToken = new Map(response.headers).get('qlik-csrf-token');
|
||||
headers = {
|
||||
'qlik-web-integration-id': webIntegrationId,
|
||||
'qlik-csrf-token': csrfToken,
|
||||
.then(async n => {
|
||||
let info = n;
|
||||
if (params.engine_url) {
|
||||
info = {
|
||||
...info,
|
||||
...parseEngineURL(params.engine_url),
|
||||
};
|
||||
} else if (params.app) {
|
||||
info = {
|
||||
...info,
|
||||
enigma: {
|
||||
...info.enigma,
|
||||
appId: params.app,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (params['web-integration-id']) {
|
||||
info.webIntegrationId = params['web-integration-id'];
|
||||
}
|
||||
if (info.invalid) {
|
||||
return info;
|
||||
}
|
||||
const rootPath = `${info.enigma.secure ? 'https' : 'http'}://${info.enigma.host}`;
|
||||
return {
|
||||
...info,
|
||||
rootPath,
|
||||
headers,
|
||||
};
|
||||
});
|
||||
|
||||
let headers;
|
||||
|
||||
const getHeaders = async ({ webIntegrationId, rootPath }) => {
|
||||
const response = await fetch(`${rootPath}/api/v1/csrf-token`, {
|
||||
credentials: 'include',
|
||||
headers: { 'qlik-web-integration-id': webIntegrationId },
|
||||
});
|
||||
if (response.status === 401) {
|
||||
const loginUrl = new URL(`${rootPath}/login`);
|
||||
loginUrl.searchParams.append('returnto', window.location.href);
|
||||
loginUrl.searchParams.append('qlik-web-integration-id', webIntegrationId);
|
||||
window.location.href = loginUrl;
|
||||
return undefined;
|
||||
}
|
||||
const csrfToken = new Map(response.headers).get('qlik-csrf-token');
|
||||
headers = {
|
||||
'qlik-web-integration-id': webIntegrationId,
|
||||
'qlik-csrf-token': csrfToken,
|
||||
};
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
const defaultConfig = {
|
||||
host: window.location.hostname || 'localhost',
|
||||
port: 9076,
|
||||
secure: false,
|
||||
};
|
||||
|
||||
let connection;
|
||||
const connect = () => {
|
||||
if (!connection) {
|
||||
connection = requestInfo.then(async info => {
|
||||
const { webIntegrationId, rootPath, headers } = info;
|
||||
connection = connectionInfo.then(async info => {
|
||||
const { webIntegrationId, rootPath } = info;
|
||||
if (webIntegrationId) {
|
||||
if (!headers) {
|
||||
headers = await getHeaders(info);
|
||||
}
|
||||
return {
|
||||
getDocList: async () => {
|
||||
const { data = [] } = await (
|
||||
@@ -149,10 +158,12 @@ const connect = () => {
|
||||
};
|
||||
|
||||
const openApp = id =>
|
||||
requestInfo.then(async info => {
|
||||
const { webIntegrationId, headers } = info;
|
||||
connectionInfo.then(async info => {
|
||||
let urlParams = {};
|
||||
if (webIntegrationId) {
|
||||
if (info.webIntegrationId) {
|
||||
if (!headers) {
|
||||
headers = await getHeaders(info);
|
||||
}
|
||||
urlParams = {
|
||||
...headers,
|
||||
};
|
||||
@@ -172,4 +183,4 @@ const openApp = id =>
|
||||
.then(global => global.openDoc(id));
|
||||
});
|
||||
|
||||
export { connect, openApp, params, requestInfo as info };
|
||||
export { connect, openApp, params, connectionInfo as info };
|
||||
|
||||
@@ -3,14 +3,15 @@ import ReactDOM from 'react-dom';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
import { openApp, params, info } from './connect';
|
||||
import { openApp, info } from './connect';
|
||||
import initiateWatch from './hot';
|
||||
|
||||
if (!params.app) {
|
||||
location.href = location.origin; //eslint-disable-line
|
||||
}
|
||||
|
||||
info.then($ =>
|
||||
openApp(params.app).then(app => {
|
||||
info.then($ => {
|
||||
if (!$.enigma.appId) {
|
||||
window.location.href = `/${window.location.search}`;
|
||||
}
|
||||
initiateWatch($);
|
||||
return openApp($.enigma.appId).then(app => {
|
||||
ReactDOM.render(<App app={app} info={$} />, document.querySelector('#app'));
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,73 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nebula hub</title>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Nebula hub</title>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(110deg, #91298C 0%, #45B3B2 100%);
|
||||
color: #404040;
|
||||
font: normal 14px/16px "Source Sans Pro", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#apps {
|
||||
background-color: white;
|
||||
max-width: 400px;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
margin-top: 32px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 32px 32px -16px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100% - 64px);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 16px;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#apps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#apps li {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
#apps li a {
|
||||
padding: 16px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#apps li a[href]:hover {
|
||||
padding: 16px;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="apps">
|
||||
<div class="title">Apps</div>
|
||||
<ul>
|
||||
<li><a>Loading...</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
body {
|
||||
background: linear-gradient(110deg, #91298c 0%, #45b3b2 100%);
|
||||
color: #404040;
|
||||
font: normal 14px/16px 'Source Sans Pro', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="hub"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { connect } from './connect';
|
||||
|
||||
const ul = document.querySelector('#apps ul');
|
||||
|
||||
connect().then(qix => {
|
||||
qix.getDocList().then(list => {
|
||||
const items = list
|
||||
.map(
|
||||
doc => `
|
||||
<li>
|
||||
<a href="/dev/app/${encodeURIComponent(doc.qDocId)}">${doc.qTitle}</a>
|
||||
</li>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
ul.innerHTML = items;
|
||||
});
|
||||
});
|
||||
6
commands/serve/web/eHub.jsx
Normal file
6
commands/serve/web/eHub.jsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Hub from './components/Hub';
|
||||
|
||||
ReactDOM.render(<Hub />, document.querySelector('#hub'));
|
||||
@@ -3,6 +3,7 @@ import snapshooter from '@nebula.js/snapshooter/dist/renderer';
|
||||
|
||||
import { openApp, params, info as serverInfo } from './connect';
|
||||
import runFixture from './run-fixture';
|
||||
import initiateWatch from './hot';
|
||||
|
||||
const nuke = async ({ app, supernova: { name }, themes, theme, language }) => {
|
||||
const nuked = nucleus.configured({
|
||||
@@ -29,11 +30,12 @@ const nuke = async ({ app, supernova: { name }, themes, theme, language }) => {
|
||||
};
|
||||
|
||||
async function renderWithEngine() {
|
||||
if (!params.app) {
|
||||
const info = await serverInfo;
|
||||
initiateWatch(info);
|
||||
if (!info.enigma.appId) {
|
||||
location.href = location.origin; //eslint-disable-line
|
||||
}
|
||||
const info = await serverInfo;
|
||||
const app = await openApp(params.app);
|
||||
const app = await openApp(info.enigma.appId);
|
||||
const nebbie = await nuke({ app, ...info, theme: params.theme, language: params.language });
|
||||
const element = document.querySelector('#chart-container');
|
||||
const vizCfg = {
|
||||
@@ -67,7 +69,9 @@ async function renderWithEngine() {
|
||||
}
|
||||
|
||||
async function renderSnapshot() {
|
||||
const { themes, supernova } = await serverInfo;
|
||||
const info = await serverInfo;
|
||||
const { themes, supernova } = info;
|
||||
initiateWatch(info);
|
||||
const element = document.querySelector('#chart-container');
|
||||
element.classList.toggle('full', true);
|
||||
|
||||
|
||||
49
commands/serve/web/hot.js
Normal file
49
commands/serve/web/hot.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { requireFrom } from 'd3-require';
|
||||
|
||||
const getModule = name => requireFrom(async n => `/pkg/${encodeURIComponent(n)}`)(name);
|
||||
|
||||
const getRemoteModule = url => requireFrom(() => url)();
|
||||
|
||||
const hotListeners = {};
|
||||
|
||||
const lightItUp = name => {
|
||||
if (!hotListeners[name]) {
|
||||
return;
|
||||
}
|
||||
hotListeners[name].forEach(fn => fn());
|
||||
};
|
||||
|
||||
const onHotChange = (name, fn) => {
|
||||
if (!hotListeners[name]) {
|
||||
hotListeners[name] = [];
|
||||
}
|
||||
|
||||
hotListeners[name].push(fn);
|
||||
if (window[name]) {
|
||||
fn();
|
||||
}
|
||||
return () => {
|
||||
// removeListener
|
||||
const idx = hotListeners[name].indexOf(fn);
|
||||
hotListeners[name].splice(idx, 1);
|
||||
};
|
||||
};
|
||||
|
||||
window.onHotChange = onHotChange;
|
||||
|
||||
export default function initiateWatch(info) {
|
||||
const update = () => {
|
||||
(info.supernova.url ? getRemoteModule(info.supernova.url) : getModule(info.supernova.name)).then(mo => {
|
||||
window[info.supernova.name] = mo;
|
||||
lightItUp(info.supernova.name);
|
||||
});
|
||||
};
|
||||
|
||||
if (info.sock.port) {
|
||||
const ws = new WebSocket(`ws://localhost:${info.sock.port}`);
|
||||
ws.onmessage = () => {
|
||||
update();
|
||||
};
|
||||
}
|
||||
update();
|
||||
}
|
||||
@@ -2,7 +2,7 @@ describe('sn', () => {
|
||||
const content = '.nebulajs-sn';
|
||||
it('should say hello', async () => {
|
||||
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
||||
await page.goto(`${process.env.BASE_URL}/render/app/${app}`);
|
||||
await page.goto(`${process.env.BASE_URL}/render/?app=${app}`);
|
||||
|
||||
await page.waitForSelector(content, { visible: true });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user