Skip to content

Commit

Permalink
feat: simple snapshooter (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
miralemd committed Dec 11, 2019
1 parent 326465d commit cf717a5
Show file tree
Hide file tree
Showing 29 changed files with 1,000 additions and 199 deletions.
8 changes: 8 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ jobs:
name: Test component
command: yarn run test:component --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch

- run:
name: Test mashup
command: yarn run test:mashup --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch

- run:
name: Test integration
command: APP_ID=$DOC_ID yarn run test:integration --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch
Expand All @@ -93,5 +97,9 @@ jobs:
cd generated/barchart
yarn run build
APP_ID=$DOC_ID yarn run test:integration --mocha.timeout 30000 --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch
- store_artifacts:
path: generated/barchart/screenshots

- store_artifacts:
path: test/mashup/__artifacts__
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ node_modules/
coverage/
dist/
temp/
test/**/__artifacts__/regression
test/**/__artifacts__/diff
3 changes: 3 additions & 0 deletions apis/locale/src/translator.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export default function translator({ initial = 'en-US', fallback = 'en-US' } = {
* @interface Translator
*/
const api = {
language: () => {
return currentLocale;
},
/**
* Register a string in multiple locales
* @param {object} item
Expand Down
29 changes: 26 additions & 3 deletions apis/nucleus/src/components/Cell.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { forwardRef, useImperativeHandle, useEffect, useState, useContext, useReducer } from 'react';
import React, { forwardRef, useImperativeHandle, useEffect, useState, useContext, useReducer, useRef } from 'react';

import { Grid, Paper } from '@material-ui/core';
import { useTheme } from '@nebula.js/ui/theme';
Expand Down Expand Up @@ -170,6 +170,7 @@ const Cell = forwardRef(({ nebulaContext, model, initialSnContext, initialSnOpti
const [contentRef, contentRect, , contentNode] = useRect();
const [snContext, setSnContext] = useState(initialSnContext);
const [snOptions, setSnOptions] = useState(initialSnOptions);
const cellRef = useRef();

useEffect(() => {
const validate = sn => {
Expand Down Expand Up @@ -231,7 +232,7 @@ const Cell = forwardRef(({ nebulaContext, model, initialSnContext, initialSnOpti
() => ({
setSnContext,
setSnOptions,
takeSnapshot: async () => {
async takeSnapshot() {
const snapshot = {
...layout,
snapshotData: {
Expand All @@ -248,8 +249,29 @@ const Cell = forwardRef(({ nebulaContext, model, initialSnContext, initialSnOpti
}
return snapshot;
},
async exportImage() {
if (!nebulaContext.snapshot.capture) {
throw new Error('Nebula has not been configured with snapshot.capture');
}
const lyt = await this.takeSnapshot(); // eslint-disable-line
const { width, height } = cellRef.current.getBoundingClientRect();
const s = {
meta: {
language: translator.language(),
theme: theme.name,
// direction: 'ltr',
size: {
width: Math.round(width),
height: Math.round(height),
},
},
layout: lyt,
};

return nebulaContext.snapshot.capture(s);
},
}),
[state.sn, contentRect, layout]
[state.sn, contentRect, layout, theme.name]
);
// console.log('content', state);
let Content = null;
Expand All @@ -276,6 +298,7 @@ const Cell = forwardRef(({ nebulaContext, model, initialSnContext, initialSnOpti
elevation={0}
square
className="nebulajs-cell"
ref={cellRef}
>
<Grid
container
Expand Down
24 changes: 24 additions & 0 deletions apis/nucleus/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ const DEFAULT_CONFIG = {
themes: [],
/** */
env: {},

snapshot: {
get: async id => {
const res = await fetch(`/njs/snapshot/${id}`);
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json();
},
capture(payload) {
return fetch(`/njs/capture`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
}).then(res => res.json());
},
},
};

const mergeConfigs = (base, c) => ({
Expand All @@ -57,6 +76,9 @@ const mergeConfigs = (base, c) => ({
locale: {
language: (c.locale ? c.locale.language : '') || base.locale.language,
},
snapshot: {
...(c.snapshot || base.snapshot),
},
types: [
// TODO - filter to avoid duplicates
...(base.types || []),
Expand Down Expand Up @@ -143,6 +165,7 @@ function nuked(configuration = {}, prev = {}) {
types,
root,
theme: appTheme.externalAPI,
snapshot: configuration.snapshot,
};

let currentThemePromise = appTheme.setTheme(configuration.theme);
Expand Down Expand Up @@ -251,6 +274,7 @@ function nuked(configuration = {}, prev = {}) {
* nucleus(app).create({ type: 'mekko' }); // will throw error since 'mekko' is not a register type on the default instance
*/
nucleus.configured = c => nuked(mergeConfigs(configuration, c));
nucleus.config = configuration;

return nucleus;
}
Expand Down
4 changes: 4 additions & 0 deletions apis/nucleus/src/viz.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ export default function viz({ model, context: nebulaContext } = {}) {
return api;
},
takeSnapshot() {
// TODO - decide if this method is useful at all
return cellRef.current.takeSnapshot();
},
exportImage(settings) {
return cellRef.current.exportImage(settings);
},

// QVisualization API
// close() {},
Expand Down
80 changes: 80 additions & 0 deletions apis/snapshooter/lib/snapshooter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint no-param-reassign: 0 */

const puppeteer = require('puppeteer');

function snapshooter({ snapshotUrl, chrome = {} } = {}) {
const snapshots = {};
const images = {};
let browser;
let cachedPage;
let rendering = false;

const hasConnectOptions = chrome.browserWSEndpoint || chrome.browserURL;

const createBrowser = () => (hasConnectOptions ? puppeteer.connect(chrome) : puppeteer.launch(chrome));

const doIt = async (snapshot, runCached) => {
browser = browser || (await createBrowser());
let page;
if (runCached) {
cachedPage = cachedPage || (await browser.newPage());
page = cachedPage;
} else {
page = await browser.newPage();
}
await page.setViewport({
width: snapshot.meta.size.width,
height: snapshot.meta.size.height,
});
await page.goto(`${snapshotUrl}?snapshot=${snapshot.key}`);
try {
await page.waitFor(
() =>
(document.querySelector('.nebulajs-sn') &&
+document.querySelector('.nebulajs-sn').getAttribute('data-render-count') > 0) ||
document.querySelector('[data-njs-error]')
);
} catch (e) {
// empty
}
const image = await page.screenshot();
images[snapshot.key] = image;
rendering = false;
return snapshot.key;
};

return {
getStoredImage(id) {
return images[id];
},
getStoredSnapshot(id) {
return snapshots[id];
},
storeSnapshot(snapshot) {
if (!snapshot) {
throw new Error('Empty snapshot');
}
if (!snapshot.key) {
snapshot.key = String(+Date.now());
}
snapshots[snapshot.key] = snapshot;
return snapshot.key;
},
async captureImageOfSnapshot(snapshot) {
this.storeSnapshot(snapshot);
if (rendering) {
return doIt(snapshot);
}
rendering = true;
const key = await doIt(snapshot, true);
rendering = false;
return key;
},
async close() {
if (browser) {
await browser.close();
}
},
};
}
module.exports = snapshooter;
32 changes: 32 additions & 0 deletions apis/snapshooter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@nebula.js/snapshooter",
"version": "0.1.0-alpha.25",
"description": "",
"license": "MIT",
"author": "QlikTech International AB",
"keywords": [
"qlik",
"nebula",
"supernova",
"snapshot"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/qlik-oss/nebula.js.git"
},
"main": "lib/snapshooter.js",
"files": [
"lib",
"dist"
],
"scripts": {
"build": "cross-env NODE_ENV=production rollup --config ./rollup.config.js",
"prepublishOnly": "rm -rf dist && yarn run build"
},
"dependencies": {
"puppeteer": "2.0.0"
}
}
61 changes: 61 additions & 0 deletions apis/snapshooter/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const path = require('path');
const babel = require('rollup-plugin-babel'); // eslint-disable-line
const { terser } = require('rollup-plugin-terser'); // eslint-disable-line

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 browserList = [
'last 2 Chrome versions',
'last 2 Firefox versions',
'last 2 Edge versions',
'Safari >= 10.0',
'iOS >= 11.2',
];

const cfg = {
input: path.resolve(cwd, 'src', 'renderer'),
output: {
file: path.resolve(cwd, 'dist/renderer.js'),
format: 'umd',
exports: 'default',
name: 'snapshooter',
banner,
},
plugins: [
babel({
babelrc: false,
presets: [
[
'@babel/preset-env',
{
modules: false,
targets: {
browsers: [...browserList, ...['ie 11', 'chrome 47']],
},
},
],
],
}),
],
};

if (process.env.NODE_ENV === 'production') {
cfg.plugins.push(
terser({
output: {
preamble: banner,
},
})
);
}

module.exports = [cfg].filter(Boolean);
Loading

0 comments on commit cf717a5

Please sign in to comment.