mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
refactor: internal packages structure (#94)
* refactor: internal packages structure * refactor: internal packages structure
This commit is contained in:
committed by
GitHub
parent
45eae91837
commit
a57abf1ead
21
commands/build/README.md
Normal file
21
commands/build/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# @nebula.js/cli-build
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install @nebula.js/cli@next
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
nebula build
|
||||
|
||||
Build supernova
|
||||
|
||||
Options:
|
||||
--version Show version number [boolean]
|
||||
--watch, -w [boolean] [default: false]
|
||||
-h, --help Show help [boolean]
|
||||
```
|
||||
|
||||
137
commands/build/build.js
Normal file
137
commands/build/build.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const path = require('path');
|
||||
|
||||
const rollup = require('rollup');
|
||||
const babel = require('rollup-plugin-babel');
|
||||
const postcss = require('rollup-plugin-postcss');
|
||||
const replace = require('rollup-plugin-replace');
|
||||
const node = require('rollup-plugin-node-resolve');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
|
||||
const config = ({ mode = 'production', format = 'umd', cwd = process.cwd() } = {}) => {
|
||||
const pkg = require(path.resolve(cwd, 'package.json')); // eslint-disable-line
|
||||
const { name, version, license, author } = pkg;
|
||||
|
||||
const auth = typeof author === 'object' ? `${author.name} <${author.email}>` : author || '';
|
||||
const moduleName = name.split('/').reverse()[0];
|
||||
|
||||
const banner = `/*
|
||||
* ${name} v${version}
|
||||
* Copyright (c) ${new Date().getFullYear()} ${auth}
|
||||
* Released under the ${license} license.
|
||||
*/
|
||||
`;
|
||||
|
||||
// all peers should be externals for esm bundle
|
||||
const external = format === 'esm' ? Object.keys(pkg.peerDependencies || {}) : [];
|
||||
|
||||
return {
|
||||
input: {
|
||||
input: path.resolve(cwd, 'src/index'),
|
||||
external,
|
||||
plugins: [
|
||||
replace({
|
||||
'process.env.NODE_ENV': JSON.stringify(mode === 'development' ? 'development' : 'production'),
|
||||
}),
|
||||
node({
|
||||
customResolveOptions: {
|
||||
moduleDirectory: path.resolve(cwd, 'node_modules'),
|
||||
},
|
||||
}),
|
||||
babel({
|
||||
babelrc: false,
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
targets: {
|
||||
browsers: ['ie 11', 'chrome 47'],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
postcss({}),
|
||||
...[
|
||||
mode === 'production'
|
||||
? terser({
|
||||
output: {
|
||||
preamble: banner,
|
||||
},
|
||||
})
|
||||
: false,
|
||||
],
|
||||
].filter(Boolean),
|
||||
},
|
||||
output: {
|
||||
banner,
|
||||
format,
|
||||
file: format === 'esm' && pkg.module ? pkg.module : pkg.main,
|
||||
name: moduleName,
|
||||
sourcemap: true,
|
||||
output: {
|
||||
preamble: banner,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const minified = async () => {
|
||||
const c = config({
|
||||
mode: 'production',
|
||||
format: 'umd',
|
||||
});
|
||||
const bundle = await rollup.rollup(c.input);
|
||||
await bundle.write(c.output);
|
||||
};
|
||||
|
||||
const esm = async () => {
|
||||
const c = config({
|
||||
mode: 'development',
|
||||
format: 'esm',
|
||||
});
|
||||
const bundle = await rollup.rollup(c.input);
|
||||
await bundle.write(c.output);
|
||||
};
|
||||
|
||||
const watch = async () => {
|
||||
const c = config({
|
||||
mode: 'development',
|
||||
format: 'esm',
|
||||
});
|
||||
|
||||
const watcher = rollup.watch({
|
||||
...c.input,
|
||||
output: c.output,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
watcher.on('event', event => {
|
||||
if (event.code === 'FATAL') {
|
||||
console.error(event);
|
||||
reject();
|
||||
}
|
||||
if (event.code === 'ERROR') {
|
||||
console.error(event);
|
||||
reject();
|
||||
}
|
||||
if (event.code === 'END') {
|
||||
resolve(watcher);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
async function build(argv) {
|
||||
if (argv.watch) {
|
||||
watch();
|
||||
} else {
|
||||
await minified();
|
||||
await esm();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
build,
|
||||
watch,
|
||||
};
|
||||
16
commands/build/command.js
Normal file
16
commands/build/command.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const { build } = require('./build');
|
||||
|
||||
module.exports = {
|
||||
command: 'build',
|
||||
desc: 'Build supernova',
|
||||
builder(yargs) {
|
||||
yargs.option('watch', {
|
||||
type: 'boolean',
|
||||
alias: 'w',
|
||||
default: false,
|
||||
});
|
||||
},
|
||||
handler(argv) {
|
||||
build(argv);
|
||||
},
|
||||
};
|
||||
35
commands/build/package.json
Normal file
35
commands/build/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@nebula.js/cli-build",
|
||||
"version": "0.1.0-alpha.18",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/qlik-oss/nebula.js.git"
|
||||
},
|
||||
"main": "build.js",
|
||||
"files": [
|
||||
"command.js"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.5.5",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"chalk": "^2.4.2",
|
||||
"rollup": "^1.20.2",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-postcss": "^2.0.3",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"rollup-plugin-terser": "^5.1.1",
|
||||
"yargs": "^14.0.0"
|
||||
}
|
||||
}
|
||||
22
commands/cli/README.md
Normal file
22
commands/cli/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# @nebula.js/cli
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install -g @nebula.js/cli@next
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
nebula <command> [options]
|
||||
|
||||
Commands:
|
||||
nebula build Build supernova
|
||||
nebula create <name> Create a supernova
|
||||
nebula serve Dev server
|
||||
|
||||
Options:
|
||||
--version Show version number [boolean]
|
||||
-h, --help Show help [boolean]
|
||||
```
|
||||
17
commands/cli/lib/index.js
Executable file
17
commands/cli/lib/index.js
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
const yargs = require('yargs');
|
||||
|
||||
const build = require('@nebula.js/cli-build/command');
|
||||
const create = require('@nebula.js/cli-create/command');
|
||||
const serve = require('@nebula.js/cli-serve/command');
|
||||
const sense = require('@nebula.js/cli-sense/command');
|
||||
|
||||
yargs
|
||||
.usage('nebula <command> [options]')
|
||||
.command(build)
|
||||
.command(create)
|
||||
.command(serve)
|
||||
.command(sense)
|
||||
.demandCommand()
|
||||
.alias('h', 'help')
|
||||
.wrap(Math.min(80, yargs.terminalWidth())).argv;
|
||||
32
commands/cli/package.json
Normal file
32
commands/cli/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@nebula.js/cli",
|
||||
"version": "0.1.0-alpha.18",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/qlik-oss/nebula.js.git"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"bin": {
|
||||
"nebula": "lib/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nebula.js/cli-build": "0.1.0-alpha.18",
|
||||
"@nebula.js/cli-create": "0.1.0-alpha.18",
|
||||
"@nebula.js/cli-sense": "0.1.0-alpha.18",
|
||||
"@nebula.js/cli-serve": "0.1.0-alpha.18",
|
||||
"chalk": "^2.4.2",
|
||||
"yargs": "^14.0.0"
|
||||
}
|
||||
}
|
||||
32
commands/create/README.md
Normal file
32
commands/create/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @nebula.js/cli-create
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install -g @nebula.js/cli@next
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI
|
||||
|
||||
```sh
|
||||
nebula create <name>
|
||||
|
||||
Create a supernova
|
||||
|
||||
Positionals:
|
||||
name name of the project [string] [required]
|
||||
|
||||
Options:
|
||||
--version Show version number [boolean]
|
||||
--install Run package installation step [boolean] [default: true]
|
||||
--pkgm Package manager [string] [choices: "npm", "yarn"]
|
||||
-h, --help Show help [boolean]
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```sh
|
||||
nebula create hello-sunshine
|
||||
```
|
||||
34
commands/create/command.js
Normal file
34
commands/create/command.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const create = require('./lib/create');
|
||||
|
||||
module.exports = {
|
||||
command: 'create <name>',
|
||||
desc: 'Create a supernova',
|
||||
builder(yargs) {
|
||||
yargs.positional('name', {
|
||||
type: 'string',
|
||||
description: 'name of the project',
|
||||
});
|
||||
yargs.option('install', {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Run package installation step',
|
||||
});
|
||||
yargs.option('pkgm', {
|
||||
type: 'string',
|
||||
choices: ['npm', 'yarn'],
|
||||
description: 'Package manager',
|
||||
});
|
||||
yargs.option('picasso', {
|
||||
type: 'string',
|
||||
choices: ['none', 'minimal', 'barchart'],
|
||||
description: 'Picasso template',
|
||||
});
|
||||
yargs.option('author', {
|
||||
type: 'string',
|
||||
description: 'Package author',
|
||||
});
|
||||
},
|
||||
handler(argv) {
|
||||
create(argv);
|
||||
},
|
||||
};
|
||||
198
commands/create/lib/create.js
Normal file
198
commands/create/lib/create.js
Normal file
@@ -0,0 +1,198 @@
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
const ejs = require('ejs');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const pkg = require('../package.json');
|
||||
|
||||
const hasYarn = () => {
|
||||
try {
|
||||
execSync('yarnpkg --version', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const author = async cwd => {
|
||||
try {
|
||||
const email = execSync('git config --get user.email', { cwd })
|
||||
.toString()
|
||||
.trim();
|
||||
const name = execSync('git config --get user.name', { cwd })
|
||||
.toString()
|
||||
.trim();
|
||||
return {
|
||||
email,
|
||||
name,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
email: '',
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const parseAuthor = (str = '') => {
|
||||
const m = str.match(/([^<(]+)?(<([^\s]+)>)?/);
|
||||
return {
|
||||
name: m && m[1] ? m[1].trim() : '',
|
||||
email: m && m[3] ? m[3].trim() : '',
|
||||
};
|
||||
};
|
||||
|
||||
function cpy(root, destination) {
|
||||
return (source, target, data) => {
|
||||
if (data) {
|
||||
const content = fs.readFileSync(path.resolve(root, ...source.split('/')), { encoding: 'utf8' });
|
||||
const rendered = ejs.render(content, data);
|
||||
fs.writeFileSync(path.resolve(destination, ...target.split('/')), rendered);
|
||||
} else {
|
||||
fse.copyFileSync(path.resolve(root, ...source.split('/')), path.resolve(destination, ...target.split('/')));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const create = async argv => {
|
||||
const { name } = argv;
|
||||
|
||||
const projectFolder = name;
|
||||
const packageName = name.split('/').slice(-1)[0];
|
||||
|
||||
const cwd = process.cwd();
|
||||
const templatesRoot = path.resolve(__dirname, '..', 'templates');
|
||||
const destination = path.resolve(cwd, projectFolder);
|
||||
|
||||
let options = {
|
||||
install: true,
|
||||
...argv,
|
||||
pkgm: argv.pkgm || ((await hasYarn()) ? 'yarn' : 'npm'),
|
||||
author: argv.author ? parseAuthor(argv.author) : await author(),
|
||||
};
|
||||
|
||||
const results = {};
|
||||
|
||||
if (await fse.exists(destination)) {
|
||||
console.error(chalk.red(`Oopsie, looks like '${projectFolder}' already exists. Try a different name.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const prompt = async () => {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'picasso',
|
||||
message: 'Pick a picasso template',
|
||||
default: 'none',
|
||||
choices: ['none', 'minimal', 'barchart'],
|
||||
when: !argv.picasso,
|
||||
},
|
||||
]);
|
||||
|
||||
options = { ...options, ...answers };
|
||||
};
|
||||
|
||||
const write = async () => {
|
||||
console.log('\n');
|
||||
console.log('> Generating files...');
|
||||
const { picasso } = options;
|
||||
fse.ensureDirSync(destination);
|
||||
|
||||
// ==== common files ====
|
||||
// copy raw files
|
||||
['editorconfig', 'eslintignore', 'gitignore', 'eslintrc.json'].forEach(filename =>
|
||||
fs.copyFileSync(
|
||||
path.resolve(templatesRoot, 'common', `_${filename}`), // copying dotfiles may not always work, so they are prefixed with an underline
|
||||
path.resolve(destination, `.${filename}`)
|
||||
)
|
||||
);
|
||||
|
||||
const copy = cpy(templatesRoot, destination);
|
||||
|
||||
copy('common/README.md', 'README.md', { name: packageName });
|
||||
|
||||
// ==== template files ====
|
||||
const folders = [];
|
||||
if (picasso !== 'none') {
|
||||
folders.push('picasso/common');
|
||||
folders.push(`picasso/${picasso}`);
|
||||
} else {
|
||||
folders.push('none');
|
||||
}
|
||||
|
||||
const traverse = (sourceFolder, targetFolder = '') => {
|
||||
const files = fs.readdirSync(path.resolve(templatesRoot, sourceFolder));
|
||||
|
||||
files.forEach(file => {
|
||||
const p = `${sourceFolder}/${file}`;
|
||||
const stats = fs.lstatSync(path.resolve(templatesRoot, p));
|
||||
const next = `${targetFolder}/${file}`.replace(/^\//, '');
|
||||
if (stats.isDirectory()) {
|
||||
fse.ensureDirSync(path.resolve(destination, next));
|
||||
traverse(p, next);
|
||||
} else if (file === '_package.json') {
|
||||
copy(`${sourceFolder}/_package.json`, 'package.json', {
|
||||
name: packageName,
|
||||
description: '',
|
||||
user: options.author.name,
|
||||
email: options.author.email,
|
||||
nebulaVersion: pkg.version,
|
||||
});
|
||||
} else {
|
||||
copy(`${sourceFolder}/${file}`, next);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
folders.forEach(folder => {
|
||||
traverse(folder);
|
||||
});
|
||||
};
|
||||
|
||||
const install = async () => {
|
||||
if (options.install !== false) {
|
||||
console.log('> Installing dependencies...');
|
||||
console.log('\n');
|
||||
const command = `${options.pkgm} install`;
|
||||
try {
|
||||
execSync(command, {
|
||||
cwd: destination,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log('\n');
|
||||
} catch (e) {
|
||||
console.log('\n');
|
||||
console.log(`> Something went wrong when running ${chalk.yellow(command)}, try running the command yourself.`);
|
||||
results.fail = true;
|
||||
results.failedInstall = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const end = async () => {
|
||||
const p = options.pkgm;
|
||||
if (!results.fail) {
|
||||
console.log(`> Successfully created project ${chalk.yellow(options.name)}`);
|
||||
}
|
||||
console.log('> Get started with the following commands:');
|
||||
console.log('\n');
|
||||
console.log(chalk.cyan(` cd ${projectFolder}`));
|
||||
if (options.install === false || results.failedInstall) {
|
||||
console.log(chalk.cyan(` ${p} install`));
|
||||
}
|
||||
console.log(chalk.cyan(` ${p} run start`));
|
||||
console.log('\n');
|
||||
};
|
||||
|
||||
await prompt();
|
||||
await write();
|
||||
await install();
|
||||
await end();
|
||||
};
|
||||
|
||||
module.exports = create;
|
||||
30
commands/create/package.json
Normal file
30
commands/create/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@nebula.js/cli-create",
|
||||
"version": "0.1.0-alpha.18",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "lib/create.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/qlik-oss/nebula.js.git"
|
||||
},
|
||||
"files": [
|
||||
"command.js",
|
||||
"templates",
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"generate": "yo ./generator/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^2.4.2",
|
||||
"ejs": "^2.6.2",
|
||||
"fs-extra": "^8.1.0",
|
||||
"inquirer": "^7.0.0"
|
||||
}
|
||||
}
|
||||
7
commands/create/templates/common/README.md
Normal file
7
commands/create/templates/common/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# <%= name %>
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
npm install <%= name %>
|
||||
```
|
||||
15
commands/create/templates/common/_editorconfig
Normal file
15
commands/create/templates/common/_editorconfig
Normal file
@@ -0,0 +1,15 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
3
commands/create/templates/common/_eslintignore
Normal file
3
commands/create/templates/common/_eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
coverage
|
||||
node_modules
|
||||
42
commands/create/templates/common/_eslintrc.json
Normal file
42
commands/create/templates/common/_eslintrc.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base"
|
||||
],
|
||||
"rules": {
|
||||
"max-len": 0,
|
||||
"no-plusplus": 0,
|
||||
"no-bitwise" : 0,
|
||||
"no-unused-expressions": 0,
|
||||
"import/no-extraneous-dependencies": [2, { "devDependencies": true }]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.spec.js"],
|
||||
"env": {
|
||||
"browser": false,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"globals": {
|
||||
"chai": false,
|
||||
"expect": false,
|
||||
"sinon": false,
|
||||
"aw": false,
|
||||
"page": false
|
||||
},
|
||||
"plugins": [
|
||||
"mocha"
|
||||
],
|
||||
"rules": {
|
||||
"mocha/no-exclusive-tests": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
commands/create/templates/common/_gitignore
Normal file
11
commands/create/templates/common/_gitignore
Normal file
@@ -0,0 +1,11 @@
|
||||
*.log
|
||||
*.log
|
||||
.cache
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
.npmrc
|
||||
|
||||
node_modules
|
||||
coverage
|
||||
dist/
|
||||
31
commands/create/templates/none/_package.json
Normal file
31
commands/create/templates/none/_package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "<%= name %>",
|
||||
"version": "0.1.0",
|
||||
"description": "<%= description %>",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "<%= user %>",
|
||||
"email": "<%= email %>"
|
||||
},
|
||||
"keywords": ["qlik", "nebula", "supernova"],
|
||||
"files": ["dist"],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"main": "dist/<%= name %>.js",
|
||||
"module": "dist/<%= name %>.esm.js",
|
||||
"scripts": {
|
||||
"build": "nebula build",
|
||||
"lint": "eslint src",
|
||||
"start": "nebula serve",
|
||||
"test:integration": "aw puppet --testExt '*.int.js' --glob 'test/integration/**/*.int.js' --chrome.headless true --mocha.timeout 15000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@after-work.js/aw": "^6.0.3",
|
||||
"@nebula.js/cli": "<%= nebulaVersion %>",
|
||||
"eslint": "^5.12.1",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-plugin-import": "^2.15.0",
|
||||
"eslint-plugin-mocha": "^5.2.1"
|
||||
}
|
||||
}
|
||||
3
commands/create/templates/none/src/data.js
Normal file
3
commands/create/templates/none/src/data.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
targets: [],
|
||||
};
|
||||
25
commands/create/templates/none/src/index.js
Normal file
25
commands/create/templates/none/src/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import properties from './object-properties';
|
||||
import data from './data';
|
||||
|
||||
export default function supernova(env) {
|
||||
return {
|
||||
qae: {
|
||||
properties,
|
||||
data,
|
||||
},
|
||||
component: {
|
||||
created() {
|
||||
console.log('created', env);
|
||||
},
|
||||
mounted(element) {
|
||||
element.innerHTML = '<div>Hello!</div>'; // eslint-disable-line
|
||||
},
|
||||
render({ layout, context }) {
|
||||
console.log('render', layout, context);
|
||||
},
|
||||
resize() {},
|
||||
willUnmount() {},
|
||||
destroy() {},
|
||||
},
|
||||
};
|
||||
}
|
||||
8
commands/create/templates/none/src/object-properties.js
Normal file
8
commands/create/templates/none/src/object-properties.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const properties = {
|
||||
showTitles: true,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
footnote: '',
|
||||
};
|
||||
|
||||
export default properties;
|
||||
10
commands/create/templates/none/test/integration/hello.int.js
Normal file
10
commands/create/templates/none/test/integration/hello.int.js
Normal file
@@ -0,0 +1,10 @@
|
||||
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.testServer.url}/render/app/${app}`);
|
||||
await page.waitForFunction(`!!document.querySelector('${content}')`);
|
||||
const text = await page.$eval(content, el => el.textContent);
|
||||
expect(text).to.equal('Hello!');
|
||||
});
|
||||
});
|
||||
19
commands/create/templates/none/test/integration/setup.int.js
Normal file
19
commands/create/templates/none/test/integration/setup.int.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const serve = require('@nebula.js/cli-serve'); // eslint-disable-line
|
||||
|
||||
let s;
|
||||
|
||||
before(async () => {
|
||||
s = await serve({
|
||||
open: false,
|
||||
});
|
||||
|
||||
process.testServer = s;
|
||||
|
||||
page.on('pageerror', e => {
|
||||
console.log('Error:', e.message, e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
s.close();
|
||||
});
|
||||
15
commands/create/templates/picasso/barchart/src/data.js
Normal file
15
commands/create/templates/picasso/barchart/src/data.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
targets: [
|
||||
{
|
||||
path: 'qHyperCubeDef',
|
||||
dimensions: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
},
|
||||
measures: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
const properties = {
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [],
|
||||
qMeasures: [],
|
||||
qInitialDataFetch: [{ qWidth: 2, qHeight: 5000 }],
|
||||
qSuppressZero: false,
|
||||
qSuppressMissing: true,
|
||||
},
|
||||
showTitles: true,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
footnote: '',
|
||||
};
|
||||
|
||||
export default properties;
|
||||
@@ -0,0 +1,68 @@
|
||||
export default function picassoDefinition({ layout, context }) {
|
||||
if (!layout.qHyperCube) {
|
||||
throw new Error('Layout is missing a hypercube');
|
||||
}
|
||||
return {
|
||||
scales: {
|
||||
x: { data: { extract: { field: 'qDimensionInfo/0' } } },
|
||||
y: {
|
||||
data: { field: 'qMeasureInfo/0' },
|
||||
expand: 0.2,
|
||||
include: [0],
|
||||
invert: true,
|
||||
},
|
||||
},
|
||||
components: [
|
||||
{
|
||||
type: 'axis',
|
||||
dock: 'left',
|
||||
scale: 'y',
|
||||
},
|
||||
{
|
||||
type: 'axis',
|
||||
dock: 'bottom',
|
||||
scale: 'x',
|
||||
},
|
||||
{
|
||||
type: 'box',
|
||||
data: {
|
||||
extract: {
|
||||
field: 'qDimensionInfo/0',
|
||||
props: {
|
||||
start: 0,
|
||||
end: { field: 'qMeasureInfo/0' },
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
major: { scale: 'x' },
|
||||
minor: { scale: 'y' },
|
||||
box: {
|
||||
width: 0.7,
|
||||
},
|
||||
},
|
||||
brush:
|
||||
context.permissions.indexOf('interact') !== -1 && context.permissions.indexOf('select') !== -1
|
||||
? {
|
||||
trigger: [
|
||||
{
|
||||
contexts: ['selection'],
|
||||
},
|
||||
],
|
||||
consume: [
|
||||
{
|
||||
context: 'selection',
|
||||
data: ['', 'end'],
|
||||
style: {
|
||||
inactive: {
|
||||
opacity: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
describe('interaction', () => {
|
||||
const content = '.nebulajs-sn';
|
||||
it('should select two bars', async () => {
|
||||
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
|
||||
await page.goto(
|
||||
`${process.testServer.url}/render/app/${app}?cols=Alpha,=5+avg(Expression1)&&permissions=interact,select`
|
||||
);
|
||||
await page.waitForFunction(`!!document.querySelector('${content}')`);
|
||||
|
||||
await page.click('rect[data-label="K"]');
|
||||
await page.click('rect[data-label="S"]');
|
||||
await page.click('button[title="Confirm selection"]');
|
||||
|
||||
const rects = await page.$$eval('rect[data-label]', sel => sel.map(r => r.getAttribute('data-label')));
|
||||
expect(rects).to.eql(['K', 'S']);
|
||||
});
|
||||
});
|
||||
33
commands/create/templates/picasso/common/_package.json
Normal file
33
commands/create/templates/picasso/common/_package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "<%= name %>",
|
||||
"version": "0.1.0",
|
||||
"description": "<%= description %>",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "<%= user %>",
|
||||
"email": "<%= email %>"
|
||||
},
|
||||
"keywords": ["qlik", "nebula", "supernova"],
|
||||
"files": ["dist"],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"main": "dist/<%= name %>.js",
|
||||
"module": "dist/<%= name %>.esm.js",
|
||||
"scripts": {
|
||||
"build": "nebula build",
|
||||
"lint": "eslint src",
|
||||
"start": "nebula serve",
|
||||
"test:integration": "aw puppet --testExt '*.int.js' --glob 'test/integration/**/*.int.js' --chrome.headless true --chrome.slowMo 10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@after-work.js/aw": "^6.0.3",
|
||||
"@nebula.js/cli": "<%= nebulaVersion %>",
|
||||
"eslint": "^5.12.1",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-plugin-import": "^2.15.0",
|
||||
"eslint-plugin-mocha": "^5.2.1",
|
||||
"picasso.js": "^0.25.2",
|
||||
"picasso-plugin-q": "^0.25.2"
|
||||
}
|
||||
}
|
||||
49
commands/create/templates/picasso/common/src/index.js
Normal file
49
commands/create/templates/picasso/common/src/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import picassojs from 'picasso.js';
|
||||
import picassoQ from 'picasso-plugin-q';
|
||||
|
||||
import properties from './object-properties';
|
||||
import data from './data';
|
||||
import picSelections from './pic-selections';
|
||||
import definition from './pic-definition';
|
||||
|
||||
export default function supernova(/* env */) {
|
||||
const picasso = picassojs();
|
||||
picasso.use(picassoQ);
|
||||
|
||||
return {
|
||||
qae: {
|
||||
properties,
|
||||
data,
|
||||
},
|
||||
component: {
|
||||
created() {},
|
||||
mounted(element) {
|
||||
this.pic = picasso.chart({
|
||||
element,
|
||||
data: [],
|
||||
settings: {},
|
||||
});
|
||||
this.picsel = picSelections({
|
||||
selections: this.selections,
|
||||
brush: this.pic.brush('selection'),
|
||||
picassoQ,
|
||||
});
|
||||
},
|
||||
render({ layout, context }) {
|
||||
this.pic.update({
|
||||
data: [
|
||||
{
|
||||
type: 'q',
|
||||
key: 'qHyperCube',
|
||||
data: layout.qHyperCube,
|
||||
},
|
||||
],
|
||||
settings: definition({ layout, context }),
|
||||
});
|
||||
},
|
||||
resize() {},
|
||||
willUnmount() {},
|
||||
destroy() {},
|
||||
},
|
||||
};
|
||||
}
|
||||
131
commands/create/templates/picasso/common/src/pic-selections.js
Normal file
131
commands/create/templates/picasso/common/src/pic-selections.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/* eslint no-param-reassign: 0 */
|
||||
|
||||
// --- enable keyboard accessibility ---
|
||||
// pressing enter (escape) key should confirm (cancel) selections
|
||||
const KEYS = {
|
||||
ENTER: 'Enter',
|
||||
ESCAPE: 'Escape',
|
||||
IE11_ESC: 'Esc',
|
||||
SHIFT: 'Shift',
|
||||
};
|
||||
|
||||
const instances = [];
|
||||
let expando = 0;
|
||||
const confirmOrCancelSelection = e => {
|
||||
const active = instances.filter(a => a.selections && a.selections.isActive());
|
||||
if (!active.length) {
|
||||
return;
|
||||
}
|
||||
if (e.key === KEYS.ENTER) {
|
||||
active.forEach(a => a.selections.confirm());
|
||||
} else if (e.key === KEYS.ESCAPE || e.key === KEYS.IE11_ESC) {
|
||||
active.forEach(a => a.selections.cancel());
|
||||
}
|
||||
};
|
||||
|
||||
const setup = () => {
|
||||
document.addEventListener('keyup', confirmOrCancelSelection);
|
||||
};
|
||||
|
||||
const teardown = () => {
|
||||
document.removeEventListener('keyup', confirmOrCancelSelection);
|
||||
};
|
||||
// ------------------------------------------------------
|
||||
|
||||
const addListeners = (emitter, listeners) => {
|
||||
Object.keys(listeners).forEach(type => {
|
||||
emitter.on(type, listeners[type]);
|
||||
});
|
||||
};
|
||||
|
||||
const removeListeners = (emitter, listeners) => {
|
||||
Object.keys(listeners).forEach(type => {
|
||||
emitter.removeListener(type, listeners[type]);
|
||||
});
|
||||
};
|
||||
|
||||
export default function({ selections, brush, picassoQ } = {}, { path = '/qHyperCubeDef' } = {}) {
|
||||
if (!selections) {
|
||||
return {
|
||||
release: () => {},
|
||||
};
|
||||
}
|
||||
const key = ++expando;
|
||||
|
||||
let layout = null;
|
||||
|
||||
// interceptors primary job is to ensure selections only occur on either values OR ranges
|
||||
const valueInterceptor = added => {
|
||||
const brushes = brush.brushes();
|
||||
brushes.forEach(b => {
|
||||
if (b.type === 'range') {
|
||||
// has range selections
|
||||
brush.clear([]);
|
||||
} else if (added[0] && added[0].key !== b.id) {
|
||||
// has selections in another dimension
|
||||
brush.clear([]);
|
||||
}
|
||||
});
|
||||
return added.filter(t => t.value !== -2); // do not allow selection on null value
|
||||
};
|
||||
|
||||
const rangeInterceptor = a => {
|
||||
const v = brush.brushes().filter(b => b.type === 'value');
|
||||
if (v.length) {
|
||||
// has dimension values selected
|
||||
brush.clear([]);
|
||||
return a;
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
brush.intercept('set-ranges', rangeInterceptor);
|
||||
brush.intercept('toggle-ranges', rangeInterceptor);
|
||||
|
||||
brush.intercept('toggle-values', valueInterceptor);
|
||||
brush.intercept('set-values', valueInterceptor);
|
||||
brush.intercept('add-values', valueInterceptor);
|
||||
|
||||
brush.on('start', () => selections.begin(path));
|
||||
|
||||
const selectionListeners = {
|
||||
activate: () => {
|
||||
// TODO - check if we can select in the current chart,
|
||||
},
|
||||
deactivated: () => brush.end(),
|
||||
cleared: () => brush.clear(),
|
||||
canceled: () => brush.end(),
|
||||
};
|
||||
addListeners(selections, selectionListeners);
|
||||
|
||||
brush.on('update', () => {
|
||||
const generated = picassoQ.selections(brush, {}, layout);
|
||||
generated.forEach(s => selections.select(s));
|
||||
});
|
||||
|
||||
if (instances.length === 0) {
|
||||
setup();
|
||||
}
|
||||
|
||||
instances.push({
|
||||
key,
|
||||
selections,
|
||||
});
|
||||
|
||||
return {
|
||||
layout: lt => {
|
||||
layout = lt;
|
||||
},
|
||||
release: () => {
|
||||
layout = null;
|
||||
const idx = instances.indexOf(instances.filter(i => i.key === key)[0]);
|
||||
if (idx !== -1) {
|
||||
instances.splice(idx, 1);
|
||||
}
|
||||
if (!instances.length) {
|
||||
teardown();
|
||||
}
|
||||
removeListeners(selections, selectionListeners);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
const serve = require('@nebula.js/cli-serve'); // eslint-disable-line
|
||||
|
||||
let s;
|
||||
|
||||
before(async () => {
|
||||
s = await serve({
|
||||
build: false,
|
||||
});
|
||||
|
||||
process.testServer = s;
|
||||
|
||||
page.on('pageerror', e => {
|
||||
console.log('Error:', e.message, e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
s.close();
|
||||
});
|
||||
13
commands/create/templates/picasso/minimal/src/data.js
Normal file
13
commands/create/templates/picasso/minimal/src/data.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
targets: [
|
||||
{
|
||||
path: 'qHyperCubeDef',
|
||||
dimensions: {
|
||||
min: 1,
|
||||
},
|
||||
measures: {
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
const properties = {
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [],
|
||||
qMeasures: [],
|
||||
qInitialDataFetch: [{ qWidth: 10, qHeight: 500 }],
|
||||
qSuppressZero: false,
|
||||
qSuppressMissing: true,
|
||||
},
|
||||
showTitles: true,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
footnote: '',
|
||||
};
|
||||
|
||||
export default properties;
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function({
|
||||
layout, // eslint-disable-line no-unused-vars
|
||||
context, // eslint-disable-line no-unused-vars
|
||||
}) {
|
||||
return {
|
||||
scales: {},
|
||||
components: [],
|
||||
};
|
||||
}
|
||||
21
commands/sense/README.md
Normal file
21
commands/sense/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# @nebula.js/cli-sense
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install @nebula.js/cli@next
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
nebula sense
|
||||
|
||||
Build a supernova as a Qlik Sense extension
|
||||
|
||||
Options:
|
||||
--version Show version number [boolean]
|
||||
--ext Extension definition [string]
|
||||
--meta Extension meta information [string]
|
||||
-h, --help Show help [boolean]
|
||||
```
|
||||
27
commands/sense/command.js
Normal file
27
commands/sense/command.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const build = require('./lib/build');
|
||||
|
||||
module.exports = {
|
||||
command: 'sense',
|
||||
desc: 'Build a supernova as a Qlik Sense extension',
|
||||
builder(yargs) {
|
||||
yargs.option('ext', {
|
||||
type: 'string',
|
||||
required: false,
|
||||
desc: 'Extension definition',
|
||||
});
|
||||
yargs.option('meta', {
|
||||
type: 'string',
|
||||
required: false,
|
||||
desc: 'Extension meta information',
|
||||
});
|
||||
yargs.option('minify', {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
default: true,
|
||||
desc: 'Minify and uglify code',
|
||||
});
|
||||
},
|
||||
handler(argv) {
|
||||
build(argv);
|
||||
},
|
||||
};
|
||||
101
commands/sense/lib/build.js
Normal file
101
commands/sense/lib/build.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const rollup = require('rollup');
|
||||
const nodeResolve = require('rollup-plugin-node-resolve');
|
||||
const common = require('rollup-plugin-commonjs');
|
||||
const babel = require('rollup-plugin-babel');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
|
||||
async function build(argv) {
|
||||
const cwd = process.cwd();
|
||||
const supernovaPkg = require(path.resolve(cwd, 'package.json')); // eslint-disable-line
|
||||
|
||||
const extName = supernovaPkg.name.replace(/\//, '-').replace('@', '');
|
||||
const targetDirectory = path.resolve(cwd, `${extName}-ext`);
|
||||
|
||||
let extDefinition = path.resolve(__dirname, '../src/ext-definition');
|
||||
|
||||
if (argv.ext) {
|
||||
extDefinition = path.resolve(argv.ext);
|
||||
}
|
||||
|
||||
const meta = argv.meta ? require(path.resolve(argv.meta)) : {}; // eslint-disable-line
|
||||
|
||||
const { module, main } = supernovaPkg;
|
||||
|
||||
const bundle = await rollup.rollup({
|
||||
input: {
|
||||
supernova: module || main,
|
||||
extDefinition,
|
||||
[extName]: path.resolve(__dirname, '../src/supernova-wrapper'),
|
||||
},
|
||||
external: ['snDefinition', 'extDefinition'],
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
common(),
|
||||
babel({
|
||||
babelrc: false,
|
||||
exclude: [/node_modules/],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
targets: {
|
||||
browsers: ['ie 11', 'chrome 47'],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
argv.minify &&
|
||||
terser({
|
||||
output: {
|
||||
comments: /@license|@preserve|Copyright|license/,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await bundle.write({
|
||||
dir: targetDirectory,
|
||||
format: 'amd',
|
||||
sourcemap: true,
|
||||
paths: {
|
||||
snDefinition: './supernova',
|
||||
extDefinition: './extDefinition',
|
||||
},
|
||||
chunkFileNames: '[name]-[hash]',
|
||||
});
|
||||
|
||||
// NOTE: chunkFileNames above must not contain '.js' at the end
|
||||
// since that would cause requirejs in sense-client to interpret the request as text/html
|
||||
// so we trim off the file extension in the modules, but then attach it to the files
|
||||
const files = fs.readdirSync(targetDirectory);
|
||||
files.forEach(f => {
|
||||
if (/^chunk-/.test(f) && !/\.js$/.test(f)) {
|
||||
// attach file extension
|
||||
fs.renameSync(path.resolve(targetDirectory, f), path.resolve(targetDirectory, `${f}.js`));
|
||||
}
|
||||
});
|
||||
|
||||
// write .qext for the extension
|
||||
fs.writeFileSync(
|
||||
path.resolve(targetDirectory, `${extName}.qext`),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: extName,
|
||||
description: supernovaPkg.description,
|
||||
author: supernovaPkg.author,
|
||||
version: supernovaPkg.version,
|
||||
...meta,
|
||||
type: 'visualization',
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = build;
|
||||
43
commands/sense/package.json
Normal file
43
commands/sense/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@nebula.js/cli-sense",
|
||||
"version": "0.1.0-alpha.18",
|
||||
"description": "Build a supernova as a Qlik Sense extension",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [
|
||||
"nebula",
|
||||
"sense",
|
||||
"qlik",
|
||||
"extension"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/qlik-oss/nebula.js.git"
|
||||
},
|
||||
"main": "lib/build.js",
|
||||
"files": [
|
||||
"command.js",
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.5.5",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@nebula.js/supernova": "0.1.0-alpha.18",
|
||||
"chalk": "^2.4.2",
|
||||
"node-event-emitter": "^0.0.1",
|
||||
"rollup": "^1.20.2",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-commonjs": "^10.0.2",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-terser": "^5.1.1",
|
||||
"yargs": "^14.0.0"
|
||||
}
|
||||
}
|
||||
101
commands/sense/src/__tests__/permissions.spec.js
Normal file
101
commands/sense/src/__tests__/permissions.spec.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import permissions from '../permissions';
|
||||
|
||||
const MODES = {
|
||||
static: {
|
||||
interactionState: 0,
|
||||
},
|
||||
analyis: {
|
||||
interactionState: 1,
|
||||
},
|
||||
edit: {
|
||||
interactionState: 2,
|
||||
},
|
||||
editstory: {
|
||||
interactionState: 1,
|
||||
// navigation: false,
|
||||
tooltips: false,
|
||||
selections: false,
|
||||
limitedInteraction: false,
|
||||
},
|
||||
playstory: {
|
||||
interactionState: 1,
|
||||
// navigation: false,
|
||||
tooltips: true,
|
||||
selections: false,
|
||||
limitedInteraction: true,
|
||||
},
|
||||
};
|
||||
|
||||
describe('permissions', () => {
|
||||
describe('with live engine', () => {
|
||||
it('in STATIC mode should only allow model SELECT and FETCH', () => {
|
||||
expect(
|
||||
permissions(
|
||||
{
|
||||
interactionState: 0,
|
||||
},
|
||||
{}
|
||||
)
|
||||
).to.eql(['select', 'fetch']);
|
||||
});
|
||||
|
||||
it('in ANALYSIS mode should allow everything', () => {
|
||||
expect(
|
||||
permissions(
|
||||
{
|
||||
interactionState: 1,
|
||||
},
|
||||
{}
|
||||
)
|
||||
).to.eql(['passive', 'interact', 'select', 'fetch']);
|
||||
});
|
||||
|
||||
it('in EDIT mode should only allow model SELECT and FETCH', () => {
|
||||
expect(
|
||||
permissions(
|
||||
{
|
||||
interactionState: 2,
|
||||
},
|
||||
{}
|
||||
)
|
||||
).to.eql(['select', 'fetch']);
|
||||
});
|
||||
|
||||
it('in ANALYSIS mode with tooltips and selections off, should only allow FETCH', () => {
|
||||
expect(
|
||||
permissions(
|
||||
{
|
||||
interactionState: 1,
|
||||
tooltips: false,
|
||||
selections: false,
|
||||
},
|
||||
{}
|
||||
)
|
||||
).to.eql(['fetch']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for a snapshot', () => {
|
||||
// interactive chart snapshot (shared chart)
|
||||
it('in ANALYSIS mode should only allow PASSIVE and INTERACT', () => {
|
||||
expect(
|
||||
permissions(
|
||||
{
|
||||
interactionState: 1,
|
||||
},
|
||||
{ isSnapshot: true }
|
||||
)
|
||||
).to.eql(['passive', 'interact']);
|
||||
});
|
||||
|
||||
// edit story
|
||||
it('in ANALYSIS mode with tooltips and selections off, should not allow anything', () => {
|
||||
expect(permissions(MODES.editstory, { isSnapshot: true })).to.eql([]);
|
||||
});
|
||||
|
||||
// play story
|
||||
it('in ANALYSIS mode with selections off, should ONLY allow PASSIVE', () => {
|
||||
expect(permissions(MODES.playstory, { isSnapshot: true })).to.eql(['passive']);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
commands/sense/src/ext-definition.js
Normal file
20
commands/sense/src/ext-definition.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
definition: {
|
||||
type: 'items',
|
||||
component: 'accordion',
|
||||
items: {
|
||||
data: {
|
||||
uses: 'data',
|
||||
},
|
||||
// settings: {
|
||||
// uses: 'settings',
|
||||
// },
|
||||
},
|
||||
},
|
||||
support: {
|
||||
export: false,
|
||||
exportData: false,
|
||||
snapshot: false,
|
||||
viewData: false,
|
||||
},
|
||||
};
|
||||
36
commands/sense/src/permissions.js
Normal file
36
commands/sense/src/permissions.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const INTERACTION_STATES = {
|
||||
STATIC: 0,
|
||||
ANALYSIS: 1,
|
||||
EDIT: 2,
|
||||
};
|
||||
|
||||
function showTooltip(options) {
|
||||
if (options.tooltips) {
|
||||
return true;
|
||||
}
|
||||
return options.interactionState === INTERACTION_STATES.ANALYSIS && options.tooltips !== false;
|
||||
}
|
||||
|
||||
function permissions(options, backendApi) {
|
||||
const p = [];
|
||||
if (showTooltip(options)) {
|
||||
p.push('passive');
|
||||
}
|
||||
if (
|
||||
options.interactionState === INTERACTION_STATES.ANALYSIS &&
|
||||
options.tooltips !== false &&
|
||||
options.limitedInteraction !== true
|
||||
) {
|
||||
p.push('interact');
|
||||
}
|
||||
if (!backendApi.isSnapshot) {
|
||||
if (options.selections !== false) {
|
||||
p.push('select');
|
||||
}
|
||||
p.push('fetch');
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
export default permissions;
|
||||
72
commands/sense/src/selections.js
Normal file
72
commands/sense/src/selections.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import EventEmitter from 'node-event-emitter';
|
||||
|
||||
export default scope => {
|
||||
/* eslint no-param-reassign: 0 */
|
||||
const mixin = obj => {
|
||||
Object.keys(EventEmitter.prototype).forEach(key => {
|
||||
obj[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
EventEmitter.init(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// TODO one and only one
|
||||
|
||||
const selectionAPI = {
|
||||
begin() {
|
||||
scope.selectionsApi.activated();
|
||||
scope.backendApi.beginSelections();
|
||||
selectionAPI.emit('activated');
|
||||
},
|
||||
clear() {
|
||||
scope.backendApi.model.resetMadeSelections();
|
||||
},
|
||||
confirm() {
|
||||
scope.selectionsApi.confirm();
|
||||
},
|
||||
cancel() {
|
||||
scope.selectionsApi.cancel();
|
||||
},
|
||||
select(s) {
|
||||
if (s.method !== 'resetMadeSelections' && !scope.selectionsApi.selectionsMade) {
|
||||
scope.backendApi.beginSelections();
|
||||
}
|
||||
scope.backendApi.model[s.method](...s.params).then(qSuccess => {
|
||||
if (!qSuccess) {
|
||||
scope.selectionsApi.selectionsMade = false;
|
||||
this.clear();
|
||||
}
|
||||
});
|
||||
scope.selectionsApi.selectionsMade = s.method !== 'resetMadeSelections';
|
||||
},
|
||||
isActive() {
|
||||
return scope.selectionsApi.active;
|
||||
},
|
||||
refreshToolbar() {
|
||||
scope.selectionsApi.refreshToolbar();
|
||||
},
|
||||
};
|
||||
|
||||
mixin(selectionAPI);
|
||||
|
||||
scope.selectionsApi.confirm = () => {
|
||||
scope.backendApi.endSelections(true).then(() => {
|
||||
scope.selectionsApi.deactivated();
|
||||
selectionAPI.emit('deactivated');
|
||||
});
|
||||
};
|
||||
|
||||
scope.selectionsApi.clear = () => {
|
||||
scope.backendApi.clearSelections();
|
||||
scope.selectionsApi.selectionsMade = false;
|
||||
selectionAPI.emit('cleared');
|
||||
};
|
||||
|
||||
scope.selectionsApi.cancel = () => {
|
||||
scope.backendApi.endSelections(false);
|
||||
scope.selectionsApi.deactivated();
|
||||
selectionAPI.emit('canceled');
|
||||
};
|
||||
|
||||
return selectionAPI;
|
||||
};
|
||||
71
commands/sense/src/supernova-wrapper.js
Normal file
71
commands/sense/src/supernova-wrapper.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import supernova from '@nebula.js/supernova';
|
||||
|
||||
import permissions from './permissions';
|
||||
import selectionsApi from './selections';
|
||||
|
||||
import snDefinition from 'snDefinition'; // eslint-disable-line
|
||||
import extDefinition from 'extDefinition'; // eslint-disable-line
|
||||
|
||||
const env = {
|
||||
Promise,
|
||||
};
|
||||
|
||||
const snGenerator = supernova(snDefinition, env);
|
||||
|
||||
const ext = typeof extDefinition === 'function' ? extDefinition(env) : extDefinition;
|
||||
|
||||
export default {
|
||||
// overridable properties
|
||||
definition: {
|
||||
type: 'items',
|
||||
component: 'accordion',
|
||||
items: {
|
||||
data: {
|
||||
uses: 'data',
|
||||
},
|
||||
},
|
||||
},
|
||||
support: {
|
||||
export: false,
|
||||
exportData: false,
|
||||
snapshot: false,
|
||||
viewData: false,
|
||||
},
|
||||
// override with user config
|
||||
...ext,
|
||||
|
||||
// =============================================
|
||||
// non-overridable properties
|
||||
initialProperties: snGenerator.qae.properties,
|
||||
importProperties: null, // Disable conversion to/from this object
|
||||
exportProperties: null, // Disable conversion to/from this object
|
||||
template: '<div style="height: 100%;position: relative"></div>',
|
||||
mounted($element) {
|
||||
const element = $element[0].children[0];
|
||||
const selectionAPI = selectionsApi(this.$scope);
|
||||
const sn = snGenerator.create({
|
||||
model: this.backendApi.model,
|
||||
app: this.backendApi.model.session.app,
|
||||
selections: selectionAPI,
|
||||
});
|
||||
this.snComponent = sn.component;
|
||||
this.snComponent.created({
|
||||
options: this.options || {},
|
||||
});
|
||||
this.snComponent.mounted(element);
|
||||
},
|
||||
paint($element, layout) {
|
||||
return this.snComponent.render({
|
||||
layout,
|
||||
context: {
|
||||
permissions: permissions(this.options, this.backendApi),
|
||||
},
|
||||
});
|
||||
},
|
||||
destroy() {
|
||||
if (this.snComponent) {
|
||||
this.snComponent.willUnmount();
|
||||
this.snComponent.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
53
commands/serve/README.md
Normal file
53
commands/serve/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# @nebula.js/cli-serve
|
||||
|
||||
Basic web development server for supernovas.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install @nebula.js/cli@next -g
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI
|
||||
|
||||
```sh
|
||||
nebula serve -h
|
||||
|
||||
Dev server
|
||||
|
||||
Options:
|
||||
--version Show version number [boolean]
|
||||
--entry File entrypoint [string]
|
||||
--build [boolean] [default: true]
|
||||
--host [string]
|
||||
--port [number]
|
||||
--enigma.host [string]
|
||||
--enigma.port [default: 9076]
|
||||
-h, --help Show help [boolean]
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
Start the server and connect to enigma on port `9077`
|
||||
```sh
|
||||
nebula serve --enigma.port 9077
|
||||
```
|
||||
|
||||
### node.js API
|
||||
|
||||
```js
|
||||
const serve = require('@nebula.js/cli-serve');
|
||||
|
||||
serve({
|
||||
port: 3000,
|
||||
entry: path.resolve(__dirname, 'sn.js') // custom entrypoint
|
||||
enigma: {
|
||||
port: 9077
|
||||
}
|
||||
}).then(s => {
|
||||
s.url; // serve url
|
||||
s.close(); // close the server
|
||||
});
|
||||
```
|
||||
32
commands/serve/command.js
Normal file
32
commands/serve/command.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const serve = require('./lib/serve');
|
||||
|
||||
module.exports = {
|
||||
command: 'serve',
|
||||
desc: 'Dev server',
|
||||
builder(yargs) {
|
||||
yargs.option('entry', {
|
||||
type: 'string',
|
||||
description: 'File entrypoint',
|
||||
});
|
||||
yargs.option('build', {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
});
|
||||
yargs.option('host', {
|
||||
type: 'string',
|
||||
});
|
||||
yargs.option('port', {
|
||||
type: 'number',
|
||||
});
|
||||
yargs.option('enigma.host', {
|
||||
type: 'string',
|
||||
});
|
||||
yargs.option('enigma.port', {
|
||||
type: 'port',
|
||||
default: 9076,
|
||||
}).argv;
|
||||
},
|
||||
handler(argv) {
|
||||
serve(argv);
|
||||
},
|
||||
};
|
||||
BIN
commands/serve/data/apps/ctrl00.qvf
Normal file
BIN
commands/serve/data/apps/ctrl00.qvf
Normal file
Binary file not shown.
11
commands/serve/docker-compose.yml
Normal file
11
commands/serve/docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: "3.1"
|
||||
|
||||
services:
|
||||
engine:
|
||||
image: qlikcore/engine:${ENGINE_VERSION:-12.368.0}
|
||||
# restart: always
|
||||
command: -S AcceptEULA=${ACCEPT_EULA:-no} -S DocumentDirectory=/apps
|
||||
ports:
|
||||
- ${ENGINE_PORT:-9076}:9076
|
||||
volumes:
|
||||
- ${APPS_PATH:-./data/apps}:/apps
|
||||
55
commands/serve/lib/engine.js
Normal file
55
commands/serve/lib/engine.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const path = require('path');
|
||||
const execa = require('execa');
|
||||
|
||||
/* eslint no-use-before-define:0 */
|
||||
const startEngine = () => {
|
||||
if (process.env.ACCEPT_EULA == null || process.env.ACCEPT_EULA.toLowerCase() !== 'yes') {
|
||||
throw new Error('Need to accept EULA in order to start engine container');
|
||||
}
|
||||
console.log('Starting engine container ...');
|
||||
return new Promise((resolve, reject) => {
|
||||
const c = execa.command('cross-env ACCEPT_EULA=yes docker-compose up -d --build', {
|
||||
cwd: path.resolve(__dirname, '../'),
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
const ping = setInterval(() => {
|
||||
const { stdout } = execa.command('docker ps -q -f name=engine -f status=running');
|
||||
if (stdout) {
|
||||
console.log('... engine container running');
|
||||
clear();
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
clear();
|
||||
reject();
|
||||
}, 60000);
|
||||
|
||||
function clear() {
|
||||
clearInterval(ping);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
c.on('exit', code => {
|
||||
if (code !== 0) {
|
||||
clear();
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const stopEngine = () => {
|
||||
execa.shellSync('docker-compose down', {
|
||||
cwd: path.resolve(__dirname, '../'),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
startEngine,
|
||||
stopEngine,
|
||||
};
|
||||
7
commands/serve/lib/placeholder.js
Normal file
7
commands/serve/lib/placeholder.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
component: {
|
||||
mounted() {
|
||||
console.log('mounted');
|
||||
},
|
||||
},
|
||||
};
|
||||
78
commands/serve/lib/serve.js
Normal file
78
commands/serve/lib/serve.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const portfinder = require('portfinder');
|
||||
const { watch } = require('@nebula.js/cli-build');
|
||||
|
||||
const webpackServe = require('./webpack.serve.js');
|
||||
|
||||
const { startEngine, stopEngine } = require('./engine');
|
||||
|
||||
module.exports = async argv => {
|
||||
if (process.env.ACCEPT_EULA === 'yes') {
|
||||
await startEngine();
|
||||
}
|
||||
const port = argv.port || (await portfinder.getPortPromise());
|
||||
const host = argv.host || 'localhost';
|
||||
const enigmaConfig = {
|
||||
port: 9076,
|
||||
host: 'localhost',
|
||||
...argv.enigma,
|
||||
};
|
||||
const context = process.cwd();
|
||||
let snPath;
|
||||
let snName;
|
||||
let watcher;
|
||||
if (argv.entry) {
|
||||
snPath = path.resolve(context, argv.entry);
|
||||
const parsed = path.parse(snPath);
|
||||
snName = parsed.name;
|
||||
} else {
|
||||
if (argv.build !== false) {
|
||||
watcher = await watch();
|
||||
}
|
||||
try {
|
||||
const externalPkg = require(path.resolve(context, 'package.json')); // eslint-disable-line global-require
|
||||
const externalEntry = externalPkg.module || externalPkg.main;
|
||||
snName = externalPkg.name;
|
||||
snPath = path.resolve(context, externalEntry);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(snPath)) {
|
||||
const rel = path.relative(context, snPath);
|
||||
console.log(chalk.red(`The specified entry point ${chalk.yellow(rel)} does not exist`));
|
||||
return;
|
||||
}
|
||||
|
||||
const server = await webpackServe({
|
||||
host,
|
||||
port,
|
||||
enigmaConfig,
|
||||
snName,
|
||||
snPath,
|
||||
dev: process.env.MONO === 'true',
|
||||
open: argv.open !== false,
|
||||
watcher,
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (process.env.ACCEPT_EULA === 'yes') {
|
||||
stopEngine();
|
||||
}
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
}
|
||||
server.close();
|
||||
};
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(signal => {
|
||||
process.on(signal, close);
|
||||
});
|
||||
|
||||
return { //eslint-disable-line
|
||||
url: server.url,
|
||||
close,
|
||||
};
|
||||
};
|
||||
15
commands/serve/lib/sn.js
Normal file
15
commands/serve/lib/sn.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import def from 'snDefinition'; // eslint-disable-line
|
||||
|
||||
window.snDefinition = def;
|
||||
|
||||
let cb = () => {};
|
||||
window.hotReload = fn => {
|
||||
cb = fn;
|
||||
};
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('snDefinition', () => {
|
||||
window.snDefinition = def;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
85
commands/serve/lib/webpack.build.js
Normal file
85
commands/serve/lib/webpack.build.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
const babelPath = require.resolve('babel-loader');
|
||||
const babelPresetEnvPath = require.resolve('@babel/preset-env');
|
||||
const babelPresetReactPath = require.resolve('@babel/preset-react');
|
||||
|
||||
const cfg = ({ srcDir, distDir, snPath, dev = false }) => {
|
||||
const config = {
|
||||
mode: dev ? 'development' : 'production',
|
||||
entry: {
|
||||
eRender: [path.resolve(srcDir, 'eRender')],
|
||||
eDev: [path.resolve(srcDir, 'eDev')],
|
||||
eHub: [path.resolve(srcDir, 'eHub')],
|
||||
},
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: distDir,
|
||||
filename: '[name].js',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
snDefinition: snPath,
|
||||
},
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
externals: dev ? {} : 'snDefinition',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
sideEffects: false,
|
||||
include: [srcDir, /nucleus/, /ui\/icons/],
|
||||
use: {
|
||||
loader: babelPath,
|
||||
options: {
|
||||
presets: [
|
||||
[
|
||||
babelPresetEnvPath,
|
||||
{
|
||||
modules: false,
|
||||
targets: {
|
||||
browsers: ['last 2 chrome versions'],
|
||||
},
|
||||
},
|
||||
],
|
||||
babelPresetReactPath,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(srcDir, 'eRender.html'),
|
||||
filename: 'eRender.html',
|
||||
chunks: ['eRender'],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(srcDir, 'eDev.html'),
|
||||
filename: 'eDev.html',
|
||||
chunks: ['eDev'],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(srcDir, 'eHub.html'),
|
||||
filename: 'eHub.html',
|
||||
chunks: ['eHub'],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
if (!process.env.DEFAULTS) {
|
||||
module.exports = cfg;
|
||||
} else {
|
||||
module.exports = cfg({
|
||||
srcDir: path.resolve(__dirname, '../web'),
|
||||
distDir: path.resolve(__dirname, '../dist'),
|
||||
snPath: path.resolve(__dirname, 'placeholder'),
|
||||
dev: false,
|
||||
});
|
||||
}
|
||||
48
commands/serve/lib/webpack.prod.js
Normal file
48
commands/serve/lib/webpack.prod.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const sourceMapLoaderPath = require.resolve('source-map-loader');
|
||||
|
||||
const cfg = ({ srcDir = path.resolve(__dirname, '../dist'), snPath = path.resolve(__dirname, 'placeholder') }) => {
|
||||
const config = {
|
||||
mode: 'development',
|
||||
entry: path.resolve(__dirname, './sn.js'),
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: path.resolve(srcDir, 'temp'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
snDefinition: snPath,
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
loader: sourceMapLoaderPath,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(srcDir, 'eRender.html'),
|
||||
filename: 'eRender.html',
|
||||
inject: 'head',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(srcDir, 'eDev.html'),
|
||||
filename: 'eDev.html',
|
||||
inject: 'head',
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
module.exports = cfg;
|
||||
128
commands/serve/lib/webpack.serve.js
Normal file
128
commands/serve/lib/webpack.serve.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint global-require: 0 */
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
|
||||
module.exports = async ({ host, port, enigmaConfig, snPath, snName, dev = false, open = true, watcher }) => {
|
||||
let config;
|
||||
let contentBase;
|
||||
|
||||
if (dev) {
|
||||
const webpackConfig = require('./webpack.build.js');
|
||||
const srcDir = path.resolve(__dirname, '../web');
|
||||
const distDir = path.resolve(srcDir, '../dist');
|
||||
contentBase = distDir;
|
||||
config = webpackConfig({
|
||||
srcDir,
|
||||
distDir,
|
||||
dev: true,
|
||||
snPath,
|
||||
});
|
||||
} else {
|
||||
const webpackConfig = require('./webpack.prod.js');
|
||||
const srcDir = path.resolve(__dirname, '../dist');
|
||||
contentBase = srcDir;
|
||||
config = webpackConfig({
|
||||
srcDir,
|
||||
snPath,
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
clientLogLevel: 'none',
|
||||
hot: true,
|
||||
host,
|
||||
port,
|
||||
overlay: {
|
||||
warnings: false,
|
||||
errors: true,
|
||||
},
|
||||
quiet: true,
|
||||
open,
|
||||
contentBase: [contentBase],
|
||||
historyApiFallback: {
|
||||
index: '/eHub.html',
|
||||
},
|
||||
before(app) {
|
||||
app.get('/info', (req, res) => {
|
||||
res.json({
|
||||
enigma: enigmaConfig,
|
||||
supernova: {
|
||||
name: snName,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
proxy: [
|
||||
{
|
||||
context: '/render',
|
||||
target: `http://${host}:${port}/eRender.html`,
|
||||
ignorePath: true,
|
||||
},
|
||||
{
|
||||
context: '/dev',
|
||||
target: `http://${host}:${port}/eDev.html`,
|
||||
ignorePath: true,
|
||||
},
|
||||
],
|
||||
watchOptions: {
|
||||
ignored: /node_modules/,
|
||||
},
|
||||
};
|
||||
|
||||
console.log('Starting development server...');
|
||||
|
||||
WebpackDevServer.addDevServerEntrypoints(config, options);
|
||||
const compiler = webpack(config);
|
||||
const server = new WebpackDevServer(compiler, options);
|
||||
|
||||
const close = () => {
|
||||
server.close();
|
||||
};
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(signal => {
|
||||
process.on(signal, close);
|
||||
});
|
||||
|
||||
if (watcher) {
|
||||
watcher.on('event', event => {
|
||||
if (event.code === 'ERROR') {
|
||||
server.sockWrite(server.sockets, 'errors', [event.error.stack]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let initiated = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-line consistent-return
|
||||
compiler.hooks.done.tap('nebula serve', stats => {
|
||||
if (!initiated) {
|
||||
initiated = true;
|
||||
const url = `http://${host}:${port}`;
|
||||
console.log(`...running at ${chalk.green(url)}`);
|
||||
|
||||
resolve({
|
||||
context: '',
|
||||
url,
|
||||
close,
|
||||
});
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
stats.compilation.errors.forEach(e => {
|
||||
console.log(chalk.red(e));
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, host, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
53
commands/serve/package.json
Normal file
53
commands/serve/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@nebula.js/cli-serve",
|
||||
"version": "0.1.0-alpha.18",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/qlik-oss/nebula.js.git"
|
||||
},
|
||||
"main": "lib/serve.js",
|
||||
"files": [
|
||||
"command.js",
|
||||
"data",
|
||||
"docker-compose.yml",
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production DEFAULTS=true webpack --config ./lib/webpack.build.js",
|
||||
"lint": "eslint web",
|
||||
"prepublishOnly": "rm -rf dist && yarn run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nebula.js/cli-build": "0.1.0-alpha.18",
|
||||
"chalk": "^2.4.2",
|
||||
"cross-env": "^5.2.0",
|
||||
"execa": "^2.0.4",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"portfinder": "^1.0.23",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"webpack": "^4.39.2",
|
||||
"webpack-dev-server": "^3.8.0",
|
||||
"yargs": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@material-ui/core": "^4.3.3",
|
||||
"@nebula.js/nucleus": "0.1.0-alpha.18",
|
||||
"@nebula.js/ui": "0.1.0-alpha.18",
|
||||
"babel-loader": "^8.0.6",
|
||||
"enigma.js": "^2.4.0",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"webpack-cli": "^3.3.7"
|
||||
}
|
||||
}
|
||||
23
commands/serve/web/.eslintrc.json
Normal file
23
commands/serve/web/.eslintrc.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"airbnb"
|
||||
],
|
||||
"rules": {
|
||||
"max-len": 0,
|
||||
"no-plusplus": 0,
|
||||
"no-bitwise" : 0,
|
||||
"no-unused-expressions": 0,
|
||||
"react/destructuring-assignment": [0, "always"],
|
||||
"react/prop-types": 0,
|
||||
"react/no-deprecated": 0,
|
||||
"import/no-extraneous-dependencies": [2, { "devDependencies": true }],
|
||||
"import/no-dynamic-require": 0
|
||||
}
|
||||
}
|
||||
209
commands/serve/web/components/App.jsx
Normal file
209
commands/serve/web/components/App.jsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import nucleus from '@nebula.js/nucleus';
|
||||
import snDefinition from 'snDefinition'; // eslint-disable-line
|
||||
|
||||
import {
|
||||
createTheme,
|
||||
ThemeProvider,
|
||||
} from '@nebula.js/ui/theme';
|
||||
|
||||
import {
|
||||
ChevronLeft,
|
||||
WbSunny,
|
||||
Brightness3,
|
||||
} from '@nebula.js/ui/icons';
|
||||
|
||||
import {
|
||||
Grid,
|
||||
Toolbar,
|
||||
Button,
|
||||
Divider,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import { observe } from '@nebula.js/nucleus/src/object/observer';
|
||||
|
||||
import Properties from './Properties';
|
||||
import Stage from './Stage';
|
||||
|
||||
import AppContext from '../contexts/AppContext';
|
||||
import NebulaContext from '../contexts/NebulaContext';
|
||||
|
||||
const storage = (() => {
|
||||
const stored = window.localStorage.getItem('nebula-dev');
|
||||
const parsed = stored ? JSON.parse(stored) : {};
|
||||
|
||||
const s = {
|
||||
save(name, value) {
|
||||
parsed[name] = value;
|
||||
window.localStorage.setItem('nebula-dev', JSON.stringify(parsed));
|
||||
},
|
||||
get(name) {
|
||||
return parsed[name];
|
||||
},
|
||||
props(name, v) {
|
||||
if (v) {
|
||||
s.save(`props:${name}`, JSON.stringify(v)); // TODO add app id to key to avoid using fields that don't exist
|
||||
return undefined;
|
||||
}
|
||||
const p = s.get(`props:${name}`);
|
||||
return p ? JSON.parse(p) : {};
|
||||
},
|
||||
};
|
||||
|
||||
return s;
|
||||
})();
|
||||
|
||||
export default function App({
|
||||
app,
|
||||
info,
|
||||
}) {
|
||||
const [viz, setViz] = useState(null);
|
||||
const [sn, setSupernova] = useState(null);
|
||||
const [isReadCacheEnabled, setReadCacheEnabled] = useState(storage.get('readFromCache') !== false);
|
||||
const [darkMode, setDarkMode] = useState(storage.get('darkMode') === true);
|
||||
const currentSelectionsRef = useRef(null);
|
||||
const uid = useRef();
|
||||
|
||||
const themeName = darkMode ? 'dark' : 'light';
|
||||
|
||||
const theme = useMemo(() => createTheme(themeName), [themeName]);
|
||||
|
||||
const nebbie = useMemo(() => {
|
||||
const n = nucleus(app, {
|
||||
load: (type, config) => config.Promise.resolve(window.snDefinition || snDefinition),
|
||||
theme: themeName,
|
||||
});
|
||||
return n;
|
||||
}, [app]);
|
||||
|
||||
|
||||
useLayoutEffect(() => {
|
||||
nebbie.theme(themeName);
|
||||
}, [nebbie, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
let propertyObserver = () => {};
|
||||
|
||||
const create = () => {
|
||||
uid.current = String(Date.now());
|
||||
nebbie.create({
|
||||
type: info.supernova.name,
|
||||
}, {
|
||||
context: {
|
||||
permissions: ['passive', 'interact', 'select', 'fetch'],
|
||||
},
|
||||
properties: {
|
||||
...(storage.get('readFromCache') !== false ? storage.props(info.supernova.name) : {}),
|
||||
qInfo: {
|
||||
qId: uid.current,
|
||||
qType: info.supernova.name,
|
||||
},
|
||||
},
|
||||
}).then((v) => {
|
||||
setViz(v);
|
||||
propertyObserver = observe(v.model, (p) => {
|
||||
storage.props(info.supernova.name, p);
|
||||
}, 'properties');
|
||||
});
|
||||
};
|
||||
|
||||
nebbie.types.get({
|
||||
name: info.supernova.name,
|
||||
}).supernova().then(setSupernova);
|
||||
nebbie.selections().mount(currentSelectionsRef.current);
|
||||
if (window.hotReload) {
|
||||
window.hotReload(() => {
|
||||
propertyObserver();
|
||||
nebbie.types.clearFromCache(info.supernova.name);
|
||||
app.destroySessionObject(uid.current).then(create);
|
||||
});
|
||||
}
|
||||
|
||||
create();
|
||||
|
||||
const unload = () => {
|
||||
app.destroySessionObject(uid.current);
|
||||
};
|
||||
window.addEventListener('beforeunload', unload);
|
||||
return () => {
|
||||
propertyObserver();
|
||||
window.removeEventListener('beforeunload', unload);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCacheChange = (e) => {
|
||||
storage.save('readFromCache', e.target.checked);
|
||||
setReadCacheEnabled(e.target.checked);
|
||||
};
|
||||
|
||||
const handleThemeChange = (e) => {
|
||||
storage.save('darkMode', e.target.checked);
|
||||
setDarkMode(e.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={app}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<NebulaContext.Provider value={nebbie}>
|
||||
<Grid container wrap="nowrap" direction="column" style={{ background: theme.palette.background.darkest }}>
|
||||
<Grid item>
|
||||
<Toolbar variant="dense" style={{ background: theme.palette.background.paper }}>
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<Button variant="outlined" href={window.location.origin}>
|
||||
<ChevronLeft style={{ marginLeft: -theme.spacing(1 * 1.5) }} />
|
||||
Hub
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs />
|
||||
<Grid item>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch checked={isReadCacheEnabled} onChange={handleCacheChange} value="isReadFromCacheEnabled" />
|
||||
}
|
||||
label="Cache"
|
||||
/>
|
||||
<FormControlLabel
|
||||
label=""
|
||||
control={(
|
||||
<>
|
||||
<WbSunny fontSize="small" style={{ color: theme.palette.text.secondary, marginLeft: theme.spacing(2) }} />
|
||||
<Switch checked={darkMode} onChange={handleThemeChange} value="darkMode" />
|
||||
<Brightness3 fontSize="small" style={{ color: theme.palette.text.secondary }} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<div ref={currentSelectionsRef} style={{ flex: '0 0 auto' }} />
|
||||
<Divider />
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Grid container wrap="nowrap" style={{ height: '100%' }}>
|
||||
<Grid item xs style={{ overflow: 'hidden' }}>
|
||||
<Stage viz={viz} />
|
||||
</Grid>
|
||||
<Grid item style={{ background: theme.palette.background.paper }}>
|
||||
<Properties sn={sn} viz={viz} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</NebulaContext.Provider>
|
||||
</ThemeProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
29
commands/serve/web/components/Cell.jsx
Normal file
29
commands/serve/web/components/Cell.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Grid,
|
||||
Card,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import Chart from './Chart';
|
||||
|
||||
export default function ({
|
||||
object,
|
||||
onSelected,
|
||||
}) {
|
||||
return (
|
||||
<Card style={{ minHeight: 400, position: 'relative', overflow: 'visible' }}>
|
||||
<Grid
|
||||
container
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Grid item xs={12}>
|
||||
<Chart id={object.qInfo.qId} onSelected={onSelected} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
44
commands/serve/web/components/Chart.jsx
Normal file
44
commands/serve/web/components/Chart.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useContext,
|
||||
useRef,
|
||||
// useState,
|
||||
} from 'react';
|
||||
|
||||
import NebulaContext from '../contexts/NebulaContext';
|
||||
|
||||
export default function Chart({
|
||||
id,
|
||||
// onSelected,
|
||||
}) {
|
||||
const nebbie = useContext(NebulaContext);
|
||||
const el = useRef();
|
||||
// const [viz, setViz] = useState(null);
|
||||
useEffect(() => {
|
||||
const n = nebbie.get({
|
||||
id,
|
||||
}, {
|
||||
context: {
|
||||
permissions: ['passive', 'interact', 'select', 'fetch'],
|
||||
},
|
||||
element: el.current,
|
||||
});
|
||||
// n.then(setViz);
|
||||
return () => {
|
||||
n.then((v) => {
|
||||
v.close();
|
||||
// v.unmount();
|
||||
});
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={el}
|
||||
// onClick={() => onSelected(viz)}
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
45
commands/serve/web/components/Collection.jsx
Normal file
45
commands/serve/web/components/Collection.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import useLayout from '@nebula.js/nucleus/src/hooks/useLayout';
|
||||
|
||||
import {
|
||||
Grid,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import AppContext from '../contexts/AppContext';
|
||||
import Cell from './Cell';
|
||||
|
||||
export default function Collection({
|
||||
type,
|
||||
onSelectedCell,
|
||||
}) {
|
||||
const app = useContext(AppContext);
|
||||
const [layout] = useLayout(app);
|
||||
const [objects, setObjects] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
app.getObjects({
|
||||
qTypes: [type],
|
||||
qIncludeSessionObjects: true,
|
||||
qData: {
|
||||
title: '/qMetaDef/title',
|
||||
},
|
||||
}).then((list) => {
|
||||
setObjects(list);
|
||||
});
|
||||
}, [layout, type]);
|
||||
|
||||
return (
|
||||
<Grid container spacing={2} style={{ padding: '12px' }}>
|
||||
{objects.map((c) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={c.qInfo.qId}>
|
||||
<Cell object={c} onSelected={onSelectedCell} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
165
commands/serve/web/components/FieldsPopover.jsx
Normal file
165
commands/serve/web/components/FieldsPopover.jsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Popover,
|
||||
List,
|
||||
ListSubheader,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Divider,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import {
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
} from '@nebula.js/ui/icons';
|
||||
|
||||
import {
|
||||
useTheme,
|
||||
} from '@nebula.js/ui/theme';
|
||||
|
||||
import useModel from '@nebula.js/nucleus/src/hooks/useModel';
|
||||
import useLayout from '@nebula.js/nucleus/src/hooks/useLayout';
|
||||
import useLibraryList from '../hooks/useLibraryList';
|
||||
|
||||
import AppContext from '../contexts/AppContext';
|
||||
|
||||
const Field = ({ field, onSelect, sub }) => (
|
||||
<ListItem button onClick={() => onSelect(field.qName)} data-key={field.qName}>
|
||||
<ListItemText>{field.qName}</ListItemText>
|
||||
{sub && <ChevronRight fontSize="small" />}
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
const LibraryItem = ({ item, onSelect }) => (
|
||||
<ListItem button onClick={() => onSelect(item.qInfo)} data-key={item.qInfo.qId}>
|
||||
<ListItemText>{item.qData.title}</ListItemText>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
const Aggr = ({ aggr, field, onSelect }) => (
|
||||
<ListItem button onClick={() => onSelect(aggr)} data-key={aggr}>
|
||||
<ListItemText>{`${aggr}(${field})`}</ListItemText>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
const LibraryList = ({
|
||||
app,
|
||||
onSelect,
|
||||
title = '',
|
||||
type = 'dimension',
|
||||
}) => {
|
||||
const [libraryItems] = useLibraryList(app, type);
|
||||
const sortedLibraryItems = useMemo(() => libraryItems
|
||||
.slice()
|
||||
.sort((a, b) => a.qData.title.toLowerCase().localeCompare(b.qData.title.toLowerCase())),
|
||||
[libraryItems]);
|
||||
|
||||
return libraryItems.length > 0 ? (
|
||||
<>
|
||||
<ListSubheader component="div" style={{ backgroundColor: 'inherit' }}>{title}</ListSubheader>
|
||||
{sortedLibraryItems.map((item) => <LibraryItem key={item.qInfo.qId} item={item} onSelect={onSelect} />)}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default function FieldsPopover({
|
||||
alignTo,
|
||||
show,
|
||||
close,
|
||||
onSelected,
|
||||
type,
|
||||
}) {
|
||||
const app = useContext(AppContext);
|
||||
const [selectedField, setSelectedField] = useState(null);
|
||||
const theme = useTheme();
|
||||
const [model] = useModel({
|
||||
qInfo: {
|
||||
qType: 'FieldList',
|
||||
qId: 'FieldList',
|
||||
},
|
||||
qFieldListDef: {
|
||||
qShowDerivedFelds: false,
|
||||
qShowHidden: false,
|
||||
qShowSemantic: true,
|
||||
qShowSrcTables: true,
|
||||
qShowSystem: false,
|
||||
},
|
||||
}, app);
|
||||
|
||||
const [layout] = useLayout(model, app);
|
||||
|
||||
const fields = useMemo(() => (layout ? (layout.qFieldList.qItems || []) : [])
|
||||
.slice()
|
||||
.sort((a, b) => a.qName.toLowerCase().localeCompare(b.qName.toLowerCase())),
|
||||
[layout]);
|
||||
|
||||
const onSelect = (s) => {
|
||||
if (s && s.qId) {
|
||||
onSelected(s);
|
||||
close();
|
||||
} else if (type === 'measure') {
|
||||
setSelectedField(s);
|
||||
} else {
|
||||
onSelected({
|
||||
field: s,
|
||||
});
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const onAggregateSelected = (s) => {
|
||||
onSelected({
|
||||
field: selectedField,
|
||||
aggregation: s,
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={show}
|
||||
onClose={close}
|
||||
anchorEl={alignTo.current}
|
||||
marginThreshold={theme.spacing(1)}
|
||||
elevation={3}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
PaperProps={{
|
||||
style: { minWidth: '250px', maxHeight: '300px', background: theme.palette.background.lightest },
|
||||
}}
|
||||
>
|
||||
{selectedField && (
|
||||
<List dense component="nav">
|
||||
<ListItem button onClick={() => setSelectedField(null)}>
|
||||
<ListItemIcon>
|
||||
<ChevronLeft />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Back</ListItemText>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListSubheader component="div">Aggregation</ListSubheader>
|
||||
{['sum', 'count', 'avg', 'min', 'max'].map((v) => <Aggr key={v} aggr={v} field={selectedField} onSelect={onAggregateSelected} />)}
|
||||
</List>
|
||||
)}
|
||||
{!selectedField && fields.length > 0 && (
|
||||
<List dense component="nav" style={{ background: theme.palette.background.lightest }}>
|
||||
<LibraryList app={app} onSelect={onSelect} type={type} title={type === 'measure' ? 'Measures' : 'Dimensions'} />
|
||||
<ListSubheader component="div" style={{ backgroundColor: 'inherit' }}>Fields</ListSubheader>
|
||||
{fields.map((field) => <Field key={field.qName} field={field} onSelect={onSelect} sub={type === 'measure'} />)}
|
||||
</List>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
42
commands/serve/web/components/Properties.jsx
Normal file
42
commands/serve/web/components/Properties.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Typography,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import useProperties from '@nebula.js/nucleus/src/hooks/useProperties';
|
||||
|
||||
import Data from './property-panel/Data';
|
||||
|
||||
export default function Properties({
|
||||
viz,
|
||||
sn,
|
||||
}) {
|
||||
const [properties] = useProperties(viz ? viz.model : null);
|
||||
|
||||
if (!sn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!viz || !properties) {
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: '250px',
|
||||
padding: '8px',
|
||||
}}
|
||||
>
|
||||
<Typography>Nothing selected</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: '250px',
|
||||
padding: '8px',
|
||||
}}
|
||||
>
|
||||
<Data properties={properties} model={viz.model} sn={sn} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
commands/serve/web/components/PropertiesDialog.jsx
Normal file
87
commands/serve/web/components/PropertiesDialog.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
export default function PropertiesDialog({
|
||||
model,
|
||||
show,
|
||||
close,
|
||||
}) {
|
||||
const text = useRef(null);
|
||||
const [objectProps, setObjectProps] = useState('');
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
if (text.current) {
|
||||
model.setProperties(JSON.parse(objectProps)).then(close);
|
||||
}
|
||||
});
|
||||
|
||||
const onChange = (e) => {
|
||||
setObjectProps(e.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!show) {
|
||||
return undefined;
|
||||
}
|
||||
const onChanged = () => {
|
||||
model && show && model.getProperties().then((props) => {
|
||||
show && setObjectProps(JSON.stringify(props || {}, null, 2));
|
||||
});
|
||||
};
|
||||
|
||||
model.on('changed', onChanged);
|
||||
onChanged();
|
||||
return () => {
|
||||
model && model.removeListener('changed', onChanged);
|
||||
};
|
||||
}, [model && model.id, show]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={show}
|
||||
scroll="paper"
|
||||
maxWidth="md"
|
||||
onClose={close}
|
||||
PaperProps={{
|
||||
style: {
|
||||
width: '80%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
Modify object properties
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
|
||||
<TextField
|
||||
value={objectProps}
|
||||
onChange={onChange}
|
||||
ref={text}
|
||||
multiline
|
||||
row="40"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
style: { fontFamily: 'Monaco, monospace', fontSize: '0.8rem' },
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={close}>Cancel</Button>
|
||||
<Button variant="outlined" onClick={onConfirm}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
56
commands/serve/web/components/Stage.jsx
Normal file
56
commands/serve/web/components/Stage.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
Toolbar,
|
||||
Divider,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import PropsDialog from './PropertiesDialog';
|
||||
|
||||
export default function Stage({
|
||||
viz,
|
||||
}) {
|
||||
const c = useRef(null);
|
||||
const model = viz && viz.model;
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
viz && viz.mount(c.current);
|
||||
}, [viz]);
|
||||
|
||||
const closeDialog = useCallback(() => { setDialogOpen(false); }, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', height: '100%', boxSizing: 'border-box' }}>
|
||||
<Card style={{ height: '100%' }}>
|
||||
<Grid container direction="column" style={{ height: '100%' }}>
|
||||
<Grid item>
|
||||
<Toolbar>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={!model}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
Props
|
||||
</Button>
|
||||
<PropsDialog model={model} show={dialogOpen} close={closeDialog} />
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<div ref={c} style={{ position: 'relative', height: '100%' }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
commands/serve/web/components/property-panel/Data.jsx
Normal file
37
commands/serve/web/components/property-panel/Data.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Typography,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import HyperCube from './HyperCube';
|
||||
|
||||
export default function Data({
|
||||
model,
|
||||
sn,
|
||||
properties,
|
||||
}) {
|
||||
if (!sn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { targets } = sn.qae.data;
|
||||
|
||||
if (!targets.length) {
|
||||
return (
|
||||
<Typography>No data targets found</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
{targets.map((t) => (
|
||||
<ListItem key={t.path} divider disableGutters>
|
||||
<HyperCube target={t} properties={properties} model={model} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
111
commands/serve/web/components/property-panel/Fields.jsx
Normal file
111
commands/serve/web/components/property-panel/Fields.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, {
|
||||
useRef,
|
||||
useState,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
IconButton,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
// Divider,
|
||||
Typography,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import Remove from '@nebula.js/ui/icons/Remove';
|
||||
|
||||
import useLibraryList from '../../hooks/useLibraryList';
|
||||
|
||||
import AppContext from '../../contexts/AppContext';
|
||||
import FieldsPopover from '../FieldsPopover';
|
||||
|
||||
const FieldTitle = ({
|
||||
field,
|
||||
libraryItems,
|
||||
}) => {
|
||||
if (field.qLibraryId) {
|
||||
const f = libraryItems.filter((ff) => ff.qInfo.qId === field.qLibraryId)[0];
|
||||
return f ? f.qData.title : '!!!';
|
||||
}
|
||||
if (field.qDef && field.qDef.qFieldDefs) {
|
||||
return field.qDef.qFieldDefs[0];
|
||||
}
|
||||
if (field.qDef && field.qDef.qDef) {
|
||||
return field.qDef.qDef;
|
||||
}
|
||||
return '???';
|
||||
};
|
||||
|
||||
export default function Fields({
|
||||
items,
|
||||
type = 'dimension',
|
||||
label = '',
|
||||
addLabel = 'Add',
|
||||
onAdded,
|
||||
onRemoved,
|
||||
canAdd = true,
|
||||
}) {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const btn = useRef(null);
|
||||
const app = useContext(AppContext);
|
||||
const [libraryItems] = useLibraryList(app, type);
|
||||
|
||||
const onAdd = () => {
|
||||
setIsActive(!isActive);
|
||||
};
|
||||
|
||||
const onSelected = (o) => {
|
||||
if (o.qId) {
|
||||
onAdded(o);
|
||||
} else if (o) {
|
||||
if (type === 'dimension') {
|
||||
onAdded(o.field);
|
||||
} else {
|
||||
onAdded(`${o.aggregation || 'sum'}([${o.field}])`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onRemove = (idx) => {
|
||||
onRemoved(idx);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="overline">{label}</Typography>
|
||||
<List dense>
|
||||
{items.map((d, i) => (
|
||||
<ListItem disableGutters key={d.qDef.cId}>
|
||||
<ListItemText>
|
||||
<FieldTitle field={d} libraryItems={libraryItems} type={type} />
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" onClick={() => onRemove(i)}><Remove /></IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={onAdd}
|
||||
ref={btn}
|
||||
disabled={!canAdd}
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
{isActive && (
|
||||
<FieldsPopover
|
||||
alignTo={btn}
|
||||
show={isActive}
|
||||
close={() => setIsActive(false)}
|
||||
onSelected={onSelected}
|
||||
type={type}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
commands/serve/web/components/property-panel/HyperCube.jsx
Normal file
86
commands/serve/web/components/property-panel/HyperCube.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, {
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import hcHandler from '@nebula.js/nucleus/src/object/hc-handler';
|
||||
|
||||
import {
|
||||
Typography,
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import Fields from './Fields';
|
||||
|
||||
const getValue = (data, reference, defaultValue) => {
|
||||
const steps = reference.split('/');
|
||||
let dataContainer = data;
|
||||
if (dataContainer === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
for (let i = 0; i < steps.length; ++i) {
|
||||
if (steps[i] === '') {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
if (typeof dataContainer[steps[i]] === 'undefined') {
|
||||
return defaultValue;
|
||||
}
|
||||
dataContainer = dataContainer[steps[i]];
|
||||
}
|
||||
|
||||
return dataContainer;
|
||||
};
|
||||
|
||||
|
||||
export default function HyperCube({
|
||||
model,
|
||||
target,
|
||||
properties,
|
||||
}) {
|
||||
const handler = useMemo(() => hcHandler({
|
||||
def: target,
|
||||
hc: getValue(properties, target.path),
|
||||
}), [properties]);
|
||||
|
||||
const onDimensionAdded = (a) => {
|
||||
handler.addDimension(typeof a === 'object' ? { qLibraryId: a.qId } : a);
|
||||
model.setProperties(properties);
|
||||
};
|
||||
|
||||
const onDimensionRemoved = (idx) => {
|
||||
handler.removeDimension(idx);
|
||||
model.setProperties(properties);
|
||||
};
|
||||
|
||||
const onMeasureAdded = (a) => {
|
||||
handler.addMeasure(typeof a === 'object' ? { qLibraryId: a.qId } : a);
|
||||
model.setProperties(properties);
|
||||
};
|
||||
|
||||
const onMeasureRemoved = (idx) => {
|
||||
handler.removeMeasure(idx);
|
||||
model.setProperties(properties);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<Typography color="textSecondary" style={{ fontFamily: 'Monaco, monospace', fontSize: '0.7rem' }}>{target.path}</Typography>
|
||||
<Fields
|
||||
onAdded={onDimensionAdded}
|
||||
onRemoved={onDimensionRemoved}
|
||||
canAdd={handler.canAddDimension()}
|
||||
items={handler.dimensions()}
|
||||
type="dimension"
|
||||
label="Dimensions"
|
||||
addLabel="Add dimension"
|
||||
/>
|
||||
<Fields
|
||||
onAdded={onMeasureAdded}
|
||||
onRemoved={onMeasureRemoved}
|
||||
canAdd={handler.canAddMeasure()}
|
||||
items={handler.measures()}
|
||||
type="measure"
|
||||
label="Measures"
|
||||
addLabel="Add measures"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
commands/serve/web/connect.js
Normal file
62
commands/serve/web/connect.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import enigma from 'enigma.js';
|
||||
import qixSchema from 'enigma.js/schemas/12.34.11.json';
|
||||
import SenseUtilities from 'enigma.js/sense-utilities';
|
||||
|
||||
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('&').forEach((pair) => {
|
||||
const idx = pair.indexOf('=');
|
||||
const name = pair.substr(0, idx);
|
||||
let value = decodeURIComponent(pair.substring(idx + 1));
|
||||
if (name === 'cols') {
|
||||
value = value.split(',');
|
||||
}
|
||||
opts[name] = value;
|
||||
});
|
||||
|
||||
return opts;
|
||||
})();
|
||||
|
||||
const requestInfo = fetch('/info').then((response) => response.json());
|
||||
|
||||
const defaultConfig = {
|
||||
host: window.location.hostname || 'localhost',
|
||||
port: 9076,
|
||||
secure: false,
|
||||
};
|
||||
|
||||
let connection;
|
||||
const connect = () => {
|
||||
if (!connection) {
|
||||
connection = requestInfo.then((info) => enigma.create({
|
||||
schema: qixSchema,
|
||||
url: SenseUtilities.buildUrl({
|
||||
...defaultConfig,
|
||||
...info.enigma,
|
||||
}),
|
||||
}).open());
|
||||
}
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
const openApp = (id) => requestInfo.then((info) => enigma.create({
|
||||
schema: qixSchema,
|
||||
url: SenseUtilities.buildUrl({
|
||||
...defaultConfig,
|
||||
...info.enigma,
|
||||
appId: id,
|
||||
}),
|
||||
}).open().then((global) => global.openDoc(id)));
|
||||
|
||||
export {
|
||||
connect,
|
||||
openApp,
|
||||
params,
|
||||
requestInfo as info,
|
||||
};
|
||||
5
commands/serve/web/contexts/AppContext.js
Normal file
5
commands/serve/web/contexts/AppContext.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const AppContext = React.createContext();
|
||||
|
||||
export default AppContext;
|
||||
5
commands/serve/web/contexts/NebulaContext.js
Normal file
5
commands/serve/web/contexts/NebulaContext.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const NebulaContext = React.createContext();
|
||||
|
||||
export default NebulaContext;
|
||||
35
commands/serve/web/eDev.html
Normal file
35
commands/serve/web/eDev.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base href="/">
|
||||
<title>Nebula dev</title>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:light" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:semibold" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #ddd;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#app, #app > * {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
18
commands/serve/web/eDev.jsx
Normal file
18
commands/serve/web/eDev.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
import {
|
||||
openApp,
|
||||
params,
|
||||
info,
|
||||
} from './connect';
|
||||
|
||||
if (!params.app) {
|
||||
location.href = location.origin; //eslint-disable-line
|
||||
}
|
||||
|
||||
info.then(($) => openApp(params.app).then((app) => {
|
||||
ReactDOM.render(<App app={app} info={$} />, document.querySelector('#app'));
|
||||
}));
|
||||
73
commands/serve/web/eHub.html
Normal file
73
commands/serve/web/eHub.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nebula hub</title>
|
||||
|
||||
<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>
|
||||
</html>
|
||||
14
commands/serve/web/eHub.js
Normal file
14
commands/serve/web/eHub.js
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
39
commands/serve/web/eRender.html
Normal file
39
commands/serve/web/eRender.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base href="/">
|
||||
<title>Nebula render</title>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:light" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:semibold" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(110deg, #92498F 0%, #45B3B2 100%);
|
||||
font: normal 14px/16px "Source Sans Pro", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#chart-container {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
top: 64px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
64
commands/serve/web/eRender.js
Normal file
64
commands/serve/web/eRender.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import nucleus from '@nebula.js/nucleus';
|
||||
import snDefinition from 'snDefinition'; // eslint-disable-line
|
||||
|
||||
import {
|
||||
openApp,
|
||||
params,
|
||||
info as serverInfo,
|
||||
} from './connect';
|
||||
|
||||
if (!params.app) {
|
||||
location.href = location.origin; //eslint-disable-line
|
||||
}
|
||||
|
||||
serverInfo.then((info) => openApp(params.app).then((app) => {
|
||||
let obj;
|
||||
let objType;
|
||||
|
||||
const nebbie = nucleus(app, {
|
||||
load: (type, config) => {
|
||||
objType = type.name;
|
||||
return config.Promise.resolve(snDefinition);
|
||||
},
|
||||
});
|
||||
|
||||
const create = () => {
|
||||
obj = nebbie.create({
|
||||
type: info.supernova.name,
|
||||
fields: params.cols || [],
|
||||
}, {
|
||||
element: document.querySelector('#chart-container'),
|
||||
context: {
|
||||
permissions: params.permissions || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const get = () => {
|
||||
obj = nebbie.get({
|
||||
id: params.object,
|
||||
}, {
|
||||
element: document.querySelector('#chart-container'),
|
||||
});
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (params.object) {
|
||||
get();
|
||||
} else {
|
||||
create();
|
||||
}
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('snDefinition', () => {
|
||||
nebbie.types.clearFromCache(objType);
|
||||
obj.then((viz) => {
|
||||
viz.close();
|
||||
render();
|
||||
});
|
||||
});
|
||||
}
|
||||
}));
|
||||
38
commands/serve/web/hooks/useLibraryList.js
Normal file
38
commands/serve/web/hooks/useLibraryList.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import useModel from '@nebula.js/nucleus/src/hooks/useModel';
|
||||
import useLayout from '@nebula.js/nucleus/src/hooks/useLayout';
|
||||
|
||||
const D = {
|
||||
qInfo: {
|
||||
qType: 'DimensionList',
|
||||
qId: 'DimensionList',
|
||||
},
|
||||
qDimensionListDef: {
|
||||
qType: 'dimension',
|
||||
qData: {
|
||||
labelExpression: '/qDimension/qLabelExpression',
|
||||
title: '/qMetaDef/title',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const M = {
|
||||
qInfo: {
|
||||
qType: 'MeasureList',
|
||||
qId: 'MeasureList',
|
||||
},
|
||||
qMeasureListDef: {
|
||||
qType: 'measure',
|
||||
qData: {
|
||||
labelExpression: '/qMeasure/qLabelExpression',
|
||||
title: '/qMetaDef/title',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function list(app, type = 'dimension') {
|
||||
const def = type === 'dimension' ? D : M;
|
||||
|
||||
const [model] = useModel(def, app);
|
||||
const [layout] = useLayout(model, app);
|
||||
return [layout ? ((layout.qDimensionList || layout.qMeasureList).qItems || []) : []];
|
||||
}
|
||||
Reference in New Issue
Block a user