This is an automated email from the ASF dual-hosted git repository.

pawarprasad123 pushed a commit to branch ATLAS-5389_broswerResourcePolicy
in repository https://gitbox.apache.org/repos/asf/atlas.git

commit 8706ccc7f704e8232c3c0f90dbe131b4959bc2a6
Author: Prasad Pawar <[email protected]>
AuthorDate: Wed Aug 26 18:01:04 2026 +0530

    ATLAS-5389: Atlas React Dashboard — Align UI with strict browser resource 
policy (nonce-based script/style handling)
---
 dashboard/index.html                               |   2 +
 dashboard/src/Main.tsx                             |  23 +++--
 dashboard/src/__tests__/Main.test.tsx              |  58 +++++++++++
 dashboard/src/utils/__tests__/cspNonce.test.ts     | 115 +++++++++++++++++++++
 dashboard/src/utils/__tests__/emotionCache.test.ts |  61 +++++++++++
 dashboard/{index.html => src/utils/cspNonce.ts}    |  53 +++++++---
 .../{index.html => src/utils/emotionCache.ts}      |  26 ++---
 7 files changed, 299 insertions(+), 39 deletions(-)

diff --git a/dashboard/index.html b/dashboard/index.html
index 58a084be1c..b02457627a 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -21,6 +21,8 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <!-- Backend: populate content with the per-request CSP nonce (same value 
as script/style nonce attrs). -->
+    <meta name="csp-nonce" content="" />
     <title>Atlas</title>
   </head>
 
diff --git a/dashboard/src/Main.tsx b/dashboard/src/Main.tsx
index b01422a171..9ff3fcef1e 100644
--- a/dashboard/src/Main.tsx
+++ b/dashboard/src/Main.tsx
@@ -14,7 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { ThemeProvider, createTheme } from "@mui/material/styles";
+import { CacheProvider } from "@emotion/react";
+import { StyledEngineProvider, ThemeProvider, createTheme } from 
"@mui/material/styles";
 import * as React from "react";
 import { createRoot } from "react-dom/client";
 import App from "./App.tsx";
@@ -30,8 +31,12 @@ import "react-quill-new/dist/quill.core.css";
 import "../src/styles/table.scss";
 import { Provider } from "react-redux";
 import store from "./redux/store/store.ts";
+import { createEmotionCache } from "./utils/emotionCache.ts";
+import { getCspNonce } from "./utils/cspNonce.ts";
 // import ErrorBoundary from "ErrorBoundary.ts";
 
+const emotionCache = createEmotionCache(getCspNonce());
+
 const theme = createTheme({
   typography: {
     allVariants: {
@@ -44,11 +49,15 @@ const theme = createTheme({
 
 createRoot(document.getElementById("root")!).render(
   <React.StrictMode>
-    <ThemeProvider theme={theme}>
-      <Provider store={store}>
-        <App />
-      </Provider>
-      <ToastContainer />
-    </ThemeProvider>
+    <CacheProvider value={emotionCache}>
+      <StyledEngineProvider injectFirst>
+        <ThemeProvider theme={theme}>
+          <Provider store={store}>
+            <App />
+          </Provider>
+          <ToastContainer />
+        </ThemeProvider>
+      </StyledEngineProvider>
+    </CacheProvider>
   </React.StrictMode>
 );
diff --git a/dashboard/src/__tests__/Main.test.tsx 
b/dashboard/src/__tests__/Main.test.tsx
index ff17c368b0..386a98e943 100644
--- a/dashboard/src/__tests__/Main.test.tsx
+++ b/dashboard/src/__tests__/Main.test.tsx
@@ -40,6 +40,31 @@ jest.mock('../App', () => ({
        default: () => <div data-testid="app">App Component</div>
 }))
 
+jest.mock('@emotion/cache', () => ({
+       __esModule: true,
+       default: jest.fn(() => ({ key: 'css' }))
+}))
+
+jest.mock('@emotion/react', () => {
+       const actual = jest.requireActual('@emotion/react')
+
+       return {
+               ...actual,
+               CacheProvider: ({ children }: { children: React.ReactNode }) => 
(
+                       <div data-testid="cache-provider">{children}</div>
+               )
+       }
+})
+
+const mockGetCspNonce = jest.fn()
+const mockCreateEmotionCache = jest.fn(() => ({ key: 'css' }))
+jest.mock('../utils/cspNonce.ts', () => ({
+       getCspNonce: () => mockGetCspNonce()
+}))
+jest.mock('../utils/emotionCache.ts', () => ({
+       createEmotionCache: (...args: unknown[]) => 
mockCreateEmotionCache(...args)
+}))
+
 // Mock all CSS imports
 jest.mock('../index.scss', () => {})
 jest.mock('react-toastify/dist/ReactToastify.css', () => {})
@@ -92,6 +117,8 @@ describe('Main.tsx', () => {
        beforeEach(() => {
                jest.clearAllMocks()
                jest.resetModules()
+               mockGetCspNonce.mockReturnValue(undefined)
+               mockCreateEmotionCache.mockReturnValue({ key: 'css' })
                mockStore = createMockStore()
                
                // Mock document.getElementById to return root element
@@ -115,6 +142,37 @@ describe('Main.tsx', () => {
                jest.resetModules()
        })
 
+       describe('CSP Nonce Wiring', () => {
+               it('should read csp nonce and create emotion cache without 
nonce when unavailable', async () => {
+                       mockGetCspNonce.mockReturnValue(undefined)
+
+                       await import('../Main.tsx')
+
+                       expect(mockGetCspNonce).toHaveBeenCalled()
+                       
expect(mockCreateEmotionCache).toHaveBeenCalledWith(undefined)
+               })
+
+               it('should pass csp nonce to createEmotionCache when 
available', async () => {
+                       
mockGetCspNonce.mockReturnValue('server-generated-nonce')
+
+                       await import('../Main.tsx')
+
+                       
expect(mockCreateEmotionCache).toHaveBeenCalledWith('server-generated-nonce')
+               })
+
+               it('should invoke createRoot.render after configuring emotion 
cache', async () => {
+                       
mockGetCspNonce.mockReturnValue('server-generated-nonce')
+
+                       await import('../Main.tsx')
+
+                       const ReactDOM = require('react-dom/client')
+                       const mockRoot = 
ReactDOM.createRoot.mock.results[0].value
+
+                       expect(mockCreateEmotionCache).toHaveBeenCalled()
+                       expect(mockRoot.render).toHaveBeenCalled()
+               })
+       })
+
        describe('Module Execution', () => {
                it('should execute Main.tsx and call createRoot with root 
element', async () => {
                        // Import Main.tsx to execute it
diff --git a/dashboard/src/utils/__tests__/cspNonce.test.ts 
b/dashboard/src/utils/__tests__/cspNonce.test.ts
new file mode 100644
index 0000000000..59a3b445a5
--- /dev/null
+++ b/dashboard/src/utils/__tests__/cspNonce.test.ts
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 { CSP_NONCE_META_NAME, getCspNonce } from "../cspNonce";
+
+describe("getCspNonce", () => {
+  const originalWindowNonce = window.__CSP_NONCE__;
+
+  beforeEach(() => {
+    document.head.innerHTML = "";
+    document.body.innerHTML = "";
+    delete window.__CSP_NONCE__;
+  });
+
+  afterEach(() => {
+    if (originalWindowNonce === undefined) {
+      delete window.__CSP_NONCE__;
+    } else {
+      window.__CSP_NONCE__ = originalWindowNonce;
+    }
+  });
+
+  it("returns nonce from meta tag when present", () => {
+    const meta = document.createElement("meta");
+    meta.setAttribute("name", CSP_NONCE_META_NAME);
+    meta.setAttribute("content", "meta-nonce-value");
+    document.head.appendChild(meta);
+
+    expect(getCspNonce()).toBe("meta-nonce-value");
+  });
+
+  it("trims whitespace from meta tag nonce", () => {
+    const meta = document.createElement("meta");
+    meta.setAttribute("name", CSP_NONCE_META_NAME);
+    meta.setAttribute("content", "  trimmed-meta-nonce  ");
+    document.head.appendChild(meta);
+
+    expect(getCspNonce()).toBe("trimmed-meta-nonce");
+  });
+
+  it("prefers meta tag nonce over script nonce", () => {
+    const meta = document.createElement("meta");
+    meta.setAttribute("name", CSP_NONCE_META_NAME);
+    meta.setAttribute("content", "meta-nonce-value");
+    document.head.appendChild(meta);
+
+    const script = document.createElement("script");
+    script.setAttribute("nonce", "script-nonce-value");
+    document.body.appendChild(script);
+
+    expect(getCspNonce()).toBe("meta-nonce-value");
+  });
+
+  it("returns nonce from script tag when meta tag is missing", () => {
+    const script = document.createElement("script");
+    script.setAttribute("nonce", "script-nonce-value");
+    document.body.appendChild(script);
+
+    expect(getCspNonce()).toBe("script-nonce-value");
+  });
+
+  it("returns nonce from window global when dom sources are missing", () => {
+    window.__CSP_NONCE__ = "window-nonce-value";
+
+    expect(getCspNonce()).toBe("window-nonce-value");
+  });
+
+  it("returns undefined when no nonce source is available", () => {
+    expect(getCspNonce()).toBeUndefined();
+  });
+
+  it("returns undefined when meta content is blank", () => {
+    const meta = document.createElement("meta");
+    meta.setAttribute("name", CSP_NONCE_META_NAME);
+    meta.setAttribute("content", "   ");
+    document.head.appendChild(meta);
+
+    expect(getCspNonce()).toBeUndefined();
+  });
+
+  it("returns undefined when script nonce is blank and no other source 
exists", () => {
+    const script = document.createElement("script");
+    script.setAttribute("nonce", "   ");
+    document.body.appendChild(script);
+
+    expect(getCspNonce()).toBeUndefined();
+  });
+
+  it("falls back to script nonce when meta content is blank", () => {
+    const meta = document.createElement("meta");
+    meta.setAttribute("name", CSP_NONCE_META_NAME);
+    meta.setAttribute("content", "   ");
+    document.head.appendChild(meta);
+
+    const script = document.createElement("script");
+    script.setAttribute("nonce", "script-nonce-value");
+    document.body.appendChild(script);
+
+    expect(getCspNonce()).toBe("script-nonce-value");
+  });
+});
diff --git a/dashboard/src/utils/__tests__/emotionCache.test.ts 
b/dashboard/src/utils/__tests__/emotionCache.test.ts
new file mode 100644
index 0000000000..06e1df7660
--- /dev/null
+++ b/dashboard/src/utils/__tests__/emotionCache.test.ts
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+jest.mock("@emotion/cache", () => ({
+  __esModule: true,
+  default: jest.fn(() => ({ key: "css" }))
+}));
+
+import createCache from "@emotion/cache";
+import { createEmotionCache } from "../emotionCache";
+
+describe("createEmotionCache", () => {
+  const mockCreateCache = createCache as jest.MockedFunction<typeof 
createCache>;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    mockCreateCache.mockReturnValue({ key: "css" });
+  });
+
+  it("creates emotion cache without nonce when nonce is undefined", () => {
+    createEmotionCache(undefined);
+
+    expect(mockCreateCache).toHaveBeenCalledWith({ key: "css" });
+  });
+
+  it("creates emotion cache without nonce when nonce is blank", () => {
+    createEmotionCache("   ");
+
+    expect(mockCreateCache).toHaveBeenCalledWith({ key: "css" });
+  });
+
+  it("creates emotion cache with nonce when nonce is provided", () => {
+    createEmotionCache("server-generated-nonce");
+
+    expect(mockCreateCache).toHaveBeenCalledWith({
+      key: "css",
+      nonce: "server-generated-nonce"
+    });
+  });
+
+  it("returns the cache instance from @emotion/cache", () => {
+    const cacheInstance = { key: "css", nonce: "server-generated-nonce" };
+    mockCreateCache.mockReturnValue(cacheInstance);
+
+    expect(createEmotionCache("server-generated-nonce")).toBe(cacheInstance);
+  });
+});
diff --git a/dashboard/index.html b/dashboard/src/utils/cspNonce.ts
similarity index 50%
copy from dashboard/index.html
copy to dashboard/src/utils/cspNonce.ts
index 58a084be1c..5a6e390c5e 100644
--- a/dashboard/index.html
+++ b/dashboard/src/utils/cspNonce.ts
@@ -1,4 +1,4 @@
-<!--
+/*
  * Licensed to the Apache Software Foundation (ASF) under one or more
  * contributor license agreements.  See the NOTICE file distributed with
  * this work for additional information regarding copyright ownership.
@@ -13,19 +13,38 @@
  * 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.
--->
-
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Atlas</title>
-  </head>
-
-  <body>
-    <div id="root"></div>
-    <script type="module" src="/src/Main.tsx"></script>
-  </body>
-</html>
+ */
+
+export const CSP_NONCE_META_NAME = "csp-nonce";
+
+declare global {
+  interface Window {
+    __CSP_NONCE__?: string;
+  }
+}
+
+const getTrimmedAttribute = (value: string | null | undefined): string | 
undefined => {
+  const trimmedValue = value?.trim();
+
+  return trimmedValue ? trimmedValue : undefined;
+};
+
+export const getCspNonce = (): string | undefined => {
+  const metaNonce = getTrimmedAttribute(
+    
document.querySelector(`meta[name="${CSP_NONCE_META_NAME}"]`)?.getAttribute("content")
+  );
+
+  if (metaNonce) {
+    return metaNonce;
+  }
+
+  const scriptNonce = getTrimmedAttribute(
+    document.querySelector("script[nonce]")?.getAttribute("nonce")
+  );
+
+  if (scriptNonce) {
+    return scriptNonce;
+  }
+
+  return getTrimmedAttribute(window.__CSP_NONCE__);
+};
diff --git a/dashboard/index.html b/dashboard/src/utils/emotionCache.ts
similarity index 68%
copy from dashboard/index.html
copy to dashboard/src/utils/emotionCache.ts
index 58a084be1c..27d94b953b 100644
--- a/dashboard/index.html
+++ b/dashboard/src/utils/emotionCache.ts
@@ -1,4 +1,4 @@
-<!--
+/*
  * Licensed to the Apache Software Foundation (ASF) under one or more
  * contributor license agreements.  See the NOTICE file distributed with
  * this work for additional information regarding copyright ownership.
@@ -13,19 +13,15 @@
  * 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.
--->
+ */
 
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Atlas</title>
-  </head>
+import createCache, { type EmotionCache } from "@emotion/cache";
 
-  <body>
-    <div id="root"></div>
-    <script type="module" src="/src/Main.tsx"></script>
-  </body>
-</html>
+export const createEmotionCache = (cspNonce?: string): EmotionCache => {
+  const trimmedNonce = cspNonce?.trim();
+
+  return createCache({
+    key: "css",
+    ...(trimmedNonce ? { nonce: trimmedNonce } : {})
+  });
+};

Reply via email to