mirror of
https://github.com/apache/zeppelin
synced 2026-05-24 09:38:26 +00:00
feat: Automated display type checking in result
This commit is contained in:
parent
5810bf1de5
commit
5c49e6e3d3
4 changed files with 213 additions and 46 deletions
|
|
@ -28,22 +28,6 @@ export class AbstractFrontendInterpreter {
|
|||
this.displayType = displayType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param paragraphText which includes magic
|
||||
* @returns {string} if magic exists, otherwise return undefined
|
||||
*/
|
||||
static extractMagic(allParagraphText) {
|
||||
const intpNameRegexp = /^\s*%(\S+)\s*/g;
|
||||
try {
|
||||
let match = intpNameRegexp.exec(allParagraphText);
|
||||
if (match) { return `%${match[1].trim()}`; }
|
||||
} catch (error) {
|
||||
// failed to parse, ignore
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static useInterpret(interpreter) {
|
||||
return (interpreter.__proto__.hasOwnProperty('interpret'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,35 +23,116 @@ export const DefaultDisplayType = {
|
|||
TEXT: 'TEXT',
|
||||
};
|
||||
|
||||
export class FrontendInterpreterResult {
|
||||
constructor(resultDisplayType, resultDataGenerator) {
|
||||
/**
|
||||
* backend interpreter uses `data` and `type` as field names.
|
||||
* let's use the same field to keep consistency.
|
||||
*/
|
||||
this.type = resultDisplayType;
|
||||
this.data = resultDataGenerator;
|
||||
export const DefaultDisplayMagic = {
|
||||
'%element': DefaultDisplayType.ELEMENT,
|
||||
'%table': DefaultDisplayType.TABLE,
|
||||
'%html': DefaultDisplayType.HTML,
|
||||
'%angular': DefaultDisplayType.ANGULAR,
|
||||
'%text': DefaultDisplayType.TEXT,
|
||||
};
|
||||
|
||||
export class GeneratorWithType {
|
||||
constructor(generator, type) {
|
||||
// use variable name `data` to keep consistency with backend
|
||||
this.data = generator;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static isFunctionGenerator(generator) {
|
||||
return (generator && typeof generator === 'function');
|
||||
static handleDefaultMagic(m) {
|
||||
// let's use default display type instead of magic in case of default
|
||||
// to keep consistency with backend interpreter
|
||||
if (DefaultDisplayMagic[m]) {
|
||||
return DefaultDisplayMagic[m];
|
||||
} else {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
static isPromiseGenerator(generator) {
|
||||
return (generator && typeof generator.then === 'function');
|
||||
}
|
||||
static parseStringGenerator(generator, customDisplayMagic) {
|
||||
function availableMagic(magic) {
|
||||
return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]);
|
||||
}
|
||||
|
||||
static isObjectGenerator(generator) {
|
||||
return (
|
||||
!FrontendInterpreterResult.isFunctionGenerator(generator) &&
|
||||
!FrontendInterpreterResult.isPromiseGenerator(generator));
|
||||
const splited = generator.split("\n");
|
||||
|
||||
const gensWithTypes = [];
|
||||
let mergedGens = [];
|
||||
let previousMagic = DefaultDisplayType.TEXT;
|
||||
|
||||
// create `GeneratorWithType` whenever see available display type.
|
||||
for(let i = 0; i < splited.length; i++) {
|
||||
const g = splited[i];
|
||||
const magic = FrontendInterpreterResult.extractMagic(g);
|
||||
|
||||
// create `GeneratorWithType` only if see new magic
|
||||
if (availableMagic(magic) && mergedGens.length > 0) {
|
||||
gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic));
|
||||
mergedGens = [];
|
||||
}
|
||||
|
||||
// accumulate `generator` to mergedGens
|
||||
if (availableMagic(magic)) {
|
||||
const withoutMagic = g.split(magic)[1];
|
||||
mergedGens.push(`${withoutMagic}\n`);
|
||||
previousMagic = GeneratorWithType.handleDefaultMagic(magic);
|
||||
} else {
|
||||
mergedGens.push(`${g}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup the last `GeneratorWithType`
|
||||
if (mergedGens.length > 0) {
|
||||
previousMagic = GeneratorWithType.handleDefaultMagic(previousMagic);
|
||||
gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic));
|
||||
}
|
||||
|
||||
return gensWithTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} display type for this frontend interpreter result.
|
||||
* get 1 `GeneratorWithType` and produce multiples using available displays
|
||||
* return an wrapped with a promise to generalize result output which can be
|
||||
* object, function or promise
|
||||
* @param generatorWithType {GeneratorWithType}
|
||||
* @param availableDisplays {Object} Map for available displays
|
||||
* @return {Promise<Array<GeneratorWithType>>}
|
||||
*/
|
||||
getType() {
|
||||
return this.type;
|
||||
static produceMultipleGenerator(generatorWithType, customDisplayType) {
|
||||
const generator = generatorWithType.getGenerator();
|
||||
const type = generatorWithType.getType();
|
||||
|
||||
// if the type is specified, just return it
|
||||
// handle non-specified generatorWithTypes only
|
||||
if (type) {
|
||||
return new Promise((resolve) => { resolve([generatorWithType]); });
|
||||
}
|
||||
|
||||
let wrapped;
|
||||
|
||||
if (FrontendInterpreterResult.isFunctionGenerator(generator)) {
|
||||
// if generator is a function, we consider it as ELEMENT type.
|
||||
wrapped = new Promise((resolve) => {
|
||||
const result = [new GeneratorWithType(generator, DefaultDisplayType.ELEMENT)];
|
||||
return resolve(result);
|
||||
});
|
||||
} else if (FrontendInterpreterResult.isPromiseGenerator(generator)) {
|
||||
// if generator is a promise,
|
||||
wrapped = generator.then(generated => {
|
||||
const result =
|
||||
GeneratorWithType.parseStringGenerator(generated, customDisplayType);
|
||||
return result;
|
||||
})
|
||||
|
||||
} else {
|
||||
// if generator is a object, parse it to multiples
|
||||
wrapped = new Promise((resolve) => {
|
||||
const result =
|
||||
GeneratorWithType.parseStringGenerator(generator, customDisplayType);
|
||||
return resolve(result);
|
||||
});
|
||||
}
|
||||
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,7 +144,91 @@ export class FrontendInterpreterResult {
|
|||
* will be called in `then()` of this promise.
|
||||
* @returns {*} `data` which can be object, function or promise.
|
||||
*/
|
||||
getData() {
|
||||
getGenerator() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value of `type` might be empty which means
|
||||
* generator can be splited into multiple generators
|
||||
* by `FrontendInterpreterResult.parseMultipleGenerators()`
|
||||
* @returns {string}
|
||||
*/
|
||||
getType() {
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
|
||||
export class FrontendInterpreterResult {
|
||||
constructor(resultGenerator, resultType) {
|
||||
this.generatorsWithTypes = [];
|
||||
this.add(resultGenerator, resultType);
|
||||
}
|
||||
|
||||
static isFunctionGenerator(generator) {
|
||||
return (generator && typeof generator === 'function');
|
||||
}
|
||||
|
||||
static isPromiseGenerator(generator) {
|
||||
return (generator && typeof generator.then === 'function');
|
||||
}
|
||||
|
||||
static isObjectGenerator(generator) {
|
||||
return (generator &&
|
||||
!FrontendInterpreterResult.isFunctionGenerator(generator) &&
|
||||
!FrontendInterpreterResult.isPromiseGenerator(generator));
|
||||
}
|
||||
|
||||
static extractMagic(allParagraphText) {
|
||||
const intpNameRegexp = /^\s*%(\S+)\s*/g;
|
||||
try {
|
||||
let match = intpNameRegexp.exec(allParagraphText);
|
||||
if (match) {
|
||||
return `%${match[1].trim()}`;
|
||||
}
|
||||
} catch (error) {
|
||||
// failed to parse, ignore
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* consume 1 generator and produce multiple
|
||||
* @param generator {string}
|
||||
* @param customDisplayType
|
||||
* @return {Array<GeneratorWithType>}
|
||||
*/
|
||||
|
||||
add(resultGenerator, resultType) {
|
||||
if (resultGenerator) {
|
||||
this.generatorsWithTypes.push(
|
||||
new GeneratorWithType(resultGenerator, resultType));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param customDisplayType
|
||||
* @return {Promise<Array<GeneratorWithType>>}
|
||||
*/
|
||||
getAllParsedGeneratorsWithTypes(customDisplayType) {
|
||||
const promises = this.generatorsWithTypes.map(gt => {
|
||||
return GeneratorWithType.produceMultipleGenerator(gt, customDisplayType);
|
||||
});
|
||||
|
||||
// some promises can include an array so we need to flatten them
|
||||
const flatten = Promise.all(promises).then(values => {
|
||||
return values.reduce((acc, cur) => {
|
||||
if (Array.isArray(cur)) {
|
||||
return acc.concat(cur);
|
||||
} else {
|
||||
return acc.concat([cur]);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return flatten;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AbstractFrontendInterpreter } from '../../frontend-interpreter'
|
||||
import { FrontendInterpreterResult } from '../../frontend-interpreter'
|
||||
|
||||
angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl);
|
||||
|
||||
|
|
@ -225,9 +225,16 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
websocketMsgSrv.cancelParagraphRun(paragraph.id);
|
||||
};
|
||||
|
||||
$scope.handleFrontendInterpreterError = function(error) {
|
||||
$scope.paragraph.status = 'ERROR';
|
||||
$scope.paragraph.errorMessage = error.stack;
|
||||
console.error('Failed to execute FrontendInterpreter.interpret\n', error);
|
||||
$scope.$digest();
|
||||
}
|
||||
|
||||
$scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, magic) {
|
||||
// clear results which is watched by `ng-repeat`. without this,
|
||||
// frontend interpreter results cannot be rendered in view twice more
|
||||
// frontend interpreter results cannot be rendered in view more than once
|
||||
$scope.paragraph.results = {};
|
||||
$scope.$digest();
|
||||
|
||||
|
|
@ -236,13 +243,17 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
const splited = paragraphText.split(magic);
|
||||
const textWithoutMagic = splited[1];
|
||||
$scope.paragraph.status = 'FINISHED';
|
||||
$scope.paragraph.results.msg = intp.interpret(textWithoutMagic);
|
||||
$scope.paragraph.config.tableHide = false;
|
||||
const frontIntpResult = intp.interpret(textWithoutMagic);
|
||||
const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes(
|
||||
heliumService.getAvailableFrontendInterpreterDisplay());
|
||||
parsed.then(resultsMsg => {
|
||||
$scope.paragraph.results.msg = resultsMsg;
|
||||
$scope.paragraph.config.tableHide = false;
|
||||
$scope.$digest();
|
||||
}).catch($scope.handleFrontendInterpreterError);
|
||||
// TODO(1ambda): broadcast to other clients
|
||||
} catch (error) {
|
||||
$scope.paragraph.status = 'ERROR';
|
||||
$scope.paragraph.errorMessage = error.stack;
|
||||
console.error('Failed to execute FrontendInterpreter.interpret\n', error);
|
||||
$scope.handleFrontendInterpreterError(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -271,7 +282,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
return;
|
||||
}
|
||||
|
||||
const magic = AbstractFrontendInterpreter.extractMagic(paragraphText);
|
||||
const magic = FrontendInterpreterResult.extractMagic(paragraphText);
|
||||
const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(magic);
|
||||
|
||||
if (frontendIntp) {
|
||||
|
|
|
|||
|
|
@ -81,10 +81,17 @@ import {
|
|||
* @param displayType {string} e.g `ELEMENT`
|
||||
* @returns {FrontendInterpreterBase} undefined for non-available displayType
|
||||
*/
|
||||
this.getFrontendInterpreterWithDisplayType = function(displayType) {
|
||||
this.getFrontendInterpreterUsingDisplayType = function(displayType) {
|
||||
return frontendIntpWithDisplayType[displayType];
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Object}
|
||||
*/
|
||||
this.getAvailableFrontendInterpreterDisplay = function() {
|
||||
return frontendIntpWithDisplayType;
|
||||
}
|
||||
|
||||
this.getVisualizationBundles = function() {
|
||||
return visualizationBundles;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue