Skip to content

Commit 4bde6b2

Browse files
authored
[ZEPPELIN-6371] Convert published paragraph rendering to Micro Frontend(Angular to React) in New UI
### What is this PR for? [Micro Frontend Migration(Angular to React) Proposal](https://cwiki.apache.org/confluence/display/ZEPPELIN/Micro+Frontend+Migration%28Angular+to+React%29+Proposal) --- #### Summary * Implement React-based micro-frontend architecture using Module Federation. * Convert published paragraph component to support React rendering. * Add environment-based configuration for development and production builds. #### Changes **1. React Micro-Frontend Project Setup** * Created new React project at `projects/zeppelin-react/`. * Configured Webpack Module Federation for micro-frontend architecture. * Set up React 18 with TypeScript support. **2. Component Implementation** *New React Components:* * `PublishedParagraph`: Main entry point for published paragraph rendering. * `SingleResultRenderer`: Template for rendering single paragraph results. *Renderers:* * `HTMLRenderer`: Renders HTML content with sanitization. * `TextRenderer`: Renders plain text with ANSI support. * `ImageRenderer`: Renders image outputs. *Visualizations:* * `TableVisualization`: Table rendering with sorting, filtering, and export. * `VisualizationControls`: Control panel for table operations. *Common Components:* * `Loading`: Loading state indicator. * `Empty`: Empty state display. **3. Angular Integration** * `paragraph.component.ts`: Added React widget loading logic via Module Federation. * `paragraph.component.html`: Added React container element. * `environment.ts` / `environment.prod.ts`: Added `reactRemoteEntryUrl` configuration. * Development: `http://localhost:3001/remoteEntry.js` * Production: `/assets/react/remoteEntry.js` **4. Build Configuration** * `angular.json`: Copy React build output to `/assets/react/`. * `webpack.config.js`: Configured Module Federation plugin: * Dev server: port 3001 * CORS headers for cross-origin requests * Environment-specific `publicPath` * `proxy.conf.js`: Updated proxy configuration. **5. Package** * Added React and React-DOM dependencies. * Added Webpack and Module Federation plugins. * Added Ant Design for React UI components. * Added <at>antv/g2plot for data visualization (also used in Angular version with G2). #### License This PR uses several open-source libraries. The `xlsx` (v0.18.5) and `typescript` (v4.6.4) packages are licensed under **Apache-2.0**, while all other dependencies and devDependencies (such as `react`, `react-dom`, `antd`, `<at>ant-design/icons`, etc.) are licensed under **MIT**. The MIT license is more permissive than Apache-2.0, so including MIT-licensed packages does not violate Apache-2.0 terms. All packages may be used commercially, and license notices should be included when distributing the project. #### Technical Details **Module Federation Configuration** ```ts // Development: http://localhost:3001/remoteEntry.js // Production: /assets/react/remoteEntry.js new ModuleFederationPlugin({ name: 'reactApp', filename: 'remoteEntry.js', exposes: { './PublishedParagraph': './src/pages/PublishedParagraph' } }) ``` #### Usage * Render published paragraph with React: `/notebook/{noteId}/paragraph/{paragraphId}?react=true` ### What type of PR is it? Improvement ### Todos ### What is the Jira issue? ZEPPELIN-6371 ### How should this be tested? ```sh // Start Zeppelin Server ./mvnw clean install -DskipTests ./mvnw clean package -DskipTests ./bin/zeppelin-daemon.sh start // Start Zeppelin New UI Client cd zeppelin-web-angular nvm use npm i npm run start ``` #### TextRenderer http://localhost:4200/#/notebook/2EYDJKFFY/paragraph/20180118-122136_1299905608?react=true #### TableVisualization http://localhost:4200/#/notebook/2EYDJKFFY/paragraph/20180118-122136_1299905608?react=true #### ImageRenderer http://localhost:4200/#/notebook/2F1S9ZY8Z/paragraph/20180117-220535_590781730?react=true #### HTMLRenderer - Table http://localhost:4200/#/notebook/2F1S9ZY8Z/paragraph/paragraph_1580885453474_1167659991?react=true #### HTMLRenderer - Script(Bokeh JS) http://localhost:4200/#/notebook/2F1S9ZY8Z/paragraph/paragraph_1580885707198_-1652524072?react=true ### Screenshots (if appropriate) ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Closes #5111 from dididy/feature/micro-frontend. Signed-off-by: ChanHo Lee <chanholee@apache.org>
1 parent c3ccd9b commit 4bde6b2

46 files changed

Lines changed: 12199 additions & 172 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

zeppelin-web-angular/.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# See http://help.github.com/ignore-files/ for more about ignoring files.
22

33
# compiled output
4-
/dist
4+
**/dist
55
/tmp
66
/out-tsc
77

88
# dependencies
9-
/node_modules
9+
**/node_modules
1010

1111
# profiling files
1212
chrome-profiler-events.json

zeppelin-web-angular/angular.json

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@
3838
},
3939
"architect": {
4040
"build": {
41-
"builder": "ngx-build-plus:browser",
41+
"builder": "@angular-builders/custom-webpack:browser",
4242
"options": {
4343
"outputPath": "dist/zeppelin",
4444
"index": "src/index.html",
4545
"main": "src/main.ts",
4646
"polyfills": "src/polyfills.ts",
4747
"tsConfig": "src/tsconfig.json",
48+
"customWebpackConfig": {
49+
"path": "./webpack.config.js"
50+
},
4851
"assets": [
4952
"src/favicon.ico",
5053
"src/assets",
@@ -62,6 +65,11 @@
6265
"glob": "**/*",
6366
"input": "./WEB-INF",
6467
"output": "/WEB-INF/"
68+
},
69+
{
70+
"glob": "**/*",
71+
"input": "./projects/zeppelin-react/dist",
72+
"output": "/assets/react/"
6573
}
6674
],
6775
"styles": [
@@ -100,27 +108,29 @@
100108
"optimization": true,
101109
"outputHashing": "all",
102110
"sourceMap": false,
103-
"extractCss": true,
104111
"namedChunks": false,
105112
"aot": true,
106113
"extractLicenses": true,
107114
"vendorChunk": false,
108115
"buildOptimizer": false
109116
},
110117
"development": {
111-
"buildOptimizer": false,
112118
"optimization": false,
113-
"vendorChunk": true,
114119
"extractLicenses": false,
115120
"namedChunks": true,
116121
"sourceMap": true
117122
}
118-
}
123+
},
124+
"defaultConfiguration": "production"
119125
},
120126
"serve": {
121-
"builder": "ngx-build-plus:dev-server",
127+
"builder": "@angular-builders/custom-webpack:dev-server",
122128
"options": {
123-
"browserTarget": "zeppelin:build"
129+
"browserTarget": "zeppelin:build",
130+
"port": 4200,
131+
"host": "localhost",
132+
"liveReload": true,
133+
"hmr": true
124134
},
125135
"configurations": {
126136
"production": {

zeppelin-web-angular/e2e/models/home-page.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ export class HomePage extends BasePage {
8989
};
9090
}
9191

92+
async navigateToHome(): Promise<void> {
93+
await this.page.goto('/');
94+
await this.waitForPageLoad();
95+
}
96+
9297
async navigateToLogin(): Promise<void> {
9398
await this.navigateToRoute('/login');
9499
// Wait for potential redirect to complete by checking URL change
@@ -152,9 +157,16 @@ export class HomePage extends BasePage {
152157
}
153158

154159
async filterNotes(searchTerm: string): Promise<void> {
160+
await this.page.waitForLoadState('domcontentloaded', { timeout: 10000 });
161+
await this.nodeList.filterInput.waitFor({ state: 'visible', timeout: 5000 });
155162
await this.nodeList.filterInput.fill(searchTerm, { timeout: 15000 });
156163
}
157164

165+
async isRefreshIconSpinning(): Promise<boolean> {
166+
const spinAttribute = await this.refreshIcon.getAttribute('nzSpin');
167+
return spinAttribute === 'true' || spinAttribute === '';
168+
}
169+
158170
async waitForRefreshToComplete(): Promise<void> {
159171
await this.waitForElementAttribute('a.refresh-note i[nz-icon]', 'nzSpin', false);
160172
}

zeppelin-web-angular/e2e/models/published-paragraph-page.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,12 @@ import { navigateToNotebookWithFallback } from '../utils';
1515
import { BasePage } from './base-page';
1616

1717
export class PublishedParagraphPage extends BasePage {
18-
readonly paragraphResult: Locator;
19-
readonly errorModalContent: Locator;
20-
readonly errorModalOkButton: Locator;
18+
private readonly errorModalContent: Locator;
19+
private readonly errorModalOkButton: Locator;
2120
readonly confirmationModal: Locator;
2221

2322
constructor(page: Page) {
2423
super(page);
25-
this.paragraphResult = page.locator('zeppelin-notebook-paragraph-result');
2624
this.errorModalContent = this.page.locator('.ant-modal-body', { hasText: 'Paragraph Not Found' }).last();
2725
this.errorModalOkButton = page.getByRole('button', { name: 'OK' }).last();
2826
this.confirmationModal = page.locator('div.ant-modal-confirm').last();

zeppelin-web-angular/e2e/models/workspace-page.util.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { expect, Page } from '@playwright/test';
1414
import { BasePage } from './base-page';
1515
import { WorkspacePage } from './workspace-page';
16+
import { performLoginIfRequired, waitForZeppelinReady } from '../utils';
1617

1718
export class WorkspaceUtil extends BasePage {
1819
private workspacePage: WorkspacePage;
@@ -22,6 +23,17 @@ export class WorkspaceUtil extends BasePage {
2223
this.workspacePage = new WorkspacePage(page);
2324
}
2425

26+
async navigateAndWaitForLoad(): Promise<void> {
27+
await this.workspacePage.navigateToWorkspace();
28+
await performLoginIfRequired(this.page);
29+
await waitForZeppelinReady(this.page);
30+
}
31+
32+
async verifyWorkspaceLayout(): Promise<void> {
33+
await expect(this.workspacePage.workspaceComponent).toBeVisible();
34+
await expect(this.workspacePage.routerOutlet).toBeAttached();
35+
}
36+
2537
async verifyHeaderVisibility(shouldBeVisible: boolean): Promise<void> {
2638
if (shouldBeVisible) {
2739
await expect(this.workspacePage.zeppelinHeader).toBeVisible();

0 commit comments

Comments
 (0)