blob: b71d59fe253718a7a664c6eaf255ff08de3793a9 [file] [log] [blame]
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable, Injector } from '@angular/core';
import {
HeliumClassicTransformation,
HeliumClassicVisualization,
HeliumClassicVisualizationConstructor
} from '@zeppelin/interfaces';
import { GraphConfig, ParagraphConfigResult } from '@zeppelin/sdk';
import { TableData } from '@zeppelin/visualization';
import * as angular from 'angular';
import { cloneDeep } from 'lodash';
import { AngularDragDropService } from './angular-drag-drop.service';
import { BootstrapCompatibilityService } from './bootstrap-compatibility.service';
import { TableDataAdapterService } from './table-data-adapter.service';
interface ClassicVisualizationInstanceInfo {
instance: HeliumClassicVisualization;
targetEl: HTMLElement;
scope: angular.IScope;
appName: string;
injector: angular.auto.IInjectorService;
}
@Injectable({
providedIn: 'root'
})
export class ClassicVisualizationService {
private appCounter = 0;
private activeInstanceInfos = new Map<string, ClassicVisualizationInstanceInfo>();
// Template cache for known HTML templates
private templateCache = new Map<string, string>();
constructor(
private injector: Injector,
private tableDataAdapter: TableDataAdapterService,
private http: HttpClient,
private angularDragDropService: AngularDragDropService,
private bootstrapCompatibilityService: BootstrapCompatibilityService
) {}
// Map of template URLs to asset file paths
private templateAssetMapping = new Map<string, string>([
[
'app/tabledata/advanced-transformation-setting.html',
'assets/classic-visualization-templates/advanced-transformation-setting.html'
],
[
'app/tabledata/columnselector_settings.html',
'assets/classic-visualization-templates/columnselector_settings.html'
],
['app/tabledata/network_settings.html', 'assets/classic-visualization-templates/network_settings.html'],
['app/tabledata/pivot_settings.html', 'assets/classic-visualization-templates/pivot_settings.html']
]);
// Custom $templateRequest implementation that intercepts known template paths
private createCustomTemplateRequest(
originalTemplateRequest: angular.ITemplateRequestService
): (tpl: string, ignorRequestError?: boolean) => Promise<string> | angular.IPromise<string> {
return (templateUrl: string, ignorRequestError?: boolean) => {
// Check if we have a cached template for this URL
if (this.templateCache.has(templateUrl)) {
// Return a promise that resolves with the cached template
return Promise.resolve(this.templateCache.get(templateUrl) ?? '');
}
// Check if this is a known template that should be loaded from assets
const assetPath = this.templateAssetMapping.get(templateUrl);
if (assetPath) {
// Load from assets and cache the result
return this.http
.get(assetPath, { responseType: 'text' })
.toPromise()
.then((templateContent: string) => {
// Cache the loaded template
this.templateCache.set(templateUrl, templateContent);
return templateContent;
})
.catch(error => {
console.error(`Failed to load template from ${assetPath}:`, error);
// Fallback to original $templateRequest
return originalTemplateRequest(templateUrl, ignorRequestError);
});
}
// For unknown templates, delegate to the original $templateRequest
return originalTemplateRequest(templateUrl, ignorRequestError);
};
}
private waitForElement(elementId: string, maxRetries = 50, interval = 100): Promise<HTMLElement> {
return new Promise((resolve, reject) => {
let retries = 0;
const checkElement = () => {
const element = document.getElementById(elementId);
if (element) {
resolve(element);
return;
}
retries++;
if (retries >= maxRetries) {
reject(new Error(`Element not found after ${maxRetries} retries: ${elementId}`));
return;
}
setTimeout(checkElement, interval);
};
checkElement();
});
}
private getTransformationSettingElement(targetElementId: string): HTMLElement | null {
// Extract the base ID from targetElementId (e.g., "p123_table" -> "123_table")
const baseId = targetElementId.replace(/^p/, '');
const trSettingId = `trsetting${baseId}`;
const element = document.getElementById(trSettingId);
return element;
}
private getVisualizationSettingElement(targetElementId: string): HTMLElement | null {
// Extract the base ID from targetElementId (e.g., "p123_table" -> "123_table")
const baseId = targetElementId.replace(/^p/, '');
const vizSettingId = `vizsetting${baseId}`;
const element = document.getElementById(vizSettingId);
return element;
}
private waitForTransformationScopeAndApply(
transformation: HeliumClassicTransformation,
timeout: angular.ITimeoutService
): void {
const waitForTransformationScope = () => {
if (transformation._scope) {
transformation._scope.$apply();
} else {
timeout(waitForTransformationScope, 10);
}
};
timeout(waitForTransformationScope, 0);
}
async createClassicVisualization(
visConstructor: HeliumClassicVisualizationConstructor,
targetElementId: string,
config: GraphConfig,
tableData: TableData,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
emitter: (config: any) => void
): Promise<HeliumClassicVisualization> {
// Inject Bootstrap compatibility styles before creating visualization
this.bootstrapCompatibilityService.injectBootstrapStyles();
// Wait for DOM element to be available
const targetElement = await this.waitForElement(targetElementId);
// Clean up any existing instance for this element
this.destroyInstance(targetElementId);
const { injector, appName } = this.getOrCreateInjector(targetElement);
const rootScope = injector.get('$rootScope');
const compile = injector.get('$compile');
const originalTemplateRequest = injector.get('$templateRequest');
// Create custom templateRequest that intercepts known template paths
const templateRequest = this.createCustomTemplateRequest(originalTemplateRequest);
const timeout = injector.get('$timeout');
// Create scope for this visualization
const scope = rootScope.$new(true);
// Create angular element wrapper
const angularElement = angular.element(targetElement);
// Convert modern TableData to classic format
const classicTableData = this.tableDataAdapter.createClassicTableDataProxy(tableData);
// Instantiate the classic visualization
const configForMode = this.getClassicVizConfig(config);
const vizInstance = new visConstructor(angularElement, configForMode);
// Inject AngularJS dependencies that classic visualizations expect
vizInstance._emitter = emitter;
vizInstance._compile = compile;
vizInstance._createNewScope = () => rootScope.$new(true);
vizInstance._templateRequest = templateRequest;
// Get or create setting elements
const transformationSettingEl = this.getTransformationSettingElement(targetElementId);
const visualizationSettingEl = this.getVisualizationSettingElement(targetElementId);
if (!transformationSettingEl || !visualizationSettingEl) {
throw new Error('Failed to find setting elements for classic visualization');
}
// Setup transformation if available
const transformation = vizInstance.getTransformation();
if (transformation) {
transformation._emitter = emitter;
transformation._templateRequest = templateRequest;
transformation._compile = compile;
transformation._createNewScope = () => rootScope.$new(true);
// Set config and transform data
transformation.setConfig(configForMode);
const transformed = transformation.transform(classicTableData);
// Render transformation setting
transformation.renderSetting(angular.element(transformationSettingEl));
// Wait for transformation rendering to complete (including async template loading)
this.waitForTransformationScopeAndApply(transformation, timeout);
// Render the visualization
vizInstance.render(transformed);
} else {
// If no transformation, render directly
vizInstance.render(classicTableData);
}
// Render visualization setting
vizInstance.renderSetting(angular.element(visualizationSettingEl));
// Activate the visualization
vizInstance.activate();
// Store the instance for cleanup later
this.activeInstanceInfos.set(targetElementId, {
instance: vizInstance,
targetEl: targetElement,
scope,
appName,
injector
});
return vizInstance;
}
private getOrCreateInjector(targetElement: HTMLElement): {
injector: angular.auto.IInjectorService;
appName: string;
} {
// Check if element is already bootstrapped
const existingInjector = angular.element(targetElement).injector();
if (existingInjector) {
// Reuse existing bootstrap
return {
injector: existingInjector,
appName: 'existingApp' // We don't need the actual name for reuse
};
} else {
// Create unique app name
const appName = `classicVizApp_${this.appCounter++}`;
// Create AngularJS module
const module = angular.module(appName, []);
// Add custom drag and drop directives
this.angularDragDropService.addDragDropDirectives(module);
// Create AngularJS app and bootstrap
const injector = angular.bootstrap(targetElement, [appName]);
return {
injector,
appName
};
}
}
updateClassicVisualization(targetElementId: string, config: GraphConfig, tableData: TableData): void {
const instanceInfo = this.activeInstanceInfos.get(targetElementId);
if (!instanceInfo) {
return;
}
const { instance } = instanceInfo;
const configForMode = this.getClassicVizConfig(config);
try {
// Convert modern TableData to classic format
const classicTableData = this.tableDataAdapter.createClassicTableDataProxy(tableData);
// Get or create setting elements
const transformationSettingEl = this.getTransformationSettingElement(targetElementId);
const visualizationSettingEl = this.getVisualizationSettingElement(targetElementId);
if (!transformationSettingEl || !visualizationSettingEl) {
throw new Error('Failed to find setting elements for classic visualization');
}
instance.setConfig(configForMode);
// Update transformation and re-render
const transformation = instance.getTransformation();
if (transformation) {
transformation.setConfig(configForMode);
const transformed = transformation.transform(classicTableData);
// Re-render transformation setting
transformation.renderSetting(angular.element(transformationSettingEl));
// Wait for transformation rendering to complete (including async template loading)
const { injector } = instanceInfo;
const timeout = injector.get('$timeout');
this.waitForTransformationScopeAndApply(transformation, timeout);
instance.render(transformed);
} else {
instance.render(classicTableData);
}
// Re-render visualization setting
instance.renderSetting(angular.element(visualizationSettingEl));
// Refresh if available
instance.refresh();
} catch (error) {
console.error('Error updating classic visualization:', error);
}
}
setClassicVisualizationConfig(targetElementId: string, config: GraphConfig): void {
const instanceInfo = this.activeInstanceInfos.get(targetElementId);
if (!instanceInfo) {
return;
}
const { instance } = instanceInfo;
try {
instance.setConfig(config);
instance.refresh();
} catch (error) {
console.error('Error setting classic visualization config:', error);
}
}
resizeClassicVisualization(targetElementId: string): void {
const instanceInfo = this.activeInstanceInfos.get(targetElementId);
if (!instanceInfo) {
return;
}
const { instance } = instanceInfo;
try {
instance.resize();
} catch (error) {
console.error('Error resizing classic visualization:', error);
}
}
destroyInstance(targetElementId: string, forceCleanBootstrap = false): void {
const instanceInfo = this.activeInstanceInfos.get(targetElementId);
if (!instanceInfo) {
return;
}
const { instance, targetEl, scope } = instanceInfo;
try {
// Destroy the visualization instance
instance.destroy();
// Destroy the scope
scope.$destroy();
// Clean up the DOM content but preserve the element
angular.element(targetEl).empty();
// If forceCleanBootstrap is true, completely remove AngularJS data
if (forceCleanBootstrap) {
// Remove all AngularJS data from the element
angular.element(targetEl).removeData();
// Remove all classes added by AngularJS
angular.element(targetEl).removeClass('ng-scope');
}
this.activeInstanceInfos.delete(targetElementId);
} catch (error) {
console.error('Error destroying classic visualization:', error);
}
}
destroyAllInstances(forceCleanBootstrap = false): void {
const elementIds = Array.from(this.activeInstanceInfos.keys());
elementIds.forEach(elementId => {
this.destroyInstance(elementId, forceCleanBootstrap);
});
}
private getClassicVizConfig(graph: GraphConfig) {
const mode = graph.mode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const configForMode: any = graph?.setting?.[mode as keyof ParagraphConfigResult['graph']['setting']]
? cloneDeep(graph.setting[mode as keyof ParagraphConfigResult['graph']['setting']])
: {};
// copy common setting
configForMode.common = cloneDeep(graph.commonSetting) || {};
// copy pivot setting
if (graph.keys) {
configForMode.common.pivot = {
keys: cloneDeep(graph.keys),
groups: cloneDeep(graph.groups),
values: cloneDeep(graph.values)
};
}
return configForMode;
}
}