)]}'
{
  "commit": "52a638e5b99ac66cb08cb5f066f807383b37d268",
  "tree": "0649bc421bfac910ac1d5ccd96771ab034c25365",
  "parents": [
    "3d92e924f2a4cc1926d544e885da6167c1dcbf9a"
  ],
  "author": {
    "name": "ChanHo Lee",
    "email": "chanholee@apache.org",
    "time": "Wed Jun 17 00:53:38 2026 +0900"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Wed Jun 17 00:53:38 2026 +0900"
  },
  "message": "[ZEPPELIN-6428] Add reusable React mount infrastructure and migrate paragraph footer behind a flag\n\n### What is this PR for?\nzeppelin-web-angular ships React islands via Webpack Module Federation (PublishedParagraph pilot). This PR promotes that ad-hoc integration into reusable mount infrastructure and migrates the notebook paragraph footer as its first consumer, behind a `?reactFooter\u003dtrue` query-param gate.\n\n- `share/react-mount/`: `ReactMountDirective`, `ReactRemoteLoaderService`, and a `ReactMountHandle` contract (`mount(element, props)` returning `{ update, unmount }`) so prop changes update the React root in place instead of remounting. `remoteEntry.js` is loaded once per page (cached promise with failure eviction); each directive instance owns an isolated React root.\n- React `ParagraphFooter` component (zeppelin-react) matching the Angular footer\u0027s execution-time and elapsed-time behavior, wrapped in an error boundary.\n- Gate: `?reactFooter\u003dtrue`. Without the flag, rendering is unchanged. With the flag, remote load/mount/update errors — and React render/lifecycle errors caught by the error boundary — fall back to the Angular footer for that paragraph only.\n- Playwright E2E (`react-footer.spec.ts`) and a vitest unit-test harness for zeppelin-react covering the error boundary and the mount contract.\n- Toolchain alignment for `projects/zeppelin-react`: typescript 4.9.5 → 5.9.3 (matching the Angular workspace), \u003cat\u003etypes/node 18 → 22 (matching the node 22 runtime), vitest 4.1.8 — the latter fixes the critical advisory GHSA-5xrq-8626-4rwp flagged by the npm-audit CI job.\n\nThe existing PublishedParagraph pilot remains on its current loader; this PR only types its `container.get()` call. Migrating it to the new directive is a follow-up.\n\n### What type of PR is it?\nImprovement\n\n### Todos\n* [x] Mount infrastructure (`react-mount/`)\n* [x] React `ParagraphFooter` + error boundary\n* [x] `?reactFooter\u003dtrue` gate with per-paragraph Angular fallback\n* [x] Playwright E2E (5 cases incl. load-failure fallback)\n* [x] vitest unit tests (error boundary spec, mount contract)\n* [x] zeppelin-react toolchain alignment (TS 5.9 / \u003cat\u003etypes/node 22 / vitest 4.1.8)\n\n### What is the Jira issue?\n* https://issues.apache.org/jira/browse/ZEPPELIN-6428\n\n### How should this be tested?\nVerified locally:\n* `npx playwright test --project\u003dchromium react-footer` — 5/5 passed against a local Zeppelin server (0.13.0-SNAPSHOT), including the remote-load-failure fallback case.\n* `cd projects/zeppelin-react \u0026\u0026 npm test` — 13/13 vitest unit tests (error boundary behavior, `mount()` contract incl. update/unmount, outdated/elapsed formatting with a pinned clock).\n* `npm audit --audit-level\u003dhigh` exits 0 in `projects/zeppelin-react` (same gate as the npm-audit CI job). One moderate uuid advisory remains via sockjs/webpack-dev-server with no compatible upstream fix; it does not trip the high-level gate.\n* `eslint`/`prettier` clean on changed files; `tsc --noEmit` clean (also covers what the `transpileOnly` webpack build skips).\n\nManual:\n* Open a notebook with `?reactFooter\u003dtrue` → footer renders from React (`data-testid\u003d\"react-paragraph-footer\"`), `remoteEntry.js` requested once.\n* Without the flag → the Angular footer renders as before.\n* Block `remoteEntry.js` in DevTools → the paragraph falls back to the Angular footer with a console diagnostic.\n\nNote on coverage: the React pieces are unit-tested via a new self-contained vitest setup in `projects/zeppelin-react` (vitest 4.1.8). The Angular-side pieces (`ReactMountDirective`, `ReactRemoteLoaderService`) are covered through E2E only, since zeppelin-web-angular has no unit-test harness (zero `.spec.ts` under `src/`, no test target in angular.json). Happy to add Angular unit tests if a harness lands or committers prefer a different approach.\n\n### Screenshots (if appropriate)\nThe React footer is intended to match the existing footer\u0027s rendering (same text and layout; styles ported to a scoped CSS class).\n\n### Questions:\n* Does the license files need to update? Adds `date-fns` (MIT) to `projects/zeppelin-react`\u0027s package.json and lockfile (the Angular app already depends on it), plus dev-only test dependencies (vitest, jsdom, Testing Library). The lockfile diff includes npm peer-flag normalization from `npm install`.\n* Is there breaking changes for older versions? No. The footer change is opt-in via query param; default rendering is unchanged.\n* Does this needs documentation? No user-facing docs; developer docs updated in `projects/zeppelin-react/README.md`.\n\n\nCloses #5266 from tbonelee/ZEPPELIN-6428-react-mount-infrastructure.\n\nSigned-off-by: ChanHo Lee \u003cchanholee@apache.org\u003e",
  "tree_diff": [
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "d76ff4438bd28093cd1ca19b9d6c8361e98a9c9e",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts"
    },
    {
      "type": "modify",
      "old_id": "58f2d8a2d4f1ae2b305d058ae8f8bbdd86e8a666",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/projects/zeppelin-react/README.md",
      "new_id": "f452ee1455a1ad5bc81dd19f9b700f3b24da33f5",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/README.md"
    },
    {
      "type": "modify",
      "old_id": "f0f3439bab6ee5b37fb020983de12ccd4a851269",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/projects/zeppelin-react/package-lock.json",
      "new_id": "354bdcc974a798b2de3ce2fc07dbb0834863f149",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/package-lock.json"
    },
    {
      "type": "modify",
      "old_id": "ecc1b361da9071bf09f4f17e0ecfc5f077b4ecf0",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/projects/zeppelin-react/package.json",
      "new_id": "cf1e807d581b556b8131b40a1229f69ac9c468c6",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/package.json"
    },
    {
      "type": "modify",
      "old_id": "a9beee6fe3a07087861b2827a4e0466c5cd7e3b4",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/index.ts",
      "new_id": "b27f8b466041fd1a7945c935ba5cc6f4420ad63f",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/index.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "52b8f842bb9e38bc9dc57c022e2f5aabe0fe6fba",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/paragraph/ParagraphFooter.css"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "69d1476b4f6d32ee9573b6fe1827ad673083b09f",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/paragraph/ParagraphFooter.spec.tsx"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "d73f881653b1457b1ae23c62205488cb751aac37",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/paragraph/ParagraphFooter.tsx"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "cf2b155072e690fac9525578a5bf12d511faef25",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/paragraph/ReactErrorBoundary.spec.tsx"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "f0d6157e0fb0a2e326ff94b2600a0aac79375466",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/paragraph/ReactErrorBoundary.tsx"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "80c0ecafbb3987113356a6879a583f1044542402",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/components/paragraph/index.ts"
    },
    {
      "type": "modify",
      "old_id": "190f1978160e8fdae73cfa8690598f84721528c6",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/projects/zeppelin-react/src/main.ts",
      "new_id": "cf8e866a3186d6e16e328cd3132f096ef611bc52",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/main.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "43cdd4dba515fe22e710ecbfe012c9ca4c7a6559",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/src/test-setup.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "8fde66b75c16cec385cfb7b13c7da87d27555269",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/vitest.config.ts"
    },
    {
      "type": "modify",
      "old_id": "4facdadc09b98f7b7022d5cea837dd9a00b7d172",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/projects/zeppelin-react/webpack.config.js",
      "new_id": "aef55f29ace1fe5f5beae63a6dc6848538dac512",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/projects/zeppelin-react/webpack.config.js"
    },
    {
      "type": "modify",
      "old_id": "20a459207717ee8b160f22285dfcaf9fbf32e0f9",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html",
      "new_id": "6dc08633df8bc3b3c53c4b93ef1eee22423af2c5",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html"
    },
    {
      "type": "modify",
      "old_id": "1cb8d2b288ba96c3c6fabdf35a3e48b1c91e6766",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts",
      "new_id": "d76bca003e44421a18bc0f6afa5d2a2fab1cbb41",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts"
    },
    {
      "type": "modify",
      "old_id": "579546a02f9fb2c005e0e04f9655fb43896a3179",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html",
      "new_id": "d509136f18cca5ba5ff73404fc45e953fa979bd0",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html"
    },
    {
      "type": "modify",
      "old_id": "d55b94efc2c0a09b4cc499ca5bfc08b56a262303",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts",
      "new_id": "67d1f787de4585d7bcc696eb6045572737b9b2cc",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts"
    },
    {
      "type": "modify",
      "old_id": "1f6e7f73997c3d2484048df0ef42699925723ef9",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts",
      "new_id": "8429e895e2428d744c375d52e9135d64db5c6883",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts"
    },
    {
      "type": "modify",
      "old_id": "312df06dbde3687b36ab56b00ab671a1a0798296",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/src/app/share/public-api.ts",
      "new_id": "07192569474f3b4a422839810316551958277e12",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/share/public-api.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "49e474044224e9aea8117bcb9f040b6ea736142f",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/share/react-mount/index.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "43a3bfccf88a2c66d9c4cc49e9367d9e8efc4e7e",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/share/react-mount/public-api.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "aefeb58fe317d253b25dba566b1e9a41ae9568d8",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/share/react-mount/react-mount-handle.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "c93c168001b10d1538a801cd58266dd7d45295e4",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/share/react-mount/react-mount.directive.ts"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "75e8a2fd4de98666463c3576ea6882a2281ab86e",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts"
    },
    {
      "type": "modify",
      "old_id": "247850864ca8e4e422ff43bae0fbeed1b687bc92",
      "old_mode": 33188,
      "old_path": "zeppelin-web-angular/src/app/share/share.module.ts",
      "new_id": "89326234a5b63d281621cd986005fe9128123422",
      "new_mode": 33188,
      "new_path": "zeppelin-web-angular/src/app/share/share.module.ts"
    }
  ]
}
