mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-25 01:04:14 -05:00
fix(cli-sense): assume supernova wrapping is done in Sense (#358)
This commit is contained in:
@@ -36,6 +36,16 @@
|
||||
"global-require": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["commands/sense/src/*.js"],
|
||||
"rules": {
|
||||
"no-var": 0,
|
||||
"import/no-amd": 0
|
||||
},
|
||||
"globals": {
|
||||
"define": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.{int,comp,spec}.{js,jsx}"],
|
||||
"env": {
|
||||
|
||||
@@ -14,9 +14,61 @@ 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]
|
||||
--output Output directory of extension
|
||||
-h, --help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
--ext Extension definition [string]
|
||||
--meta Extension meta information [string]
|
||||
--minify Minify and uglify code [boolean] [default: true]
|
||||
--sourcemap Generate sourcemaps [boolean] [default: false]
|
||||
-h, --help Show help [boolean]
|
||||
```
|
||||
|
||||
### Extension
|
||||
|
||||
You can provide some additional information as part of the Qlik Sense Extension API by creating a separate file for the extension info and providing it as argument to `--ext`:
|
||||
|
||||
```js
|
||||
// def.js
|
||||
export default {
|
||||
definition: {
|
||||
// Property panel definition
|
||||
},
|
||||
support: {
|
||||
exportData: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```bash
|
||||
nebula sense --ext def.js
|
||||
```
|
||||
|
||||
The provided file will be transpiled and placed in the folder `/dist-ext`. Two additional files will be created which are the entrypoints for the extension in Qlik Sense; If your supernova module is named `banana-chart`, the files `banana-chart.js` and `banana-chart.qext` will be created in the root of your project. If you have a `files` property in your `package.json` you should include these files in addition to the already existing ones:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "banana-chart",
|
||||
"files": [
|
||||
"// other files",
|
||||
"dist-ext",
|
||||
"banana-chart.js"
|
||||
"banana-chart.qext"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Meta
|
||||
|
||||
You can add more meta about the extension by providing a `.json` formatted file with `--meta`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My tasty banana extension",
|
||||
"icon": "barchart"
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
nebula sense --meta meta.json
|
||||
```
|
||||
|
||||
The rest of the required information will be populated automatically.
|
||||
|
||||
@@ -20,10 +20,11 @@ module.exports = {
|
||||
default: true,
|
||||
desc: 'Minify and uglify code',
|
||||
});
|
||||
yargs.option('output', {
|
||||
type: 'string',
|
||||
yargs.option('sourcemap', {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
desc: 'Specify the output location',
|
||||
default: false,
|
||||
desc: 'Generate sourcemaps',
|
||||
});
|
||||
},
|
||||
handler(argv) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const rollup = require('rollup');
|
||||
const nodeResolve = require('@rollup/plugin-node-resolve');
|
||||
@@ -9,98 +9,97 @@ 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 outputDirectory = argv.output ? argv.output : undefined;
|
||||
// define targetDirectory: use outputDirectory if defined, otherwise create extension in CWD
|
||||
const targetDirectory = outputDirectory
|
||||
? path.resolve(argv.output, `${extName}-ext`)
|
||||
: path.resolve(cwd, `${extName}-ext`);
|
||||
const { main } = supernovaPkg;
|
||||
|
||||
let extDefinition = path.resolve(__dirname, '../src/ext-definition');
|
||||
const qextTargetDir = path.resolve(cwd, 'dist-ext');
|
||||
const qextFileName = path.resolve(cwd, `${extName}.qext`);
|
||||
const qextFileNameJs = qextFileName.replace(/\.qext$/, '.js');
|
||||
|
||||
if (argv.ext) {
|
||||
extDefinition = path.resolve(argv.ext);
|
||||
fs.removeSync(qextTargetDir);
|
||||
fs.removeSync(qextFileName);
|
||||
fs.removeSync(qextFileNameJs);
|
||||
|
||||
const extDefinition = argv.ext ? path.resolve(argv.ext) : undefined;
|
||||
|
||||
const createQextFiles = () => {
|
||||
const qext = supernovaPkg.qext || {};
|
||||
if (argv.meta) {
|
||||
const meta = require(path.resolve(cwd, argv.meta)); // eslint-disable-line
|
||||
Object.assign(qext, meta);
|
||||
}
|
||||
const contents = {
|
||||
name: qext.name || extName,
|
||||
version: supernovaPkg.version,
|
||||
description: supernovaPkg.description,
|
||||
author: supernovaPkg.author,
|
||||
icon: qext.icon || 'extension',
|
||||
preview: qext.preview,
|
||||
type: 'visualization',
|
||||
supernova: true,
|
||||
};
|
||||
|
||||
let qextjs = fs.readFileSync(path.resolve(__dirname, extDefinition ? '../src/ext.js' : '../src/empty-ext.js'), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
qextjs = qextjs.replace('{{DIST}}', `./${main.replace(/^[./]*/, '').replace(/\.js$/, '')}`);
|
||||
|
||||
fs.writeFileSync(qextFileName, JSON.stringify(contents, null, 2));
|
||||
fs.writeFileSync(qextFileNameJs, qextjs);
|
||||
|
||||
if (supernovaPkg.files) {
|
||||
[extDefinition ? path.basename(qextTargetDir) : false, path.basename(qextFileNameJs), path.basename(qextFileName)]
|
||||
.filter(Boolean)
|
||||
.forEach(f => {
|
||||
if (!supernovaPkg.files.includes(f)) {
|
||||
console.warn(` \x1b[33mwarn:\x1b[0m \x1b[36m${f}\x1b[0m should be included in package.json 'files' array`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (extDefinition) {
|
||||
const bundle = await rollup.rollup({
|
||||
input: 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({
|
||||
file: path.resolve(qextTargetDir, 'ext.js'),
|
||||
format: 'amd',
|
||||
sourcemap: argv.ext && argv.sourcemap,
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
);
|
||||
createQextFiles();
|
||||
}
|
||||
|
||||
module.exports = build;
|
||||
|
||||
@@ -30,13 +30,9 @@
|
||||
"@babel/cli": "7.8.3",
|
||||
"@babel/core": "7.8.3",
|
||||
"@babel/preset-env": "7.8.3",
|
||||
"@nebula.js/locale": "0.1.0",
|
||||
"@nebula.js/supernova": "0.1.1",
|
||||
"@nebula.js/theme": "0.1.0",
|
||||
"@rollup/plugin-commonjs": "11.0.1",
|
||||
"@rollup/plugin-node-resolve": "7.0.0",
|
||||
"chalk": "3.0.0",
|
||||
"node-event-emitter": "0.0.1",
|
||||
"fs-extra": "8.1.0",
|
||||
"rollup": "1.30.0",
|
||||
"rollup-plugin-babel": "4.3.3",
|
||||
"rollup-plugin-terser": "5.2.0",
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
3
commands/sense/src/empty-ext.js
Normal file
3
commands/sense/src/empty-ext.js
Normal file
@@ -0,0 +1,3 @@
|
||||
define(['{{DIST}}'], function m(supernova) {
|
||||
return supernova;
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
export default {
|
||||
definition: {
|
||||
type: 'items',
|
||||
component: 'accordion',
|
||||
items: {
|
||||
data: {
|
||||
uses: 'data',
|
||||
},
|
||||
// settings: {
|
||||
// uses: 'settings',
|
||||
// },
|
||||
},
|
||||
},
|
||||
support: {
|
||||
export: false,
|
||||
exportData: false,
|
||||
snapshot: false,
|
||||
viewData: false,
|
||||
},
|
||||
};
|
||||
7
commands/sense/src/ext.js
Normal file
7
commands/sense/src/ext.js
Normal file
@@ -0,0 +1,7 @@
|
||||
define(['{{DIST}}', './dist-ext/ext'], function m(supernova, ext) {
|
||||
return function supernovaExtension(env) {
|
||||
var v = supernova(env);
|
||||
v.ext = typeof ext === 'function' ? ext(env) : ext;
|
||||
return v;
|
||||
};
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
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;
|
||||
@@ -1,68 +0,0 @@
|
||||
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();
|
||||
selectionAPI.emit('activated');
|
||||
},
|
||||
clear() {
|
||||
scope.backendApi.model.resetMadeSelections();
|
||||
},
|
||||
confirm() {
|
||||
scope.selectionsApi.confirm();
|
||||
},
|
||||
cancel() {
|
||||
scope.selectionsApi.cancel();
|
||||
},
|
||||
select(s) {
|
||||
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;
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
import { generator as supernova } from '@nebula.js/supernova';
|
||||
import themeFn from '@nebula.js/theme';
|
||||
import localeFn from '@nebula.js/locale';
|
||||
|
||||
import permissions from './permissions';
|
||||
import selectionsApi from './selections';
|
||||
|
||||
import snDefinition from 'snDefinition'; // eslint-disable-line
|
||||
import extDefinition from 'extDefinition'; // eslint-disable-line
|
||||
|
||||
// TODO - how to initiate with the language used in Sense without importing the translator module from Sense?
|
||||
const loc = localeFn();
|
||||
const env = {
|
||||
translator: loc.translator,
|
||||
};
|
||||
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.initial,
|
||||
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) {
|
||||
// create a theme api with default nebula theme
|
||||
// note that this will not consume a Sense theme
|
||||
this.theme = themeFn();
|
||||
|
||||
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.sn = sn;
|
||||
this.snComponent = sn.component;
|
||||
this.snComponent.created({
|
||||
options: this.options || {},
|
||||
});
|
||||
this.snComponent.mounted(element);
|
||||
},
|
||||
paint($element, layout) {
|
||||
const perms = permissions(this.options, this.backendApi);
|
||||
const constraints = {
|
||||
passive: perms.indexOf('passive') === -1 || undefined,
|
||||
active: perms.indexOf('interact') === -1 || undefined,
|
||||
select: perms.indexOf('select') === -1 || undefined,
|
||||
};
|
||||
return this.snComponent.render({
|
||||
layout,
|
||||
context: {
|
||||
appLayout: {
|
||||
qLocaleInfo: this.backendApi.localeInfo,
|
||||
},
|
||||
constraints,
|
||||
theme: this.theme.externalAPI,
|
||||
},
|
||||
});
|
||||
},
|
||||
destroy() {
|
||||
this.sn.destroy();
|
||||
if (this.snComponent) {
|
||||
this.snComponent.willUnmount();
|
||||
this.snComponent.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user