mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
initial commit
This commit is contained in:
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# 2 space indentation
|
||||
[*.*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
25
.eslintrc.json
Normal file
25
.eslintrc.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"airbnb"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "preact"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": [2, { "devDependencies": true }],
|
||||
"max-len": 0,
|
||||
"no-plusplus": 0,
|
||||
"no-bitwise" : 0,
|
||||
"react/destructuring-assignment": [0, "always"],
|
||||
"react/prop-types": 0
|
||||
}
|
||||
}
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
*.log
|
||||
*.rej
|
||||
*.tmp
|
||||
*.log
|
||||
.cache/
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
.npmrc
|
||||
|
||||
node_modules/
|
||||
coverage/
|
||||
dist/
|
||||
67
aw.config.js
Normal file
67
aw.config.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* eslint import/no-extraneous-dependencies: 0, prefer-destructuring: 0, no-param-reassign: 0 */
|
||||
|
||||
const yargs = require('yargs');
|
||||
const path = require('path');
|
||||
const globby = require('globby');
|
||||
const { workspaces } = require('./package.json');
|
||||
|
||||
const argv = yargs
|
||||
.options({
|
||||
scope: {
|
||||
description: 'Scope to package',
|
||||
type: 'string',
|
||||
default: '',
|
||||
alias: 's',
|
||||
},
|
||||
type: {
|
||||
description: 'Type of tests to run',
|
||||
type: 'string',
|
||||
alias: 't',
|
||||
default: 'unit',
|
||||
choices: ['unit'],
|
||||
},
|
||||
})
|
||||
.coerce('scope', (scope) => {
|
||||
const scopes = new Map();
|
||||
globby.sync(workspaces.map(p => `${p}/package.json`)).forEach((p) => {
|
||||
const name = require(`./${p}`).name; //eslint-disable-line
|
||||
const pkgPath = path.dirname(p);
|
||||
scopes.set(name, pkgPath);
|
||||
});
|
||||
const s = scopes.get(scope);
|
||||
if (s) {
|
||||
return s;
|
||||
}
|
||||
if (scope && !s) {
|
||||
throw new Error(`Scope ${scope} not found`);
|
||||
}
|
||||
return `*(${workspaces.join('|').split('/*').join('')})/*`;
|
||||
})
|
||||
.argv;
|
||||
|
||||
const TYPES = {
|
||||
unit: {
|
||||
glob: `${argv.scope}/__tests__/unit/**/*.spec.js`,
|
||||
reportDir: 'coverage/unit',
|
||||
},
|
||||
};
|
||||
|
||||
const type = TYPES[argv.type];
|
||||
|
||||
const glob = [type.glob];
|
||||
const src = [`${argv.scope}/src/**/*.{js,jsx}`];
|
||||
|
||||
module.exports = {
|
||||
glob,
|
||||
src,
|
||||
watchGlob: [...src, ...glob],
|
||||
nyc: {
|
||||
include: src,
|
||||
sourceMap: false,
|
||||
instrumenter: './lib/instrumenters/noop',
|
||||
reportDir: 'coverage/unit',
|
||||
},
|
||||
mocha: Object.assign({
|
||||
timeout: 30000,
|
||||
}, type.mocha),
|
||||
};
|
||||
21
babel.config.js
Normal file
21
babel.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
[
|
||||
'istanbul',
|
||||
{
|
||||
exclude: [
|
||||
'**/test/**',
|
||||
'**/dist/**',
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
21
examples/mashup/connect.js
Normal file
21
examples/mashup/connect.js
Normal file
@@ -0,0 +1,21 @@
|
||||
window.connect = function connect() {
|
||||
const loadSchema = () => fetch('https://raw.githubusercontent.com/qlik-oss/enigma.js/master/schemas/3.2.json').then(response => response.json());
|
||||
|
||||
const createConnection = () => loadSchema().then(schema => window.enigma.create({
|
||||
schema,
|
||||
url: `ws://${window.location.hostname || 'localhost'}:19076/app/${Date.now()}`,
|
||||
}).open().then(qix => qix.createSessionApp()));
|
||||
|
||||
return createConnection().then(app => app.setScript(`
|
||||
Characters:
|
||||
Load Chr(RecNo()+Ord('A')-1) as Alpha, RecNo() as Num autogenerate 26;
|
||||
|
||||
ASCII:
|
||||
Load
|
||||
if(RecNo()>=65 and RecNo()<=90,RecNo()-64) as Num,
|
||||
Chr(RecNo()) as AsciiAlpha,
|
||||
RecNo() as AsciiNum
|
||||
autogenerate 85
|
||||
Where (RecNo()>=65 and RecNo()<=126) or RecNo()>=160;
|
||||
`).then(() => app.doReload().then(() => app)));
|
||||
};
|
||||
9
examples/mashup/docker-compose.yml
Normal file
9
examples/mashup/docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
engine:
|
||||
image: qlikcore/engine:12.300.0
|
||||
restart: always
|
||||
command: -S AcceptEULA=${ACCEPT_EULA}
|
||||
ports:
|
||||
- "19076:19076"
|
||||
45
examples/mashup/index.html
Normal file
45
examples/mashup/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nebula mashup</title>
|
||||
<script src="./node_modules/enigma.js/enigma.js"></script>
|
||||
<script src="../../packages/nucleus/dist/nucleus.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="./node_modules/leonardo-ui/dist/leonardo-ui.css" type="text/css"/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "LUI icons";
|
||||
src: url(node_modules/leonardo-ui/dist/lui-icons.woff) format('woff'),
|
||||
url(node_modules/leonardo-ui/dist/lui-icons.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.object {
|
||||
position: relative;
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="content">
|
||||
<div class="object" data-type='foo'></div>
|
||||
</div>
|
||||
|
||||
<script src="connect.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</html>
|
||||
22
examples/mashup/index.js
Normal file
22
examples/mashup/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
connect().then((app) => {
|
||||
const sn = {
|
||||
component: {
|
||||
mounted(element) {
|
||||
element.textContent = 'Hello';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nebbie = window.nucleus(app)
|
||||
.load((type, config) => config.Promise.resolve(sn));
|
||||
|
||||
document.querySelectorAll('.object').forEach((el) => {
|
||||
const type = el.getAttribute('data-type');
|
||||
|
||||
nebbie.create({
|
||||
type,
|
||||
}, {
|
||||
element: el,
|
||||
});
|
||||
});
|
||||
});
|
||||
7
examples/mashup/package.json
Normal file
7
examples/mashup/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"enigma.js": "^2.4.0",
|
||||
"leonardo-ui": "^1.6.0"
|
||||
}
|
||||
}
|
||||
11
examples/mashup/yarn.lock
Normal file
11
examples/mashup/yarn.lock
Normal file
@@ -0,0 +1,11 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
enigma.js@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/enigma.js/-/enigma.js-2.4.0.tgz#e5263116e7168e27f11cfe326ac83ed738bb7cc4"
|
||||
|
||||
leonardo-ui@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/leonardo-ui/-/leonardo-ui-1.6.0.tgz#65eab0a4fd9a54f87a75d08e4440900555f56c24"
|
||||
15
lerna.json
Normal file
15
lerna.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"command": {
|
||||
"publish": {
|
||||
"registry": "https://qliktech.jfrog.io/qliktech/api/npm/npm-local/"
|
||||
},
|
||||
"version": {
|
||||
"allowBranch": "master",
|
||||
"conventionalCommits": true,
|
||||
"message": "chore(release): %s 🚀"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true
|
||||
}
|
||||
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production FORCE_COLOR=1 lerna run build --stream",
|
||||
"build:watch": "FORCE_COLOR=1 lerna run build:watch --stream",
|
||||
"test": "yarn run test:unit",
|
||||
"test:unit": "aw -c aw.config.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/qlik-trial/nebula.js.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.2.3",
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/plugin-transform-react-jsx": "^7.3.0",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"after-work.js": "^4.3.2",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "^5.12.1",
|
||||
"eslint-config-airbnb": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.15.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.0",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"lerna": "^3.10.7",
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-babel": "^4.3.2",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-dependency-flow": "^0.3.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-replace": "^2.1.0",
|
||||
"rollup-plugin-sass": "^1.1.0",
|
||||
"rollup-plugin-terser": "^4.0.3",
|
||||
"yargs": "^12.0.5"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
72
packages/nucleus/nucleus-bundle.css
Normal file
72
packages/nucleus/nucleus-bundle.css
Normal file
@@ -0,0 +1,72 @@
|
||||
.nucleus-selection-toolbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -48px;
|
||||
padding: 8px;
|
||||
line-height: 24px;
|
||||
box-sizing: border-box;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.nucleus-selection-toolbar > * {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.nucleus-cell {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: #404040;
|
||||
font-family: "Source Sans Pro", Arial, "sans-serif";
|
||||
}
|
||||
.nucleus-cell .nucleus-cell__header {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.nucleus-cell .nucleus-cell__footnote {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.nucleus-cell .nucleus-content {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.nucleus-cell__error {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAM0lEQVQYV2P8z8BgzMjAcJYBCUDFkIUYGECCIBFGdJVQwbNwCSSVYGPBEuiCYAlsgiAJANbwDGtj7MYdAAAAAElFTkSuQmCC);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nucleus-content {
|
||||
position: relative;
|
||||
}
|
||||
.nucleus-content .nucleus-content__body {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.nucleus-type--s {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.nucleus-type--m {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
22
packages/nucleus/package.json
Normal file
22
packages/nucleus/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@nebula.js/nucleus",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [],
|
||||
"main": "dist/nucleus.js",
|
||||
"module": "dist/nucleus.esm.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production rollup --config ../../rollup.config.js",
|
||||
"build:watch": "rollup --config ../../rollup.config.js -w",
|
||||
"lint": "eslint --ext .js,.jsx src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nebula.js/selections": "^0.1.0",
|
||||
"@nebula.js/supernova": "^0.1.0",
|
||||
"node-event-emitter": "^0.0.1",
|
||||
"preact": "^8.4.2"
|
||||
}
|
||||
}
|
||||
71
packages/nucleus/src/booter.jsx
Normal file
71
packages/nucleus/src/booter.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import preact from 'preact';
|
||||
import { createAppSelectionAPI } from '@nebula.js/selections';
|
||||
import Cell from './components/Cell';
|
||||
|
||||
import populateData from './populator';
|
||||
|
||||
import './components/Style.scss';
|
||||
|
||||
// import viz from './viz';
|
||||
|
||||
export function boot({
|
||||
id,
|
||||
}, visual, config, nebbie, app) {
|
||||
if (!app.selections) {
|
||||
let selections = null;
|
||||
Object.defineProperty(app, 'selections', {
|
||||
get() {
|
||||
selections = selections || createAppSelectionAPI(app);
|
||||
return selections;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new config.env.Promise((resolve) => {
|
||||
let reference;
|
||||
const unmount = () => {
|
||||
preact.render('', visual.element, reference);
|
||||
};
|
||||
|
||||
const init = (model) => {
|
||||
reference = preact.render(
|
||||
<Cell
|
||||
nebbie={nebbie}
|
||||
model={model}
|
||||
app={app}
|
||||
prom={resolve}
|
||||
config={config}
|
||||
visual={visual}
|
||||
unmount={unmount}
|
||||
/>,
|
||||
visual.element,
|
||||
);
|
||||
model.once('closed', unmount);
|
||||
};
|
||||
|
||||
app.getObject(id).then(init);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function create({
|
||||
type,
|
||||
version,
|
||||
}, visual, nebbie, app) {
|
||||
const t = nebbie.types.fetch(type, version);
|
||||
return t.initialProperties(visual.props)
|
||||
.then(properties => t.supernova().then((sn) => {
|
||||
if (visual.fields) {
|
||||
populateData({
|
||||
sn,
|
||||
properties,
|
||||
fields: visual.fields,
|
||||
});
|
||||
}
|
||||
|
||||
return app.createSessionObject(properties)
|
||||
.then(model => nebbie.get({
|
||||
id: model.id,
|
||||
}, visual));
|
||||
}));
|
||||
}
|
||||
42
packages/nucleus/src/components/Button.jsx
Normal file
42
packages/nucleus/src/components/Button.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// https://github.com/ricsv/react-leonardo-ui/blob/master/src/button/button.js
|
||||
|
||||
import preact from 'preact';
|
||||
|
||||
import { luiClassName } from '../lui/util';
|
||||
|
||||
export default class Button extends preact.Component {
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
block,
|
||||
rounded,
|
||||
active,
|
||||
...extraProps
|
||||
} = this.props;
|
||||
|
||||
const finalClassName = luiClassName('button', {
|
||||
className,
|
||||
modifiers: {
|
||||
variant,
|
||||
size,
|
||||
block,
|
||||
rounded,
|
||||
},
|
||||
states: { active },
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
ref={(element) => { this.element = element; }}
|
||||
className={finalClassName}
|
||||
{...extraProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
225
packages/nucleus/src/components/Cell.jsx
Normal file
225
packages/nucleus/src/components/Cell.jsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import preact from 'preact';
|
||||
|
||||
import { prefixer } from '../utils';
|
||||
|
||||
import Requirements from './Requirements';
|
||||
import CError from './Error';
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import Supernova from './Supernova';
|
||||
import Placeholder from './Placeholder';
|
||||
import SelectionToolbar from './SelectionToolbar';
|
||||
|
||||
// import './Cell.scss';
|
||||
|
||||
import viz from '../viz';
|
||||
|
||||
const showRequirements = (sn, layout) => {
|
||||
if (!sn || !sn.definition || !sn.definition.qae || !layout || !layout.qHyperCube) {
|
||||
return false;
|
||||
}
|
||||
const def = sn.definition.qae.data.targets[0];
|
||||
if (!def) {
|
||||
return false;
|
||||
}
|
||||
const minD = def.dimensions.min();
|
||||
const minM = def.measures.min();
|
||||
return (layout.qHyperCube.qDimensionInfo.length < minD
|
||||
|| layout.qHyperCube.qMeasureInfo.length < minM);
|
||||
};
|
||||
|
||||
const Content = ({ children }) => (
|
||||
<div className={prefixer(['content'])}>
|
||||
<div className={prefixer(['content__body'])}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
class Cell extends preact.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
context: {
|
||||
...props.visual.context,
|
||||
},
|
||||
options: {
|
||||
...props.visual.options,
|
||||
},
|
||||
layout: {},
|
||||
sn: null,
|
||||
};
|
||||
|
||||
this.contentRef = preact.createRef();
|
||||
|
||||
let type = '__placeholder__';
|
||||
let version = '__placeholder__';
|
||||
|
||||
const { Promise } = props.config.env;
|
||||
const { model, app } = props;
|
||||
|
||||
const externalAPI = viz({
|
||||
app,
|
||||
model,
|
||||
cell: this,
|
||||
});
|
||||
|
||||
this.unmount = () => {
|
||||
props.unmount();
|
||||
};
|
||||
|
||||
this.takeSnapshot = () => {
|
||||
const rect = this.contentRef.current.base.children[0].getBoundingClientRect();
|
||||
if (this.state.sn) {
|
||||
const snapshot = {
|
||||
...this.state.layout,
|
||||
snapshotData: {
|
||||
object: {
|
||||
size: {
|
||||
w: rect.width,
|
||||
h: rect.height,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof this.state.sn.component.setSnapshotData === 'function') {
|
||||
return this.state.sn.component.setSnapshotData(snapshot);
|
||||
}
|
||||
return Promise.resolve(snapshot);
|
||||
}
|
||||
return Promise.reject();
|
||||
};
|
||||
|
||||
const onDone = new Promise((resolve) => { // eslint-disable-line
|
||||
this.onDone = resolve;
|
||||
}).then(() => {
|
||||
this.props.prom(externalAPI);
|
||||
});
|
||||
|
||||
const setType = (t, v) => {
|
||||
type = t;
|
||||
version = v;
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
props.nebbie.types.supernova(t, v).then((SN) => {
|
||||
const sn = SN.create({
|
||||
model,
|
||||
app: props.app,
|
||||
});
|
||||
sn.component.on('rendered', (...args) => {
|
||||
externalAPI.emit('rendered', ...args);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
sn,
|
||||
error: null,
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
error: {
|
||||
message: `${e.message}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onChanged = () => model.getLayout().then((layout) => {
|
||||
if (model.selections) {
|
||||
model.selections.setLayout(layout);
|
||||
if (layout.qSelectionInfo && layout.qSelectionInfo.qInSelections
|
||||
&& !model.selections.isModal()) {
|
||||
model.selections.goModal('/qHyperCubeDef');
|
||||
}
|
||||
if (!layout.qSelectionInfo || !layout.qSelectionInfo.qInSelections) {
|
||||
if (model.selections.isModal()) {
|
||||
model.selections.noModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (layout.visualization !== type || layout.version !== version) {
|
||||
setType(layout.visualization, layout.version);
|
||||
this.setState({
|
||||
layout,
|
||||
sn: null,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
layout,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
model.on('changed', onChanged);
|
||||
model.once('closed', () => {
|
||||
model.removeListener('changed', onChanged);
|
||||
});
|
||||
|
||||
onChanged();
|
||||
|
||||
this.cleanUp = () => {
|
||||
model.removeListener('changed', onChanged);
|
||||
};
|
||||
}
|
||||
|
||||
// componentDidMount() {
|
||||
// // console.log('mounted');
|
||||
// }
|
||||
|
||||
// componentDidUpdate() {
|
||||
// }
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cleanUp();
|
||||
}
|
||||
|
||||
componentDidCatch() {
|
||||
this.setState({
|
||||
error: {
|
||||
message: 'Failed to render',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const s = this.state;
|
||||
const SN = (showRequirements(this.state.sn, this.state.layout) ? Requirements : Supernova);
|
||||
const Comp = !this.state.sn ? Placeholder : SN;
|
||||
return (
|
||||
<div className={prefixer(['cell-wrapper'])}>
|
||||
{
|
||||
this.state.sn
|
||||
&& this.state.layout.qSelectionInfo
|
||||
&& this.state.layout.qSelectionInfo.qInSelections
|
||||
&& <SelectionToolbar model={this.props.model} sn={this.state.sn} />
|
||||
}
|
||||
<div className={prefixer(['cell'])}>
|
||||
<Header layout={s.layout} />
|
||||
<Content ref={this.contentRef}>
|
||||
{this.state.error
|
||||
? (<CError {...this.state.error} />)
|
||||
: (
|
||||
<Comp
|
||||
key={this.state.layout.visualization}
|
||||
sn={this.state.sn}
|
||||
snContext={this.state.context}
|
||||
snOptions={this.state.options}
|
||||
prom={this.onDone}
|
||||
model={this.props.model}
|
||||
layout={this.state.layout}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Content>
|
||||
<Footer layout={s.layout} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Cell;
|
||||
62
packages/nucleus/src/components/Cell.scss
Normal file
62
packages/nucleus/src/components/Cell.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
.#{$ns}-cell {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
|
||||
color: #404040;
|
||||
font-family: 'Source Sans Pro', Arial, 'sans-serif';
|
||||
|
||||
.#{$ns}-cell__header {
|
||||
flex: 0 0 auto;
|
||||
padding: $spacing;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.#{$ns}-cell__footnote {
|
||||
flex: 0 0 auto;
|
||||
padding: $spacing;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.#{$ns}-content {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}-cell__error {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAM0lEQVQYV2P8z8BgzMjAcJYBCUDFkIUYGECCIBFGdJVQwbNwCSSVYGPBEuiCYAlsgiAJANbwDGtj7MYdAAAAAElFTkSuQmCC);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.#{$ns}-content {
|
||||
position: relative;
|
||||
|
||||
.#{$ns}-content__body {
|
||||
position: absolute;
|
||||
top: $spacing;
|
||||
left: $spacing;
|
||||
bottom: $spacing;
|
||||
right: $spacing;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}-type--s {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.#{$ns}-type--m {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
12
packages/nucleus/src/components/Error.jsx
Normal file
12
packages/nucleus/src/components/Error.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import preact from 'preact';
|
||||
|
||||
import { prefixer } from '../utils';
|
||||
|
||||
const Component = props => (
|
||||
<div className={prefixer('cell__error')}>
|
||||
<div>Error</div>
|
||||
<div>{props.message}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Component;
|
||||
41
packages/nucleus/src/components/FadeButton.jsx
Normal file
41
packages/nucleus/src/components/FadeButton.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
// https://github.com/ricsv/react-leonardo-ui/blob/master/src/fade-button/fade-button.js
|
||||
|
||||
import preact from 'preact';
|
||||
|
||||
import { luiClassName } from '../lui/util';
|
||||
|
||||
const FadeButton = ({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
block,
|
||||
rounded,
|
||||
active,
|
||||
...extraProps
|
||||
}) => {
|
||||
const finalClassName = luiClassName('fade-button', {
|
||||
className,
|
||||
modifiers: {
|
||||
variant,
|
||||
size,
|
||||
block,
|
||||
rounded,
|
||||
},
|
||||
states: { active },
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={finalClassName}
|
||||
{
|
||||
...extraProps
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FadeButton;
|
||||
12
packages/nucleus/src/components/Footer.jsx
Normal file
12
packages/nucleus/src/components/Footer.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import preact from 'preact';
|
||||
import { prefixer } from '../utils';
|
||||
|
||||
const Footer = ({ layout }) => (
|
||||
layout && layout.showTitles && layout.footnote ? (
|
||||
<footer className={prefixer(['cell__footnote'])}>
|
||||
<div className={prefixer(['type', 'type--s'])}>{layout.footnote}</div>
|
||||
</footer>
|
||||
) : null
|
||||
);
|
||||
|
||||
export default Footer;
|
||||
15
packages/nucleus/src/components/Header.jsx
Normal file
15
packages/nucleus/src/components/Header.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import preact from 'preact';
|
||||
import { prefixer } from '../utils';
|
||||
|
||||
const Header = ({ layout }) => (
|
||||
layout && layout.showTitles && (layout.title || layout.subtitle) ? (
|
||||
<header className={prefixer(['cell__header'])}>
|
||||
<div className={prefixer(['type--m'])}>
|
||||
{layout.title}
|
||||
</div>
|
||||
<div className={prefixer(['type--s'])}>{layout.subtitle}</div>
|
||||
</header>
|
||||
) : null
|
||||
);
|
||||
|
||||
export default Header;
|
||||
22
packages/nucleus/src/components/Icon.jsx
Normal file
22
packages/nucleus/src/components/Icon.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
// https://github.com/ricsv/react-leonardo-ui/blob/master/src/icon/icon.js
|
||||
|
||||
import preact from 'preact';
|
||||
|
||||
import { luiClassName } from '../lui/util';
|
||||
|
||||
const Icon = ({
|
||||
className,
|
||||
name,
|
||||
size,
|
||||
...extraProps
|
||||
}) => {
|
||||
const finalClassName = luiClassName('icon', {
|
||||
className,
|
||||
modifiers: { name, size },
|
||||
});
|
||||
return (
|
||||
<span className={finalClassName} aria-hidden="true" {...extraProps} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
0
packages/nucleus/src/components/Loading.jsx
Normal file
0
packages/nucleus/src/components/Loading.jsx
Normal file
7
packages/nucleus/src/components/Placeholder.jsx
Normal file
7
packages/nucleus/src/components/Placeholder.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import preact from 'preact';
|
||||
|
||||
const Component = () => (
|
||||
<div> </div>
|
||||
);
|
||||
|
||||
export default Component;
|
||||
7
packages/nucleus/src/components/Requirements.jsx
Normal file
7
packages/nucleus/src/components/Requirements.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import preact from 'preact';
|
||||
|
||||
const Component = () => (
|
||||
<div>More stuff required</div>
|
||||
);
|
||||
|
||||
export default Component;
|
||||
133
packages/nucleus/src/components/SelectionToolbar.jsx
Normal file
133
packages/nucleus/src/components/SelectionToolbar.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import preact from 'preact';
|
||||
import Button from './Button';
|
||||
import FadeButton from './FadeButton';
|
||||
import Icon from './Icon';
|
||||
|
||||
import { prefixer } from '../utils';
|
||||
/* eslint react/no-multi-comp: 0 */
|
||||
|
||||
class Item extends preact.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
active: props.item.active && props.item.active(),
|
||||
disabled: props.item.enabled && !props.item.enabled(),
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps) {
|
||||
return {
|
||||
disabled: nextProps.item.enabled && !nextProps.item.enabled(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.item.active && this.props.item.action && this.props.item.on) {
|
||||
this.onChange = () => {
|
||||
this.setState({
|
||||
active: this.props.item.active && this.props.item.active(),
|
||||
disabled: this.props.item.enabled && !this.props.item.enabled(),
|
||||
});
|
||||
};
|
||||
|
||||
this.props.item.on('changed', this.onChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.onChange && this.props.item.removeListener) {
|
||||
this.props.item.removeListener('changed', this.onChange);
|
||||
}
|
||||
this.onChange = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props.item;
|
||||
const Btn = props.type === 'fade-button' || this.props.isCustom ? FadeButton : Button;
|
||||
return (
|
||||
<Btn
|
||||
onClick={() => props.action()}
|
||||
variant={props.variant}
|
||||
active={this.state.active}
|
||||
disabled={this.state.disabled}
|
||||
>
|
||||
{props.icon ? <Icon name={props.icon} /> : props.label}
|
||||
</Btn>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Component extends preact.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const api = props.model.selections;
|
||||
|
||||
this.state = {
|
||||
confirmable: api.canConfirm(),
|
||||
cancelable: api.canCancel(),
|
||||
clearable: api.canClear(),
|
||||
};
|
||||
|
||||
const items = [];
|
||||
|
||||
items.push({
|
||||
key: 'confirm',
|
||||
type: 'fade-button',
|
||||
label: 'Confirm',
|
||||
// variant: 'success',
|
||||
icon: 'tick',
|
||||
enabled: () => this.state.confirmable,
|
||||
action: () => api.confirm(props.sn.component),
|
||||
});
|
||||
|
||||
items.push({
|
||||
key: 'cancel',
|
||||
type: 'fade-button',
|
||||
label: 'Cancel',
|
||||
// variant: 'danger',
|
||||
icon: 'close',
|
||||
enabled: () => this.state.cancelable,
|
||||
action: () => api.cancel(props.sn.component),
|
||||
});
|
||||
|
||||
items.push({
|
||||
key: 'clear',
|
||||
type: 'fade-button',
|
||||
label: 'Clear',
|
||||
icon: 'clear-selections',
|
||||
enabled: () => this.state.clearable,
|
||||
action: () => api.clear(props.sn.component),
|
||||
});
|
||||
|
||||
this.listeners = [];
|
||||
this.custom = {};
|
||||
|
||||
(props.sn.selectionToolbar.items || []).forEach((item) => {
|
||||
this.custom[item.key] = true;
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
items.reverse();
|
||||
|
||||
this.state.items = items;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps) {
|
||||
const api = nextProps.model.selections;
|
||||
return {
|
||||
confirmable: api.canConfirm(),
|
||||
cancelable: api.canCancel(),
|
||||
clearable: api.canClear(),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={prefixer('selection-toolbar')}>
|
||||
{this.state.items.map(itm => <Item key={itm.key} item={itm} isCustom={!!this.custom[itm.key]} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Component;
|
||||
16
packages/nucleus/src/components/SelectionToolbar.scss
Normal file
16
packages/nucleus/src/components/SelectionToolbar.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.#{$ns}-selection-toolbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -48px;
|
||||
padding: $spacing;
|
||||
line-height: 24px;
|
||||
box-sizing: border-box;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
> * {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
5
packages/nucleus/src/components/Style.scss
Normal file
5
packages/nucleus/src/components/Style.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
$ns: nucleus;
|
||||
$spacing: 8px;
|
||||
|
||||
@import 'SelectionToolbar';
|
||||
@import 'Cell';
|
||||
201
packages/nucleus/src/components/Supernova.jsx
Normal file
201
packages/nucleus/src/components/Supernova.jsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import preact from 'preact';
|
||||
|
||||
const constrainElement = (el, d) => {
|
||||
/* eslint-disable no-param-reassign */
|
||||
if (d) {
|
||||
el.style.width = `${d.width}px`;
|
||||
el.style.height = `${d.height}px`;
|
||||
el.style.top = `${d.top}px`;
|
||||
el.style.left = `${d.left}px`;
|
||||
} else {
|
||||
el.style.width = undefined;
|
||||
el.style.height = undefined;
|
||||
el.style.top = 0;
|
||||
el.style.left = 0;
|
||||
el.style.right = 0;
|
||||
el.style.bottom = 0;
|
||||
}
|
||||
/* eslint-enable no-param-reassign */
|
||||
};
|
||||
|
||||
const scheduleRender = (props, prev, initial, contentElement) => {
|
||||
if (prev) {
|
||||
prev.reject();
|
||||
}
|
||||
const prom = {};
|
||||
|
||||
const p = new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
const parentRect = contentElement.parentElement.parentElement.getBoundingClientRect();
|
||||
const r = typeof props.snContext.logicalSize === 'function' ? props.snContext.logicalSize(props.layout, props.sn) : props.sn.logicalSize({ layout: props.layout });
|
||||
const logicalSize = r || undefined;
|
||||
if (r) {
|
||||
// const rect = that.element.getBoundingClientRect();
|
||||
|
||||
const parentRatio = parentRect.width / parentRect.height;
|
||||
const rRatio = r.width / r.height;
|
||||
|
||||
let width;
|
||||
let height;
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
if (parentRatio > rRatio) { // parent is wider -> limit height
|
||||
({ height } = parentRect);
|
||||
width = height * rRatio;
|
||||
left = (parentRect.width - width) / 2;
|
||||
top = 0;
|
||||
} else {
|
||||
({ width } = parentRect);
|
||||
height = width / rRatio;
|
||||
left = 0;
|
||||
top = (parentRect.height - height) / 2;
|
||||
}
|
||||
|
||||
constrainElement(contentElement, {
|
||||
top, left, width, height,
|
||||
});
|
||||
} else {
|
||||
constrainElement(contentElement);
|
||||
}
|
||||
|
||||
initial.mount();
|
||||
|
||||
Promise.resolve(props.sn.component.render({
|
||||
layout: props.layout,
|
||||
layoutChanges: props.layoutChanges,
|
||||
options: props.snOptions || {},
|
||||
context: {
|
||||
permissions: (props.snContext || {}).permissions,
|
||||
theme: (props.snContext || {}).theme,
|
||||
rtl: (props.snContext || {}).rtl,
|
||||
localeInfo: (props.snContext || {}).localeInfo,
|
||||
logicalSize,
|
||||
},
|
||||
})).then(() => {
|
||||
initial.rendered();
|
||||
// props.sn.component.didUpdate(); // TODO - should check if component is in update stage
|
||||
}).then(resolve);
|
||||
}, 0);
|
||||
|
||||
prom.reject = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
prom.then = p.then;
|
||||
|
||||
return prom;
|
||||
};
|
||||
|
||||
class Supernova extends preact.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.initial = {
|
||||
mount: () => {
|
||||
this.props.sn.component.created({
|
||||
options: this.props.snOptions,
|
||||
context: this.props.snContext,
|
||||
});
|
||||
this.props.sn.component.mounted(this.contentElement);
|
||||
this.initial.mount = () => {};
|
||||
},
|
||||
rendered: () => {
|
||||
this.props.prom();
|
||||
this.initial.rendered = () => {};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let resizeObserver;
|
||||
|
||||
this.dimensions = this.element.getBoundingClientRect();
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
const dims = this.element.getBoundingClientRect();
|
||||
if (dims.width !== this.dimensions.width || dims.height !== this.dimensions.height) {
|
||||
this.dimensions = dims;
|
||||
this.setState({});
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(this.element);
|
||||
}
|
||||
|
||||
this.onUnmount = () => {
|
||||
this.onUnmount = null;
|
||||
if (resizeObserver) {
|
||||
resizeObserver.unobserve(this.element);
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
|
||||
this.props.sn.component.willUnmount();
|
||||
if (this.next) {
|
||||
this.next.reject();
|
||||
}
|
||||
};
|
||||
|
||||
this.next = scheduleRender({
|
||||
snOptions: this.props.snOptions,
|
||||
snContext: this.props.snContext,
|
||||
sn: this.props.sn,
|
||||
layout: this.props.layout,
|
||||
}, this.next, this.initial, this.contentElement);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const update = nextProps.sn
|
||||
&& !(nextProps.layout
|
||||
&& nextProps.layout.qSelectionInfo
|
||||
&& nextProps.layout.qSelectionInfo.qInSelections);
|
||||
if (!update) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
// const should = nextProps.sn.component.shouldUpdate({
|
||||
// layout: nextProps.layout,
|
||||
// options: {},
|
||||
// });
|
||||
|
||||
// return should;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.next = scheduleRender(this.props, this.next, this.initial, this.contentElement);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.onUnmount();
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
ref={(element) => { this.element = element; }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
}}
|
||||
ref={(element) => { this.contentElement = element; }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Supernova;
|
||||
33
packages/nucleus/src/index.js
Normal file
33
packages/nucleus/src/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { create, boot } from './booter';
|
||||
import types from './sn/types';
|
||||
|
||||
function apiGenerator(app) {
|
||||
const config = {
|
||||
env: {
|
||||
Promise,
|
||||
},
|
||||
load: () => undefined,
|
||||
};
|
||||
|
||||
const context = {
|
||||
types: types(config),
|
||||
};
|
||||
|
||||
const api = {
|
||||
get: (common, viz) => boot(common, viz, config, api, app),
|
||||
create: (common, viz) => create(common, viz, api, app),
|
||||
env: (e) => {
|
||||
Object.assign(config.env, e);
|
||||
return api;
|
||||
},
|
||||
load: ($) => {
|
||||
config.load = $;
|
||||
return api;
|
||||
},
|
||||
types: context.types,
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
export default apiGenerator;
|
||||
38
packages/nucleus/src/lui/util.js
Normal file
38
packages/nucleus/src/lui/util.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export function camelToKebabCase(value) {
|
||||
return value.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
}
|
||||
|
||||
export function luiClassName(name, opts = {}) {
|
||||
const {
|
||||
className,
|
||||
modifiers = {},
|
||||
states = {},
|
||||
} = opts;
|
||||
|
||||
const baseClass = `lui-${name}`;
|
||||
let resClassName = baseClass;
|
||||
|
||||
Object.keys(modifiers).forEach((key) => {
|
||||
// Modifiers can be booleans or key-value pair of strings
|
||||
if (typeof modifiers[key] === 'boolean') {
|
||||
if (modifiers[key]) {
|
||||
resClassName += ` ${baseClass}--${key}`;
|
||||
}
|
||||
} else if (modifiers[key]) {
|
||||
resClassName += ` ${baseClass}--${modifiers[key]}`;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(states).forEach((key) => {
|
||||
// States are always booleans
|
||||
if (states[key]) {
|
||||
resClassName += ` lui-${key}`;
|
||||
}
|
||||
});
|
||||
|
||||
if (className) {
|
||||
resClassName += ` ${className}`;
|
||||
}
|
||||
|
||||
return resClassName;
|
||||
}
|
||||
66
packages/nucleus/src/populator.js
Normal file
66
packages/nucleus/src/populator.js
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
const nxDimension = f => ({
|
||||
qDef: {
|
||||
qFieldDefs: [f],
|
||||
qSortCriterias: [{
|
||||
qSortByLoadOrder: 1,
|
||||
qSortByNumeric: 1,
|
||||
qSortByAscii: 1,
|
||||
}],
|
||||
},
|
||||
qOtherTotalSpec: {},
|
||||
});
|
||||
|
||||
const nxMeasure = f => ({
|
||||
qDef: {
|
||||
qDef: f,
|
||||
},
|
||||
qSortBy: {
|
||||
qSortByLoadOrder: 1,
|
||||
qSortByNumeric: -1,
|
||||
},
|
||||
});
|
||||
|
||||
export default function populateData({
|
||||
sn,
|
||||
properties,
|
||||
fields,
|
||||
}) {
|
||||
const target = sn.qae.data.targets[0];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const { path } = target;
|
||||
|
||||
const parts = path.split('/');
|
||||
let p = properties;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const s = parts[i];
|
||||
p = s ? p[s] : p;
|
||||
}
|
||||
|
||||
const hc = p;
|
||||
|
||||
hc.qInterColumnSortOrder = hc.qInterColumnSortOrder || [];
|
||||
fields.forEach((f, i) => {
|
||||
let type = 'dimension';
|
||||
if ((typeof f === 'string' && f[0] === '=')
|
||||
|| (typeof f === 'object' && f.qDef.qDef)
|
||||
|| (typeof f === 'object' && f.qLibraryId && f.qType === 'measure')) {
|
||||
type = 'measure';
|
||||
}
|
||||
|
||||
hc.qInterColumnSortOrder.push(i);
|
||||
if (type === 'measure') {
|
||||
hc.qMeasures = hc.qMeasures || [];
|
||||
const def = nxMeasure(f);
|
||||
hc.qMeasures.push(def);
|
||||
target.measures.add.call(null, def, properties);
|
||||
} else {
|
||||
hc.qDimensions = hc.qDimensions || [];
|
||||
const def = nxDimension(f);
|
||||
hc.qDimensions.push(def);
|
||||
target.dimensions.add.call(null, def, properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
64
packages/nucleus/src/sn/type.js
Normal file
64
packages/nucleus/src/sn/type.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import SNFactory from '@nebula.js/supernova';
|
||||
|
||||
const LOADED = {};
|
||||
|
||||
const load = ({ name, version }, config) => {
|
||||
const key = `${name}__${version}`;
|
||||
if (!LOADED[key]) {
|
||||
const p = config.load({
|
||||
name,
|
||||
version,
|
||||
}, config.env);
|
||||
if (typeof p === 'string') {
|
||||
throw new Error('Return value must be a Promise');
|
||||
}
|
||||
LOADED[key] = p.catch((e) => {
|
||||
console.error(e);
|
||||
throw new Error(`Failed to load supernova: ${name}`);
|
||||
});
|
||||
}
|
||||
|
||||
return LOADED[key];
|
||||
};
|
||||
|
||||
export function clearFromCache(name) {
|
||||
Object.keys(LOADED).forEach((key) => {
|
||||
if (key.split('__')[0] === name) {
|
||||
LOADED[key] = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function (opts, meta, config) {
|
||||
let sn;
|
||||
const type = {
|
||||
name: opts.name,
|
||||
version: opts.version,
|
||||
/**
|
||||
* Initiate load of supernova
|
||||
* @returns {Promise<function|SNDefinition>}
|
||||
*/
|
||||
load: () => load(type, config),
|
||||
supernova: () => load(type, config)
|
||||
.then((SNDefinition) => {
|
||||
sn = sn || SNFactory(SNDefinition, config.env);
|
||||
return sn;
|
||||
}),
|
||||
initialProperties(initial) {
|
||||
return this.supernova().then((s) => {
|
||||
const props = {
|
||||
qInfo: {
|
||||
qType: 'foobar',
|
||||
},
|
||||
visualization: type.name,
|
||||
version: type.version,
|
||||
...s.qae.properties,
|
||||
...initial,
|
||||
};
|
||||
return props;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return type;
|
||||
}
|
||||
50
packages/nucleus/src/sn/types.js
Normal file
50
packages/nucleus/src/sn/types.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import type, { clearFromCache } from './type';
|
||||
|
||||
const collection = (name, config) => {
|
||||
const versions = {};
|
||||
|
||||
const get = (version) => {
|
||||
// TODO determine latest version based on semver
|
||||
const v = version || Object.keys(versions).sort().slice(-1);
|
||||
return versions[v];
|
||||
};
|
||||
|
||||
const add = (version, meta) => {
|
||||
if (!versions[version]) {
|
||||
versions[version] = type({
|
||||
name,
|
||||
version,
|
||||
}, meta, config);
|
||||
}
|
||||
return get(version);
|
||||
};
|
||||
|
||||
return {
|
||||
fetch: version => add(version),
|
||||
register: (version, meta) => {
|
||||
if (versions[version]) {
|
||||
throw new Error('Already registered!');
|
||||
}
|
||||
return add(version, meta);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function (config) {
|
||||
const types = {};
|
||||
|
||||
const fetch = (name, version) => {
|
||||
if (!types[name]) {
|
||||
types[name] = collection(name, config);
|
||||
}
|
||||
return types[name].fetch(version);
|
||||
};
|
||||
|
||||
return {
|
||||
fetch,
|
||||
clearFromCache,
|
||||
load: (name, version) => fetch(name, version).load(),
|
||||
supernova: (name, version) => fetch(name, version).supernova(),
|
||||
// instance: (name, version) => fetch(name, version).instance(),
|
||||
};
|
||||
}
|
||||
11
packages/nucleus/src/utils.js
Normal file
11
packages/nucleus/src/utils.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/* eslint import/prefer-default-export: 0 */
|
||||
|
||||
export const prefixer = (list) => {
|
||||
const arr = Array.isArray(list) ? list : list.split(/\s+/);
|
||||
return arr.map((s) => {
|
||||
if (s[0] === '-' || s[0] === '_') {
|
||||
return `nucleus${s}`;
|
||||
}
|
||||
return `nucleus-${s}`;
|
||||
}).join(' ');
|
||||
};
|
||||
53
packages/nucleus/src/viz.js
Normal file
53
packages/nucleus/src/viz.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import EventEmitter from 'node-event-emitter';
|
||||
|
||||
const mixin = (obj) => {
|
||||
/* eslint no-param-reassign: 0 */
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
obj[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
EventEmitter.init(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
export default function ({
|
||||
app,
|
||||
model,
|
||||
cell,
|
||||
} = {}) {
|
||||
const api = {
|
||||
model,
|
||||
app,
|
||||
close() {
|
||||
cell.unmount();
|
||||
},
|
||||
// properties(props) {
|
||||
|
||||
// },
|
||||
// options(options) {
|
||||
// cell.setState({
|
||||
// options,
|
||||
// });
|
||||
// return api;
|
||||
// },
|
||||
// context(context) {
|
||||
// cell.setState({
|
||||
// context,
|
||||
// });
|
||||
// return api;
|
||||
// },
|
||||
set(o) {
|
||||
cell.setState(o);
|
||||
return api;
|
||||
},
|
||||
takeSnapshot() {
|
||||
return cell.takeSnapshot();
|
||||
},
|
||||
show() {
|
||||
return api;
|
||||
},
|
||||
};
|
||||
|
||||
mixin(api);
|
||||
|
||||
return api;
|
||||
}
|
||||
21
packages/selections/package.json
Normal file
21
packages/selections/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@nebula.js/selections",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [],
|
||||
"main": "dist/selections.js",
|
||||
"module": "dist/selections.esm.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production rollup --config ../../rollup.config.js --exports named",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-event-emitter": "0.0.1"
|
||||
}
|
||||
}
|
||||
97
packages/selections/src/app-selections.js
Normal file
97
packages/selections/src/app-selections.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import eventmixin from './event-mixin';
|
||||
|
||||
export default function (app) {
|
||||
let canGoForward = false;
|
||||
let canGoBack = false;
|
||||
let canClear = false;
|
||||
|
||||
let modalObject;
|
||||
const api = {
|
||||
switchModal(object, path, accept = true) {
|
||||
if (object === modalObject) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (modalObject) {
|
||||
modalObject.endSelections(accept);
|
||||
api.emit('modal-unset');
|
||||
modalObject.selections.emit('deactivated');
|
||||
}
|
||||
if (object && object !== null) { // TODO check model state
|
||||
modalObject = object;
|
||||
api.emit('modal', modalObject.selections);
|
||||
return modalObject.beginSelections(Array.isArray(path) ? path : [path]);
|
||||
}
|
||||
modalObject = null;
|
||||
api.emit('modal-unset');
|
||||
return Promise.resolve();
|
||||
},
|
||||
isModal(objectModel) {
|
||||
// TODO check model state
|
||||
return objectModel ? modalObject === objectModel : modalObject !== null;
|
||||
},
|
||||
abortModal(accept = true) {
|
||||
if (!modalObject) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// modalObject.selections.
|
||||
modalObject = null;
|
||||
api.emit('modal-unset');
|
||||
return app.abortModal(accept);
|
||||
},
|
||||
canGoForward() {
|
||||
return canGoForward;
|
||||
},
|
||||
canGoBack() {
|
||||
return canGoBack;
|
||||
},
|
||||
canClear() {
|
||||
return canClear;
|
||||
},
|
||||
forward() {
|
||||
this.switchModal();
|
||||
return app.forward();
|
||||
},
|
||||
back() {
|
||||
this.switchModal();
|
||||
return app.back();
|
||||
},
|
||||
clear() {
|
||||
this.switchModal();
|
||||
return app.clearAll();
|
||||
},
|
||||
};
|
||||
|
||||
eventmixin(api);
|
||||
|
||||
const prom = app.getObject('CurrentSelection');
|
||||
const obj = new Promise((resolve) => {
|
||||
prom.then((sel) => {
|
||||
resolve(sel);
|
||||
}).catch(() => {
|
||||
app.createSessionObject({
|
||||
qInfo: {
|
||||
qId: 'CurrentSelection',
|
||||
qType: 'CurrentSelection',
|
||||
},
|
||||
qSelectionObjectDef: {},
|
||||
}).then((sel) => {
|
||||
resolve(sel);
|
||||
});
|
||||
});
|
||||
});
|
||||
obj.then((model) => {
|
||||
const onChanged = () => model.getLayout().then((layout) => {
|
||||
canGoBack = layout.qSelectionObject && layout.qSelectionObject.qBackCount > 0;
|
||||
canGoForward = layout.qSelectionObject && layout.qSelectionObject.qForwardCount > 0;
|
||||
canClear = layout.qSelectionObject && layout.qSelectionObject.qSelections.length > 0;
|
||||
api.emit('changed');
|
||||
});
|
||||
model.on('changed', onChanged);
|
||||
model.once('closed', () => {
|
||||
model.removeListener('changed', onChanged);
|
||||
});
|
||||
onChanged();
|
||||
});
|
||||
|
||||
return api;
|
||||
}
|
||||
10
packages/selections/src/event-mixin.js
Normal file
10
packages/selections/src/event-mixin.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import EventEmitter from 'node-event-emitter';
|
||||
|
||||
export default function (obj) {
|
||||
/* eslint no-param-reassign: 0 */
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
obj[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
EventEmitter.init(obj);
|
||||
return obj;
|
||||
}
|
||||
44
packages/selections/src/index.js
Normal file
44
packages/selections/src/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/* eslint no-param-reassign: 0 */
|
||||
import createAppSelectionAPI from './app-selections';
|
||||
import createObjectSelectionAPI from './object-selections';
|
||||
|
||||
// MIXINS
|
||||
// const objectSelectionAPI = {
|
||||
// types: ['GenericObject'],
|
||||
// init(args) {
|
||||
// let selections = null;
|
||||
// Object.defineProperty(args.api, 'selections', {
|
||||
// get() {
|
||||
// selections = selections || createObjectSelectionAPI(args.api);
|
||||
// return selections;
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
// };
|
||||
|
||||
// const appSelectionAPI = {
|
||||
// types: ['Doc'],
|
||||
// init(args) {
|
||||
// let selections = null;
|
||||
// Object.defineProperty(args.api, 'selections', {
|
||||
// get() {
|
||||
// selections = selections || createAppSelectionAPI(args.api);
|
||||
// return selections;
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
// };
|
||||
|
||||
// const all = {
|
||||
// types: ['Doc', 'GenericObject'],
|
||||
// init({ api }) {
|
||||
// if (api.getAppLayout) {
|
||||
// api.session.app = api;
|
||||
// }
|
||||
// api.app = api.session.app;
|
||||
// },
|
||||
// };
|
||||
|
||||
// export default [all, objectSelectionAPI, appSelectionAPI];
|
||||
|
||||
export { createObjectSelectionAPI, createAppSelectionAPI };
|
||||
82
packages/selections/src/object-selections.js
Normal file
82
packages/selections/src/object-selections.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import eventmixin from './event-mixin';
|
||||
|
||||
const event = () => {
|
||||
let prevented = false;
|
||||
return {
|
||||
isPrevented: () => prevented,
|
||||
preventDefault: () => {
|
||||
prevented = true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function (model, app) {
|
||||
const appAPI = app.selections;
|
||||
let hasSelected = false;
|
||||
let isActive = false;
|
||||
let layout = {};
|
||||
const api = {
|
||||
id: model.id,
|
||||
setLayout(lyt) {
|
||||
layout = lyt;
|
||||
},
|
||||
begin(paths) {
|
||||
const e = event();
|
||||
this.emit('activate', e);
|
||||
if (e.isPrevented()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
isActive = true;
|
||||
this.emit('activated');
|
||||
return appAPI.switchModal(model, paths, true);
|
||||
},
|
||||
clear() {
|
||||
hasSelected = false;
|
||||
this.emit('cleared');
|
||||
return model.resetMadeSelections();
|
||||
},
|
||||
confirm() {
|
||||
hasSelected = false;
|
||||
isActive = false;
|
||||
this.emit('confirmed');
|
||||
this.emit('deactivated');
|
||||
return appAPI.switchModal(null, null, true);
|
||||
},
|
||||
cancel() {
|
||||
hasSelected = false;
|
||||
isActive = false;
|
||||
this.emit('canceled');
|
||||
this.emit('deactivated');
|
||||
return appAPI.switchModal(null, null, false, false);
|
||||
},
|
||||
select(s) {
|
||||
this.begin([s.params[0]]);
|
||||
if (!appAPI.isModal()) {
|
||||
return;
|
||||
}
|
||||
hasSelected = true;
|
||||
model[s.method](...s.params).then((qSuccess) => {
|
||||
if (!qSuccess) {
|
||||
this.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
canClear() {
|
||||
return hasSelected && layout.qSelectionInfo.qMadeSelections;
|
||||
},
|
||||
canConfirm() {
|
||||
return hasSelected && layout.qSelectionInfo.qMadeSelections;
|
||||
},
|
||||
canCancel() {
|
||||
return true;
|
||||
},
|
||||
isActive: () => isActive,
|
||||
isModal: () => appAPI.isModal(model),
|
||||
goModal: paths => appAPI.switchModal(model, paths, false),
|
||||
noModal: () => appAPI.switchModal(null, null, false),
|
||||
};
|
||||
|
||||
eventmixin(api);
|
||||
|
||||
return api;
|
||||
}
|
||||
40
packages/supernova/__tests__/unit/supernova.spec.js
Normal file
40
packages/supernova/__tests__/unit/supernova.spec.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import supernova from '../../src/index';
|
||||
|
||||
describe('supernova', () => {
|
||||
it('should pass down resources', () => {
|
||||
let res;
|
||||
|
||||
const mySn = {
|
||||
component: {
|
||||
created() {
|
||||
res = this.resources;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const generator = supernova(mySn, {
|
||||
translator: 't',
|
||||
flags: 'f',
|
||||
Promise: 'P',
|
||||
});
|
||||
|
||||
const sn = generator.create({
|
||||
model: {},
|
||||
app: {},
|
||||
});
|
||||
|
||||
sn.component.created();
|
||||
|
||||
expect(res).to.eql({
|
||||
translator: 't',
|
||||
flags: 'f',
|
||||
Promise: 'P',
|
||||
PERMISSIONS: {
|
||||
PASSIVE: 1,
|
||||
INTERACT: 2,
|
||||
SELECT: 4,
|
||||
FETCH: 8,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
21
packages/supernova/package.json
Normal file
21
packages/supernova/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@nebula.js/supernova",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "QlikTech International AB",
|
||||
"keywords": [],
|
||||
"main": "dist/supernova.js",
|
||||
"module": "dist/supernova.esm.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production rollup --config ../../rollup.config.js",
|
||||
"build:dev": "rollup --config ../../rollup.config.js",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nebula.js/selections": "^0.1.0",
|
||||
"extend": "^3.0.2",
|
||||
"node-event-emitter": "^0.0.1"
|
||||
}
|
||||
}
|
||||
62
packages/supernova/src/action-hero.js
Normal file
62
packages/supernova/src/action-hero.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import EventEmitter from 'node-event-emitter';
|
||||
import extend from 'extend';
|
||||
|
||||
const mixin = (obj) => {
|
||||
/* eslint no-param-reassign: 0 */
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
obj[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
EventEmitter.init(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
const actionWrapper = component => (item) => {
|
||||
const wrapped = mixin(extend(true, {}, item, {
|
||||
action() {
|
||||
if (typeof item.action === 'function') {
|
||||
item.action.call(wrapped, component);
|
||||
}
|
||||
wrapped.emit('changed');
|
||||
},
|
||||
enabled() {
|
||||
if (typeof item.enabled === 'function') {
|
||||
return item.enabled.call(wrapped, component);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
active: typeof item.active === 'function' ? function active() {
|
||||
return item.active.call(wrapped, component);
|
||||
} : undefined,
|
||||
}));
|
||||
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
export default function ({
|
||||
sn,
|
||||
component,
|
||||
}) {
|
||||
const actions = {};
|
||||
const selectionToolbarItems = [];
|
||||
const w = actionWrapper(component);
|
||||
((sn.selectionToolbar || {}).items || []).forEach((item) => {
|
||||
const wrapped = w(item);
|
||||
// TODO - check if key exists
|
||||
actions[item.key] = wrapped;
|
||||
selectionToolbarItems.push(wrapped);
|
||||
});
|
||||
|
||||
(sn.actions || []).forEach((item) => {
|
||||
const wrapped = w(item);
|
||||
// TODO - check if key exists
|
||||
actions[item.key] = wrapped;
|
||||
});
|
||||
|
||||
return {
|
||||
actions,
|
||||
selectionToolbarItems,
|
||||
destroy() {
|
||||
selectionToolbarItems.length = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
15
packages/supernova/src/flags.js
Normal file
15
packages/supernova/src/flags.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// dummy capability flags until exposed as require alias in sense-client
|
||||
const FLAGS = {
|
||||
PIE_RADIUS: true,
|
||||
};
|
||||
|
||||
export default function flags() {
|
||||
return {
|
||||
getFlag(f) {
|
||||
if (!(f in FLAGS)) {
|
||||
throw new Error('Oopsie');
|
||||
}
|
||||
return FLAGS[f];
|
||||
},
|
||||
};
|
||||
}
|
||||
158
packages/supernova/src/index.js
Normal file
158
packages/supernova/src/index.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import extend from 'extend';
|
||||
import EventEmitter from 'node-event-emitter';
|
||||
import { createObjectSelectionAPI } from '@nebula.js/selections';
|
||||
|
||||
import translator from './translator';
|
||||
import flags from './flags';
|
||||
import actionhero from './action-hero';
|
||||
import qae from './qae';
|
||||
|
||||
const PERMISSIONS = {
|
||||
PASSIVE: 1, // allowed actions that don't effect the state of the chart (e.g. click on links, mousehover etc)
|
||||
INTERACT: 2, // allowed to click, zoom, scroll, pan etc, (anything that does not affect selections)
|
||||
SELECT: 4, // allowed to select (should never be on if ENGINE is not available)
|
||||
FETCH: 8, // engine is available (might be better to proxy enigma calls in snapshot mode)
|
||||
};
|
||||
|
||||
const defaultComponent = {
|
||||
app: null,
|
||||
model: null,
|
||||
actions: null,
|
||||
created: () => {},
|
||||
mounted: () => {},
|
||||
render: () => {},
|
||||
resize: () => {},
|
||||
willUnmount: () => {},
|
||||
destroy: () => {},
|
||||
|
||||
emit: () => {},
|
||||
getViewState: () => {},
|
||||
|
||||
// temporary
|
||||
setSnapshotData: snapshot => Promise.resolve(snapshot),
|
||||
};
|
||||
|
||||
const reservedKeys = Object.keys(defaultComponent);
|
||||
|
||||
const mixin = (obj) => {
|
||||
/* eslint no-param-reassign: 0 */
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
obj[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
EventEmitter.init(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
function create(sn, opts, env) {
|
||||
const componentInstance = {
|
||||
...defaultComponent,
|
||||
};
|
||||
|
||||
mixin(componentInstance);
|
||||
|
||||
const userInstance = {
|
||||
emit(...args) {
|
||||
componentInstance.emit(...args);
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(sn.component || {}).forEach((key) => {
|
||||
if (reservedKeys.indexOf(key) !== -1) {
|
||||
componentInstance[key] = sn.component[key].bind(userInstance);
|
||||
} else {
|
||||
userInstance[key] = sn.component[key];
|
||||
}
|
||||
});
|
||||
|
||||
const hero = actionhero({
|
||||
sn,
|
||||
component: userInstance,
|
||||
});
|
||||
|
||||
let selections = null;
|
||||
|
||||
if (!opts.selections && !opts.model.selections) {
|
||||
Object.defineProperty(opts.model, 'selections', {
|
||||
get() {
|
||||
selections = selections || createObjectSelectionAPI(opts.model, opts.app);
|
||||
return selections;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
extend(userInstance, {
|
||||
model: opts.model,
|
||||
app: opts.app,
|
||||
selections: opts.selections || opts.model.selections,
|
||||
actions: hero.actions,
|
||||
resources: {
|
||||
translator: env.translator || translator,
|
||||
Promise: env.Promise || Promise,
|
||||
flags: env.flags || flags(),
|
||||
PERMISSIONS,
|
||||
},
|
||||
});
|
||||
|
||||
extend(componentInstance, {
|
||||
actions: hero.actions,
|
||||
});
|
||||
|
||||
// componentInstance.created({
|
||||
// options: snOptions,
|
||||
// });
|
||||
|
||||
return {
|
||||
definition: sn,
|
||||
component: componentInstance,
|
||||
selectionToolbar: {
|
||||
items: hero.selectionToolbarItems,
|
||||
},
|
||||
logicalSize: sn.logicalSize || (() => false),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns
|
||||
*/
|
||||
export default function generator(UserSN, env) {
|
||||
let sn;
|
||||
|
||||
const localEnv = extend({
|
||||
translator,
|
||||
Promise,
|
||||
flags: flags(),
|
||||
PERMISSIONS,
|
||||
}, env);
|
||||
|
||||
if (typeof UserSN === 'function') {
|
||||
sn = UserSN(localEnv);
|
||||
} else {
|
||||
sn = UserSN;
|
||||
}
|
||||
|
||||
const gen = {
|
||||
qae: qae(sn.qae),
|
||||
component: sn.component || {},
|
||||
create({
|
||||
model,
|
||||
app,
|
||||
selections,
|
||||
}) {
|
||||
const ss = create(gen, {
|
||||
model,
|
||||
app,
|
||||
selections,
|
||||
}, localEnv);
|
||||
|
||||
return ss;
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(sn).forEach((key) => {
|
||||
if (!gen[key]) {
|
||||
gen[key] = sn[key];
|
||||
}
|
||||
});
|
||||
|
||||
return gen;
|
||||
}
|
||||
37
packages/supernova/src/qae.js
Normal file
37
packages/supernova/src/qae.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const noop = () => {};
|
||||
|
||||
function fallback(x, value) {
|
||||
if (typeof x === 'undefined') {
|
||||
return () => value;
|
||||
}
|
||||
return () => x;
|
||||
}
|
||||
|
||||
function defFn(def = {}) {
|
||||
return {
|
||||
min: typeof def.min === 'function' ? def.min : fallback(def.min, 0),
|
||||
max: typeof def.max === 'function' ? def.max : fallback(def.max, 1000),
|
||||
add: def.add || noop,
|
||||
};
|
||||
}
|
||||
|
||||
function target(def) {
|
||||
return {
|
||||
path: def.path || '/qHyperCubeDef',
|
||||
dimensions: defFn(def.dimensions),
|
||||
measures: defFn(def.measures),
|
||||
};
|
||||
}
|
||||
|
||||
export default function qae(def = {}) {
|
||||
const q = {
|
||||
properties: {
|
||||
...def.properties,
|
||||
},
|
||||
data: {
|
||||
targets: ((def.data || {}).targets || []).map(target),
|
||||
},
|
||||
};
|
||||
|
||||
return q;
|
||||
}
|
||||
5
packages/supernova/src/translator.js
Normal file
5
packages/supernova/src/translator.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
get(str/* , args */) {
|
||||
return `$$$${str}$$$`;
|
||||
},
|
||||
};
|
||||
102
rollup.config.js
Normal file
102
rollup.config.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const path = require('path');
|
||||
const nodeResolve = require('rollup-plugin-node-resolve');
|
||||
const commonjs = require('rollup-plugin-commonjs');
|
||||
const babel = require('rollup-plugin-babel');
|
||||
const replace = require('rollup-plugin-replace');
|
||||
const sass = require('rollup-plugin-sass');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
|
||||
const cwd = process.cwd();
|
||||
const pkg = require(path.join(cwd, 'package.json')); // eslint-disable-line
|
||||
const {
|
||||
name,
|
||||
version,
|
||||
license,
|
||||
} = pkg;
|
||||
|
||||
const banner = `/*
|
||||
* ${name} v${version}
|
||||
* Copyright (c) ${new Date().getFullYear()} QlikTech International AB
|
||||
* Released under the ${license} license.
|
||||
*/
|
||||
`;
|
||||
|
||||
const GLOBALS = {
|
||||
};
|
||||
|
||||
// const watch = process.argv.indexOf('-w') > 2;
|
||||
|
||||
const config = (isEsm) => {
|
||||
const outputFile = isEsm ? pkg.module : pkg.main;
|
||||
const basename = path.basename(outputFile);
|
||||
const dir = path.dirname(outputFile);
|
||||
const umdName = basename.replace(/-([a-z])/g, (m, p1) => p1.toUpperCase()).split('.js').join('');
|
||||
|
||||
const external = isEsm ? Object.keys(pkg.dependencies || {}) : [];
|
||||
const globals = {};
|
||||
external.forEach((e) => {
|
||||
if ([GLOBALS[e]]) {
|
||||
globals[e] = GLOBALS[e];
|
||||
}
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
input: path.resolve(cwd, 'src', 'index'),
|
||||
output: {
|
||||
file: path.resolve(dir, basename),
|
||||
format: isEsm ? 'esm' : 'umd',
|
||||
exports: 'default',
|
||||
name: umdName,
|
||||
sourcemap: true,
|
||||
banner,
|
||||
globals,
|
||||
},
|
||||
external,
|
||||
plugins: [
|
||||
replace({
|
||||
'process.env.NODE_ENV': JSON.stringify(isEsm ? 'development' : 'production'),
|
||||
}),
|
||||
sass({
|
||||
insert: true,
|
||||
}),
|
||||
nodeResolve({
|
||||
extensions: ['.js', '.jsx'],
|
||||
}),
|
||||
commonjs(),
|
||||
babel({
|
||||
babelrc: false,
|
||||
// include: [
|
||||
// 'src/**',
|
||||
// // /react-leonardo-ui/
|
||||
// ],
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
modules: false,
|
||||
targets: {
|
||||
browsers: ['ie 11', 'chrome 47'],
|
||||
},
|
||||
}],
|
||||
],
|
||||
plugins: [
|
||||
['@babel/plugin-transform-react-jsx', { pragma: 'preact.h' }],
|
||||
],
|
||||
}),
|
||||
// deps(),
|
||||
],
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && !isEsm) {
|
||||
cfg.plugins.push(terser({
|
||||
output: {
|
||||
preamble: banner,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
config(),
|
||||
config(true),
|
||||
].filter(Boolean);
|
||||
Reference in New Issue
Block a user