mirror of
https://github.com/apache/zeppelin
synced 2026-05-24 09:38:26 +00:00
feat: Support custom display
This commit is contained in:
parent
c906da6230
commit
0fa7eda609
6 changed files with 108 additions and 78 deletions
|
|
@ -23,9 +23,8 @@ import {
|
|||
/*eslint-enable no-unused-vars */
|
||||
|
||||
export class AbstractFrontendInterpreter {
|
||||
constructor(magic, displayType) {
|
||||
constructor(magic) {
|
||||
this.magic = magic;
|
||||
this.displayType = displayType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,6 +36,7 @@ export class AbstractFrontendInterpreter {
|
|||
*/
|
||||
interpret(paragraphText) {
|
||||
/** implement this if you want to add a frontend interpreter */
|
||||
throw new Error('AbstractFrontendInterpreter.interpret is not overrided');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,13 +47,4 @@ export class AbstractFrontendInterpreter {
|
|||
getMagic() {
|
||||
return this.magic;
|
||||
}
|
||||
|
||||
/**
|
||||
* return display type for this frontend interpreter.
|
||||
* (e.g `DefaultDisplayType.TEXT`)
|
||||
* @return {string}
|
||||
*/
|
||||
getDisplayType() {
|
||||
return this.displayType;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class GeneratorWithType {
|
|||
return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]);
|
||||
}
|
||||
|
||||
const splited = generator.split("\n");
|
||||
const splited = generator.split('\n');
|
||||
|
||||
const gensWithTypes = [];
|
||||
let mergedGens = [];
|
||||
|
|
|
|||
|
|
@ -230,18 +230,18 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
$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 more than once
|
||||
$scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText,
|
||||
magic, digestRequired) {
|
||||
$scope.paragraph.results = {};
|
||||
$scope.$digest();
|
||||
if (digestRequired) { $scope.$digest(); }
|
||||
|
||||
try {
|
||||
// remove magic from paragraphText
|
||||
const splited = paragraphText.split(magic);
|
||||
const textWithoutMagic = splited[1];
|
||||
// remove leading spaces
|
||||
const textWithoutMagic = splited[1].replace(/^\s+/g, '');
|
||||
$scope.paragraph.status = 'FINISHED';
|
||||
const frontIntpResult = intp.interpret(textWithoutMagic);
|
||||
const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes(
|
||||
|
|
@ -249,9 +249,8 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
parsed.then(resultsMsg => {
|
||||
$scope.paragraph.results.msg = resultsMsg;
|
||||
$scope.paragraph.config.tableHide = false;
|
||||
$scope.$digest();
|
||||
if (digestRequired) { $scope.$digest(); }
|
||||
}).catch($scope.handleFrontendInterpreterError);
|
||||
// TODO(1ambda): broadcast to other clients
|
||||
} catch (error) {
|
||||
$scope.handleFrontendInterpreterError(error);
|
||||
}
|
||||
|
|
@ -277,7 +276,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
commitParagraph(paragraph);
|
||||
};
|
||||
|
||||
$scope.runParagraph = function(paragraphText) {
|
||||
$scope.runParagraph = function(paragraphText, digestRequired) {
|
||||
if (!paragraphText || $scope.isRunning($scope.paragraph)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -286,7 +285,8 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(magic);
|
||||
|
||||
if (frontendIntp) {
|
||||
$scope.runParagraphUsingFrontendInterpreter(frontendIntp, paragraphText, magic);
|
||||
$scope.runParagraphUsingFrontendInterpreter(
|
||||
frontendIntp, paragraphText, magic, digestRequired);
|
||||
} else {
|
||||
$scope.runParagraphUsingBackendInterpreter(paragraphText);
|
||||
}
|
||||
|
|
@ -306,14 +306,14 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
|
|||
};
|
||||
|
||||
$scope.runParagraphFromShortcut = function(paragraphText) {
|
||||
// we need to call `$digest()` to update view immediately
|
||||
$scope.runParagraph(paragraphText);
|
||||
$scope.$digest();
|
||||
// passing `digestRequired` as true to update view immediately
|
||||
// without this, results cannot be rendered in view more than once
|
||||
$scope.runParagraph(paragraphText, true);
|
||||
};
|
||||
|
||||
$scope.runParagraphFromButton = function(paragraphText) {
|
||||
// we come here from `$scope.on`, so we don't need to call `$digest()`
|
||||
$scope.runParagraph(paragraphText)
|
||||
$scope.runParagraph(paragraphText, false)
|
||||
};
|
||||
|
||||
$scope.moveUp = function(paragraph) {
|
||||
|
|
|
|||
|
|
@ -252,8 +252,40 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location
|
|||
}
|
||||
};
|
||||
|
||||
var renderResult = function(type, refresh) {
|
||||
var activeApp;
|
||||
$scope.createDisplayDOMId = function(baseDOMId, type) {
|
||||
if (type === DefaultDisplayType.TABLE) {
|
||||
return `${baseDOMId}_graph`;
|
||||
} else if (type === DefaultDisplayType.HTML) {
|
||||
return `${baseDOMId}_html`;
|
||||
} else if (type === DefaultDisplayType.ANGULAR) {
|
||||
return `${baseDOMId}_angular`;
|
||||
} else if (type === DefaultDisplayType.TEXT) {
|
||||
return `${baseDOMId}_text`;
|
||||
} else if (type === DefaultDisplayType.ELEMENT) {
|
||||
return `${baseDOMId}_elem`;
|
||||
} else {
|
||||
console.error(`Cannot create display DOM Id due to unknown display type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.renderDefaultDisplay = function(targetElemId, type, data, refresh) {
|
||||
if (type === DefaultDisplayType.TABLE) {
|
||||
$scope.renderGraph(targetElemId, $scope.graphMode, refresh);
|
||||
} else if (type === DefaultDisplayType.HTML) {
|
||||
renderHtml(targetElemId, data);
|
||||
} else if (type === DefaultDisplayType.ANGULAR) {
|
||||
renderAngular(targetElemId, data);
|
||||
} else if (type === DefaultDisplayType.TEXT) {
|
||||
renderText(targetElemId, data);
|
||||
} else if (type === DefaultDisplayType.ELEMENT) {
|
||||
renderElem(targetElemId, data);
|
||||
} else {
|
||||
console.error(`Unknown Display Type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const renderResult = function(type, refresh) {
|
||||
let activeApp;
|
||||
if (enableHelium) {
|
||||
getSuggestions();
|
||||
getApplicationStates();
|
||||
|
|
@ -261,50 +293,64 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location
|
|||
}
|
||||
|
||||
if (activeApp) {
|
||||
const app = _.find($scope.apps, {id: activeApp});
|
||||
renderApp(app, `p${appState.id}`);
|
||||
const appState = _.find($scope.apps, {id: activeApp});
|
||||
renderApp(`p${appState.id}`, appState);
|
||||
} else {
|
||||
|
||||
/**
|
||||
* find proper interpreter which can handle the non-default display type
|
||||
* the displayed type which is generated from `display()`
|
||||
* should be one of the default interpreter type
|
||||
*/
|
||||
// const isDefaultDisplayType = DefaultDisplayType[type];
|
||||
// if (!isDefaultDisplayType) {
|
||||
// const intp = heliumService.getFrontendInterpreterWithDisplayType(type);
|
||||
//
|
||||
// if (intp) {
|
||||
// // currently display only accepts `Object` data not
|
||||
// // doesn't support function and promise
|
||||
// const result = intp.display(data);
|
||||
// type = result.getType();
|
||||
// data = result.getData();
|
||||
// }
|
||||
// }
|
||||
|
||||
if (type === DefaultDisplayType.TABLE) {
|
||||
$scope.renderGraph($scope.graphMode, refresh);
|
||||
} else if (type === DefaultDisplayType.HTML) {
|
||||
renderHtml(`p${$scope.id}_html`, data);
|
||||
} else if (type === DefaultDisplayType.ANGULAR) {
|
||||
renderAngular(`p${$scope.id}_angular`, data);
|
||||
} else if (type === DefaultDisplayType.TEXT) {
|
||||
renderText(`p${$scope.id}_text`, data);
|
||||
} else if (type === DefaultDisplayType.ELEMENT) {
|
||||
renderElem(`p${$scope.id}_elem`, data);
|
||||
if (!DefaultDisplayType[type]) {
|
||||
const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(type);
|
||||
if (!frontendIntp) {
|
||||
console.error(`Unknown Display Type: ${type}`);
|
||||
return;
|
||||
}
|
||||
$scope.renderCustomDisplay(type, data, frontendIntp);
|
||||
} else {
|
||||
console.error(`Unknown Display Type: ${type}`);
|
||||
const targetElemId = $scope.createDisplayDOMId(`p${$scope.id}`, type);
|
||||
$scope.renderDefaultDisplay(targetElemId, type, data, refresh);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isDefaultDisplay = function() {
|
||||
return DefaultDisplayType[$scope.type];
|
||||
};
|
||||
|
||||
/**
|
||||
* Render multiple sub results for custom display
|
||||
*/
|
||||
$scope.renderCustomDisplay = function(type, data, frontendIntp) {
|
||||
// get result from intp
|
||||
|
||||
const frontIntpResult = frontendIntp.interpret(data.trim());
|
||||
const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes(
|
||||
heliumService.getAvailableFrontendInterpreters());
|
||||
|
||||
// custom display result can include multiple subset results
|
||||
parsed.then(dataWithTypes => {
|
||||
const containerDOM = document.getElementById(`p${$scope.id}_custom`);
|
||||
for(let i = 0; i < dataWithTypes.length; i++) {
|
||||
const dt = dataWithTypes[i];
|
||||
const data = dt.data;
|
||||
const type = dt.type;
|
||||
|
||||
// prepare DOM to be filled
|
||||
const subResultDOMId = $scope.createDisplayDOMId(`p${$scope.id}_custom`, type);
|
||||
const subResultDOM = document.createElement('div');
|
||||
containerDOM.appendChild(subResultDOM);
|
||||
subResultDOM.setAttribute('id', subResultDOMId);
|
||||
|
||||
$scope.renderDefaultDisplay(subResultDOMId, type, data, true);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(`Failed to render custom display: ${$scope.type}\n` + error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* generates actually object which will be consumed from `data` property
|
||||
* feed it to the success callback.
|
||||
* if error occurs, the error is passed to the failure callback
|
||||
*
|
||||
* @param generator {Object or Promise or Function}
|
||||
* @param generator {Object or Function}
|
||||
* @param type {string} Display Type
|
||||
* @param successCallback
|
||||
* @param failureCallback
|
||||
|
|
@ -317,13 +363,6 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location
|
|||
failureCallback(error);
|
||||
console.error(`Failed to handle ${type} type, function generator\n`, error);
|
||||
}
|
||||
} else if (FrontendInterpreterResult.isPromiseGenerator(generator)) {
|
||||
generator
|
||||
.then((generated) => { successCallback(generated); })
|
||||
.catch((error) => {
|
||||
failureCallback(error);
|
||||
console.error(`Failed to handle ${type} type, promise generator\n`, error);
|
||||
});
|
||||
} else if (FrontendInterpreterResult.isObjectGenerator(generator)) {
|
||||
try {
|
||||
successCallback(generator);
|
||||
|
|
@ -343,7 +382,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location
|
|||
|
||||
generateData(() => { generator(targetElemId) }, DefaultDisplayType.ELEMENT,
|
||||
() => {}, /** HTML element will be filled in generator . thus pass empty success callback */
|
||||
(error) => { elem.html(`${error.stack}`); }
|
||||
(error) => { elem.html(`${error.stack}`); }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +460,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location
|
|||
}
|
||||
|
||||
$timeout(retryRenderer);
|
||||
}
|
||||
};
|
||||
|
||||
var clearTextOutput = function() {
|
||||
var textEl = getTextResultElem($scope.id);
|
||||
|
|
@ -457,11 +496,11 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location
|
|||
}
|
||||
};
|
||||
|
||||
$scope.renderGraph = function(type, refresh) {
|
||||
$scope.renderGraph = function(targetElemId, type, refresh) {
|
||||
// set graph height
|
||||
var height = $scope.config.graph.height;
|
||||
var graphContainerEl = angular.element('#p' + $scope.id + '_graph');
|
||||
graphContainerEl.height(height);
|
||||
var targetElem = angular.element(`#${targetElemId}`);
|
||||
targetElem.height(height);
|
||||
|
||||
if (!type) {
|
||||
type = 'table';
|
||||
|
|
@ -807,7 +846,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location
|
|||
|
||||
const renderApp = function(targetElemId, appState) {
|
||||
function retryRenderer() {
|
||||
var elem = angular.element(document.getElementById(elememId));
|
||||
var elem = angular.element(document.getElementById(targetElemId));
|
||||
console.log('retry renderApp %o', elem);
|
||||
if (!elem.length) {
|
||||
$timeout(retryRenderer, 1000);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ limitations under the License.
|
|||
tooltip="Scroll Top"></div>
|
||||
</div>
|
||||
|
||||
<div id="p{{id}}_custom" class="resultContained"
|
||||
ng-if="!isDefaultDisplay()">
|
||||
</div>
|
||||
|
||||
<div id="p{{id}}_elem" class="resultContained"
|
||||
ng-if="type == 'ELEMENT'">
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@
|
|||
*/
|
||||
|
||||
import { HeliumType, } from './helium-type';
|
||||
import {
|
||||
AbstractFrontendInterpreter,
|
||||
DefaultDisplayType
|
||||
} from '../../app/frontend-interpreter'
|
||||
|
||||
(function() {
|
||||
angular.module('zeppelinWebApp').service('heliumService', heliumService);
|
||||
|
|
|
|||
Loading…
Reference in a new issue