diff --git a/packages/app/package.json b/packages/app/package.json
index 9401ca89be..79101dff70 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -142,7 +142,8 @@
"@types/react": "*",
"@types/react-dom": "*",
"@types/zen-observable": "^0.8.0",
- "cross-env": "^7.0.0"
+ "cross-env": "^7.0.0",
+ "msw": "^1.0.0"
},
"bundled": true
}
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 3d8bd45e5a..06851d6ac7 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -108,6 +108,7 @@ import { DevToolsPage } from '@backstage/plugin-devtools';
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities';
import { NotificationsPage } from '@backstage/plugin-notifications';
+import { AppMode } from './AppMode';
const app = createApp({
apis,
@@ -282,8 +283,10 @@ export default app.createRoot(
-
- {routes}
+
+
+ {routes}
+
>,
);
diff --git a/packages/app/src/AppMode.test.tsx b/packages/app/src/AppMode.test.tsx
new file mode 100644
index 0000000000..b110944e1e
--- /dev/null
+++ b/packages/app/src/AppMode.test.tsx
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * 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 React from 'react';
+import { screen, waitFor } from '@testing-library/react';
+import { rest } from 'msw';
+import { setupServer } from 'msw/node';
+import {
+ MockConfigApi,
+ TestApiProvider,
+ renderInTestApp,
+ setupRequestMockHandlers,
+} from '@backstage/test-utils';
+import { AppMode } from './AppMode';
+import {
+ configApiRef,
+ discoveryApiRef,
+ fetchApiRef,
+} from '@backstage/core-plugin-api';
+
+describe('AppMode', () => {
+ const worker = setupServer();
+ setupRequestMockHandlers(worker);
+
+ const configApiMock = new MockConfigApi({
+ backend: {
+ baseUrl: 'http://localhost:7000',
+ },
+ });
+
+ const fetchApiMock = {
+ fetch: jest.fn(),
+ };
+
+ const discoveryApiMock = {
+ getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the progress component while loading', async () => {
+ fetchApiMock.fetch.mockReturnValueOnce(new Promise(() => {}));
+ await renderInTestApp(
+
+ Test content
+ ,
+ );
+ expect(screen.getByTestId('progress')).toBeVisible();
+ expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
+ });
+
+ it('should render the children even when there is an error', async () => {
+ const error = new Error('Failed to fetch');
+ fetchApiMock.fetch.mockRejectedValueOnce(error);
+ await renderInTestApp(
+
+ Test content
+ ,
+ );
+ await waitFor(() =>
+ expect(screen.getByText('Test content')).toBeInTheDocument(),
+ );
+ expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
+ expect(screen.queryByText('Failed to fetch')).not.toBeInTheDocument();
+ });
+
+ it('should render the children even when the public index is not available', async () => {
+ fetchApiMock.fetch.mockResolvedValueOnce({ ok: false });
+ await renderInTestApp(
+
+ Test content
+ ,
+ );
+ await waitFor(() =>
+ expect(screen.getByText('Test content')).toBeInTheDocument(),
+ );
+ expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
+ });
+
+ it('should render the children also when the public index is available', async () => {
+ fetchApiMock.fetch.mockResolvedValueOnce({ ok: true });
+ await renderInTestApp(Test content);
+ await waitFor(() =>
+ expect(screen.getByText('Test content')).toBeInTheDocument(),
+ );
+ expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
+ });
+
+ it('should render the children wrapped in the CookieAuthRefreshProvider', async () => {
+ worker.use(
+ rest.get('http://localhost:7000/public/index.html', (_, res, ctx) => {
+ return res(ctx.status(200));
+ }),
+ rest.get(
+ 'http://localhost:7000/app/.backstage/v1-cookie',
+ (_, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({ expiresAt: Date.now() + 10 * 60 * 1000 }),
+ );
+ },
+ ),
+ );
+ await renderInTestApp(
+
+ Test content
+ ,
+ );
+ await waitFor(() =>
+ expect(screen.getByText('Test content')).toBeInTheDocument(),
+ );
+ });
+});
diff --git a/packages/app/src/AppMode.tsx b/packages/app/src/AppMode.tsx
new file mode 100644
index 0000000000..2b11064c65
--- /dev/null
+++ b/packages/app/src/AppMode.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * 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 React, { ReactNode } from 'react';
+import useAsync from 'react-use/lib/useAsync';
+import {
+ configApiRef,
+ fetchApiRef,
+ useApi,
+ useApp,
+} from '@backstage/core-plugin-api';
+import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react';
+
+export function AppMode(props: { children: ReactNode }): JSX.Element {
+ const { children } = props;
+ const fetchApi = useApi(fetchApiRef);
+ const configApi = useApi(configApiRef);
+ const Components = useApp().getComponents();
+
+ const { loading, error, value } = useAsync(async () => {
+ const baseUrl = configApi.getString('backend.baseUrl');
+ const response = await fetchApi.fetch(`${baseUrl}/public/index.html`);
+ return response.ok;
+ }, [fetchApi, configApi]);
+
+ if (loading) {
+ return ;
+ }
+
+ // Request failed, or the public index is not available
+ if (error || !value) {
+ return <>{children}>;
+ }
+
+ // The public index is available
+ // That means the app is running in public experimental mode
+ return (
+
+ {children}
+
+ );
+}
diff --git a/yarn.lock b/yarn.lock
index ce9665c22d..ffc26aff2b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -27526,6 +27526,7 @@ __metadata:
"@vitejs/plugin-react": ^4.0.4
cross-env: ^7.0.0
history: ^5.0.0
+ msw: ^1.0.0
react: ^18.0.2
react-dom: ^18.0.2
react-router: ^6.3.0