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';
|
const content = '.nebulajs-sn';
|
||||||
it('should say hello', async () => {
|
it('should say hello', async () => {
|
||||||
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
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}')`);
|
await page.waitForFunction(`!!document.querySelector('${content}')`);
|
||||||
const text = await page.$eval(content, el => el.textContent);
|
const text = await page.$eval(content, el => el.textContent);
|
||||||
expect(text).to.equal('Hello!');
|
expect(text).to.equal('Hello!');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ describe('interaction', () => {
|
|||||||
it('should select two bars', async () => {
|
it('should select two bars', async () => {
|
||||||
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
||||||
await page.goto(
|
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 });
|
await page.waitForSelector(content, { visible: true });
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ module.exports = async argv => {
|
|||||||
let snPath;
|
let snPath;
|
||||||
let snName;
|
let snName;
|
||||||
let watcher;
|
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);
|
snPath = path.resolve(context, serveConfig.entry);
|
||||||
const parsed = path.parse(snPath);
|
const parsed = path.parse(snPath);
|
||||||
snName = parsed.name;
|
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({
|
const server = await webpackServe({
|
||||||
host,
|
host,
|
||||||
@@ -119,7 +123,7 @@ module.exports = async argv => {
|
|||||||
enigmaConfig,
|
enigmaConfig,
|
||||||
webIntegrationId: serveConfig.webIntegrationId,
|
webIntegrationId: serveConfig.webIntegrationId,
|
||||||
snName: serveConfig.type || snName,
|
snName: serveConfig.type || snName,
|
||||||
snPath,
|
snUrl,
|
||||||
dev: process.env.MONO === 'true',
|
dev: process.env.MONO === 'true',
|
||||||
open: serveConfig.open !== false,
|
open: serveConfig.open !== false,
|
||||||
entryWatcher: ww,
|
entryWatcher: ww,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ module.exports = async ({
|
|||||||
enigmaConfig,
|
enigmaConfig,
|
||||||
webIntegrationId,
|
webIntegrationId,
|
||||||
snName,
|
snName,
|
||||||
|
snUrl,
|
||||||
dev = false,
|
dev = false,
|
||||||
open = true,
|
open = true,
|
||||||
entryWatcher,
|
entryWatcher,
|
||||||
@@ -74,7 +75,9 @@ module.exports = async ({
|
|||||||
before(app) {
|
before(app) {
|
||||||
app.use(snapshotRoute, snapRouter);
|
app.use(snapshotRoute, snapRouter);
|
||||||
|
|
||||||
entryWatcher.addRoutes(app);
|
if (entryWatcher) {
|
||||||
|
entryWatcher.addRoutes(app);
|
||||||
|
}
|
||||||
|
|
||||||
app.get('/themes', (req, res) => {
|
app.get('/themes', (req, res) => {
|
||||||
const arr = themes.map(theme => theme.key);
|
const arr = themes.map(theme => theme.key);
|
||||||
@@ -96,9 +99,10 @@ module.exports = async ({
|
|||||||
webIntegrationId,
|
webIntegrationId,
|
||||||
supernova: {
|
supernova: {
|
||||||
name: snName,
|
name: snName,
|
||||||
|
url: snUrl,
|
||||||
},
|
},
|
||||||
sock: {
|
sock: {
|
||||||
port: entryWatcher.port,
|
port: entryWatcher && entryWatcher.port,
|
||||||
},
|
},
|
||||||
themes: themes.map(theme => theme.key),
|
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 }}>
|
<Toolbar variant="dense" style={{ background: theme.palette.background.paper }}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item container alignItems="center" style={{ width: 'auto' }}>
|
<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' }}>
|
{/* <IconButton style={{ padding: '0px' }}>
|
||||||
<ChevronLeft style={{ verticalAlign: 'middle' }} />
|
<ChevronLeft style={{ verticalAlign: 'middle' }} />
|
||||||
</IconButton> */}
|
</IconButton> */}
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ export default function({ id, expandable, minHeight }) {
|
|||||||
title="Open in single render view"
|
title="Open in single render view"
|
||||||
href={
|
href={
|
||||||
model
|
model
|
||||||
? `${document.location.href.replace(/\/dev\//, '/render/')}?object=${
|
? `${document.location.href.replace(/\/dev\//, '/render/')}${
|
||||||
|
window.location.search ? '&' : '?'
|
||||||
|
}object=${
|
||||||
model.id
|
model.id
|
||||||
}&permissions=passive,interact,select&theme=${currentThemeName}&language=${language}`
|
}&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 qixSchema from 'enigma.js/schemas/12.34.11.json';
|
||||||
import SenseUtilities from 'enigma.js/sense-utilities';
|
import SenseUtilities from 'enigma.js/sense-utilities';
|
||||||
|
|
||||||
import { requireFrom } from 'd3-require';
|
|
||||||
|
|
||||||
const params = (() => {
|
const params = (() => {
|
||||||
const opts = {};
|
const opts = {};
|
||||||
const { pathname } = window.location;
|
|
||||||
const am = pathname.match(/\/app\/([^/?&]+)/);
|
|
||||||
if (am) {
|
|
||||||
opts.app = decodeURIComponent(am[1]);
|
|
||||||
}
|
|
||||||
window.location.search
|
window.location.search
|
||||||
.substring(1)
|
.substring(1)
|
||||||
.split('&')
|
.split('&')
|
||||||
@@ -27,96 +20,112 @@ const params = (() => {
|
|||||||
return opts;
|
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 = {};
|
if (!m) {
|
||||||
|
return {
|
||||||
const lightItUp = name => {
|
engineUrl: url,
|
||||||
if (!hotListeners[name]) {
|
invalid: true,
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
hotListeners[name].forEach(fn => fn());
|
|
||||||
};
|
|
||||||
|
|
||||||
const onHotChange = (name, fn) => {
|
|
||||||
if (!hotListeners[name]) {
|
|
||||||
hotListeners[name] = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hotListeners[name].push(fn);
|
let appId;
|
||||||
if (window[name]) {
|
let engineUrl = url;
|
||||||
fn();
|
let appUrl;
|
||||||
|
|
||||||
|
const rxApp = /\/app\/([^?&#:]+)/.exec(url);
|
||||||
|
|
||||||
|
if (rxApp) {
|
||||||
|
[, appId] = rxApp;
|
||||||
|
engineUrl = url.substring(0, rxApp.index);
|
||||||
|
appUrl = url;
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
// removeListener
|
return {
|
||||||
const idx = hotListeners[name].indexOf(fn);
|
enigma: {
|
||||||
hotListeners[name].splice(idx, 1);
|
secure: m[1] === 'wss',
|
||||||
|
host: m[2],
|
||||||
|
port: m[3] || undefined,
|
||||||
|
appId,
|
||||||
|
},
|
||||||
|
engineUrl,
|
||||||
|
appUrl,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
window.onHotChange = onHotChange;
|
const connectionInfo = fetch('/info')
|
||||||
|
|
||||||
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')
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(async info => {
|
.then(async n => {
|
||||||
initiateWatch(info);
|
let info = n;
|
||||||
const { webIntegrationId } = info;
|
if (params.engine_url) {
|
||||||
const rootPath = `${info.enigma.secure ? 'https' : 'http'}://${info.enigma.host}`;
|
info = {
|
||||||
let headers = {};
|
...info,
|
||||||
if (webIntegrationId) {
|
...parseEngineURL(params.engine_url),
|
||||||
const response = await fetch(`${rootPath}/api/v1/csrf-token`, {
|
};
|
||||||
credentials: 'include',
|
} else if (params.app) {
|
||||||
headers: { 'qlik-web-integration-id': webIntegrationId },
|
info = {
|
||||||
});
|
...info,
|
||||||
if (response.status === 401) {
|
enigma: {
|
||||||
const loginUrl = new URL(`${rootPath}/login`);
|
...info.enigma,
|
||||||
loginUrl.searchParams.append('returnto', window.location.href);
|
appId: params.app,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
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 {
|
return {
|
||||||
...info,
|
...info,
|
||||||
rootPath,
|
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 = {
|
const defaultConfig = {
|
||||||
host: window.location.hostname || 'localhost',
|
|
||||||
port: 9076,
|
|
||||||
secure: false,
|
secure: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let connection;
|
let connection;
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
connection = requestInfo.then(async info => {
|
connection = connectionInfo.then(async info => {
|
||||||
const { webIntegrationId, rootPath, headers } = info;
|
const { webIntegrationId, rootPath } = info;
|
||||||
if (webIntegrationId) {
|
if (webIntegrationId) {
|
||||||
|
if (!headers) {
|
||||||
|
headers = await getHeaders(info);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
getDocList: async () => {
|
getDocList: async () => {
|
||||||
const { data = [] } = await (
|
const { data = [] } = await (
|
||||||
@@ -149,10 +158,12 @@ const connect = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openApp = id =>
|
const openApp = id =>
|
||||||
requestInfo.then(async info => {
|
connectionInfo.then(async info => {
|
||||||
const { webIntegrationId, headers } = info;
|
|
||||||
let urlParams = {};
|
let urlParams = {};
|
||||||
if (webIntegrationId) {
|
if (info.webIntegrationId) {
|
||||||
|
if (!headers) {
|
||||||
|
headers = await getHeaders(info);
|
||||||
|
}
|
||||||
urlParams = {
|
urlParams = {
|
||||||
...headers,
|
...headers,
|
||||||
};
|
};
|
||||||
@@ -172,4 +183,4 @@ const openApp = id =>
|
|||||||
.then(global => global.openDoc(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 App from './components/App';
|
||||||
|
|
||||||
import { openApp, params, info } from './connect';
|
import { openApp, info } from './connect';
|
||||||
|
import initiateWatch from './hot';
|
||||||
|
|
||||||
if (!params.app) {
|
info.then($ => {
|
||||||
location.href = location.origin; //eslint-disable-line
|
if (!$.enigma.appId) {
|
||||||
}
|
window.location.href = `/${window.location.search}`;
|
||||||
|
}
|
||||||
info.then($ =>
|
initiateWatch($);
|
||||||
openApp(params.app).then(app => {
|
return openApp($.enigma.appId).then(app => {
|
||||||
ReactDOM.render(<App app={app} info={$} />, document.querySelector('#app'));
|
ReactDOM.render(<App app={app} info={$} />, document.querySelector('#app'));
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
|
|||||||
@@ -1,73 +1,27 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Nebula hub</title>
|
<title>Nebula hub</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html,
|
||||||
height: 100%;
|
body {
|
||||||
margin: 0;
|
height: 100%;
|
||||||
padding: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
padding: 0;
|
||||||
}
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(110deg, #91298C 0%, #45B3B2 100%);
|
background: linear-gradient(110deg, #91298c 0%, #45b3b2 100%);
|
||||||
color: #404040;
|
color: #404040;
|
||||||
font: normal 14px/16px "Source Sans Pro", Arial, sans-serif;
|
font: normal 14px/16px 'Source Sans Pro', Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
#apps {
|
</head>
|
||||||
background-color: white;
|
<body>
|
||||||
max-width: 400px;
|
<div id="hub"></div>
|
||||||
width: 80%;
|
</body>
|
||||||
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>
|
|
||||||
</html>
|
</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 { openApp, params, info as serverInfo } from './connect';
|
||||||
import runFixture from './run-fixture';
|
import runFixture from './run-fixture';
|
||||||
|
import initiateWatch from './hot';
|
||||||
|
|
||||||
const nuke = async ({ app, supernova: { name }, themes, theme, language }) => {
|
const nuke = async ({ app, supernova: { name }, themes, theme, language }) => {
|
||||||
const nuked = nucleus.configured({
|
const nuked = nucleus.configured({
|
||||||
@@ -29,11 +30,12 @@ const nuke = async ({ app, supernova: { name }, themes, theme, language }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function renderWithEngine() {
|
async function renderWithEngine() {
|
||||||
if (!params.app) {
|
const info = await serverInfo;
|
||||||
|
initiateWatch(info);
|
||||||
|
if (!info.enigma.appId) {
|
||||||
location.href = location.origin; //eslint-disable-line
|
location.href = location.origin; //eslint-disable-line
|
||||||
}
|
}
|
||||||
const info = await serverInfo;
|
const app = await openApp(info.enigma.appId);
|
||||||
const app = await openApp(params.app);
|
|
||||||
const nebbie = await nuke({ app, ...info, theme: params.theme, language: params.language });
|
const nebbie = await nuke({ app, ...info, theme: params.theme, language: params.language });
|
||||||
const element = document.querySelector('#chart-container');
|
const element = document.querySelector('#chart-container');
|
||||||
const vizCfg = {
|
const vizCfg = {
|
||||||
@@ -67,7 +69,9 @@ async function renderWithEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderSnapshot() {
|
async function renderSnapshot() {
|
||||||
const { themes, supernova } = await serverInfo;
|
const info = await serverInfo;
|
||||||
|
const { themes, supernova } = info;
|
||||||
|
initiateWatch(info);
|
||||||
const element = document.querySelector('#chart-container');
|
const element = document.querySelector('#chart-container');
|
||||||
element.classList.toggle('full', true);
|
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';
|
const content = '.nebulajs-sn';
|
||||||
it('should say hello', async () => {
|
it('should say hello', async () => {
|
||||||
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
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 });
|
await page.waitForSelector(content, { visible: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user