Merge pull request #23512 from backstage/camilaibs/create-generic-auth-cookie-provider
[Auth] Create generic auth cookie provider
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-react': patch
|
||||
---
|
||||
|
||||
Create a generic React component for refreshing user cookie.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-techdocs-addons-test-utils': patch
|
||||
---
|
||||
|
||||
Use the new generic refresh user cookie provider.
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# @backstage/plugin-auth-react
|
||||
|
||||
Welcome to the web library package for the auth plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,39 @@
|
||||
## API Report File for "@backstage/plugin-auth-react"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// @public
|
||||
export function CookieAuthRefreshProvider(
|
||||
props: CookieAuthRefreshProviderProps,
|
||||
): JSX.Element;
|
||||
|
||||
// @public
|
||||
export type CookieAuthRefreshProviderProps = {
|
||||
pluginId: string;
|
||||
path?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function useCookieAuthRefresh(options: {
|
||||
pluginId: string;
|
||||
path?: string;
|
||||
}):
|
||||
| {
|
||||
status: 'loading';
|
||||
}
|
||||
| {
|
||||
status: 'error';
|
||||
error: Error;
|
||||
retry: () => void;
|
||||
}
|
||||
| {
|
||||
status: 'success';
|
||||
data: {
|
||||
expiresAt: string;
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-auth-react
|
||||
title: '@backstage/plugin-auth-react'
|
||||
description: Web library for the auth plugin
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-web-library
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-react",
|
||||
"version": "0.0.0",
|
||||
"description": "Web library for the auth plugin",
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/auth-react"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"sideEffects": false,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@react-hookz/web": "^24.0.0",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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 userEvent from '@testing-library/user-event';
|
||||
import { CookieAuthRefreshProvider } from './CookieAuthRefreshProvider';
|
||||
import {
|
||||
MockStorageApi,
|
||||
TestApiProvider,
|
||||
renderInTestApp,
|
||||
} from '@backstage/test-utils';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
describe('CookieAuthRefreshProvider', () => {
|
||||
const storageApiMock = MockStorageApi.create();
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('http://localhost:7000/techdocs/api'),
|
||||
};
|
||||
|
||||
function getExpiresAtInFuture() {
|
||||
const tenMinutesInMilliseconds = 10 * 60 * 1000;
|
||||
return new Date(Date.now() + tenMinutesInMilliseconds).toISOString();
|
||||
}
|
||||
|
||||
it('should render a progress bar', async () => {
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockReturnValue(new Promise(() => {})),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('progress')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render an error panel', async () => {
|
||||
const error = new Error('Failed to get cookie');
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockRejectedValue(error),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(error.message)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call the api again when retry is clicked', async () => {
|
||||
const error = new Error('Failed to get cookie');
|
||||
const fetchApiMock = {
|
||||
fetch: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(error)
|
||||
.mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
expiresAt: getExpiresAtInFuture(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(error.message)).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('Retry'));
|
||||
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the children', async () => {
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
expiresAt: getExpiresAtInFuture(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { ErrorPanel } from '@backstage/core-components';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { useCookieAuthRefresh } from '../../hooks';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Props for the {@link CookieAuthRefreshProvider} component.
|
||||
*/
|
||||
export type CookieAuthRefreshProviderProps = {
|
||||
// The plugin ID used for discovering the API origin
|
||||
pluginId: string;
|
||||
// The path used for calling the refresh cookie endpoint, default to '/cookie'
|
||||
path?: string;
|
||||
// The children to render when the refresh is successful
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A provider that will refresh the cookie when it is about to expire.
|
||||
*/
|
||||
export function CookieAuthRefreshProvider(
|
||||
props: CookieAuthRefreshProviderProps,
|
||||
): JSX.Element {
|
||||
const { children, ...options } = props;
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
|
||||
const result = useCookieAuthRefresh(options);
|
||||
|
||||
if (result.status === 'loading') {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
return (
|
||||
<ErrorPanel error={result.error}>
|
||||
<Button variant="outlined" onClick={result.retry}>
|
||||
Retry
|
||||
</Button>
|
||||
</ErrorPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
CookieAuthRefreshProvider,
|
||||
type CookieAuthRefreshProviderProps,
|
||||
} from './CookieAuthRefreshProvider';
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// The index file in ./components/ is typically responsible for selecting
|
||||
// which components are public API and should be exported from the package.
|
||||
|
||||
export * from './CookieAuthRefreshProvider';
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// The index file in ./hooks/ is typically responsible for selecting
|
||||
// which hooks are public API and should be exported from the package.
|
||||
|
||||
export * from './useCookieAuthRefresh';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { useCookieAuthRefresh } from './useCookieAuthRefresh';
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* 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 { renderHook, waitFor } from '@testing-library/react';
|
||||
import { fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { useCookieAuthRefresh } from './useCookieAuthRefresh';
|
||||
|
||||
describe('useCookieAuthRefresh', () => {
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('http://localhost:7000/techdocs/api'),
|
||||
};
|
||||
|
||||
const now = 1710316886171;
|
||||
const tenMinutesInMilliseconds = 10 * 60 * 1000;
|
||||
const tenMinutesFromNowInMilliseconds = now + tenMinutesInMilliseconds;
|
||||
const expiresAt = new Date(tenMinutesFromNowInMilliseconds).toISOString();
|
||||
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ now });
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return a loading status when the refresh is in progress first time', () => {
|
||||
const error = new Error('Failed to get cookie');
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
fetchApiRef,
|
||||
{
|
||||
fetch: jest.fn().mockRejectedValue(error),
|
||||
},
|
||||
],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.status).toBe('loading');
|
||||
});
|
||||
|
||||
it('should return a loading status when retrying without previous success', async () => {
|
||||
const error = new Error('Failed to get cookie');
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
fetchApiRef,
|
||||
{
|
||||
fetch: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(error)
|
||||
.mockReturnValue(new Promise(() => {})),
|
||||
},
|
||||
],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toStrictEqual({ status: 'loading' });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'error',
|
||||
error,
|
||||
retry: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
if (result.current.status === 'error') {
|
||||
result.current.retry();
|
||||
}
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'loading',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a loading status when retrying with previous success', async () => {
|
||||
const error = new Error('Failed to get cookie');
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
fetchApiRef,
|
||||
{
|
||||
fetch: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
})
|
||||
.mockRejectedValueOnce(error)
|
||||
.mockReturnValue(new Promise(() => {})),
|
||||
},
|
||||
],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toStrictEqual({ status: 'loading' });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'success',
|
||||
data: { expiresAt },
|
||||
}),
|
||||
);
|
||||
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'error',
|
||||
error,
|
||||
retry: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
if (result.current.status === 'error') {
|
||||
result.current.retry();
|
||||
}
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'loading',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an error status when the refresh has failed', async () => {
|
||||
const error = new Error('Failed to get cookie');
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
fetchApiRef,
|
||||
{
|
||||
fetch: jest.fn().mockRejectedValue(error),
|
||||
},
|
||||
],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'error',
|
||||
error,
|
||||
retry: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the api to get the cookie and use it', async () => {
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'success',
|
||||
data: { expiresAt },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should cancel the refresh when a message is received from another tab', async () => {
|
||||
const pluginId = 'techdocs';
|
||||
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
const twentyMinutesFromNowInMilliseconds =
|
||||
now + 2 * tenMinutesInMilliseconds;
|
||||
|
||||
// simulating other tab refreshing the cookie
|
||||
new global.BroadcastChannel(
|
||||
`${pluginId}-auth-cookie-expires-at`,
|
||||
).postMessage({
|
||||
action: 'COOKIE_REFRESH_SUCCESS',
|
||||
payload: {
|
||||
expiresAt: new Date(twentyMinutesFromNowInMilliseconds).toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
// advance the timers in 10 minutes to match the old expires at
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
// should not call the api
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
// advance the timers in more 10 minutes to match the new expires at
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds - 1000);
|
||||
|
||||
// should call the api
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it('should cancel the refresh when the component is unmounted', async () => {
|
||||
const { result, unmount } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toStrictEqual({
|
||||
status: 'success',
|
||||
data: { expiresAt },
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
unmount();
|
||||
|
||||
// advance the timers to ensure that the refresh is not called
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
// should not call the api after unmount
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).not.toHaveBeenCalledTimes(2),
|
||||
);
|
||||
});
|
||||
|
||||
it('should refresh the cookie when it is about to expire', async () => {
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId: 'techdocs' }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
// advance the timers to the expiration date
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds - 1000);
|
||||
|
||||
// should call the api
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 { useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A hook that will refresh the cookie when it is about to expire.
|
||||
* @param options - Options for configuring the refresh cookie endpoint
|
||||
*/
|
||||
export function useCookieAuthRefresh(options: {
|
||||
// The plugin id used for discovering the API origin
|
||||
pluginId: string;
|
||||
// The path used for calling the refresh cookie endpoint, default to '/cookie'
|
||||
path?: string;
|
||||
}):
|
||||
| { status: 'loading' }
|
||||
| { status: 'error'; error: Error; retry: () => void }
|
||||
| { status: 'success'; data: { expiresAt: string } } {
|
||||
const { pluginId, path = '/cookie' } = options ?? {};
|
||||
const fetchApi = useApi(fetchApiRef);
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
|
||||
const channel = useMemo(() => {
|
||||
return 'BroadcastChannel' in window
|
||||
? new BroadcastChannel(`${pluginId}-auth-cookie-expires-at`)
|
||||
: null;
|
||||
}, [pluginId]);
|
||||
|
||||
const [state, actions] = useAsync<{ expiresAt: string }>(async () => {
|
||||
const apiOrigin = await discoveryApi.getBaseUrl(pluginId);
|
||||
const requestUrl = `${apiOrigin}${path}`;
|
||||
const response = await fetchApi.fetch(`${requestUrl}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data.expiresAt) {
|
||||
throw new Error('No expiration date found in response');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
useMountEffect(actions.execute);
|
||||
|
||||
const retry = useCallback(() => {
|
||||
actions.execute();
|
||||
}, [actions]);
|
||||
|
||||
const refresh = useCallback(
|
||||
(params: { expiresAt: string }) => {
|
||||
// Randomize the refreshing margin with a margin of 1-4 minutes to avoid all tabs refreshing at the same time
|
||||
// It cannot be less than 5 minutes otherwise the backend will return the same expiration date
|
||||
const margin = (1 + 3 * Math.random()) * 60000;
|
||||
const delay = Date.parse(params.expiresAt) - Date.now() - margin;
|
||||
const timeout = setTimeout(retry, delay);
|
||||
return () => clearTimeout(timeout);
|
||||
},
|
||||
[retry],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Only schedule a refresh if we have a successful response
|
||||
if (state.status !== 'success' || !state.result) {
|
||||
return () => {};
|
||||
}
|
||||
channel?.postMessage({
|
||||
action: 'COOKIE_REFRESH_SUCCESS',
|
||||
payload: state.result,
|
||||
});
|
||||
let cancel = refresh(state.result);
|
||||
const listener = (
|
||||
event: MessageEvent<{ action: string; payload: { expiresAt: string } }>,
|
||||
) => {
|
||||
const { action, payload } = event.data;
|
||||
if (action === 'COOKIE_REFRESH_SUCCESS') {
|
||||
cancel();
|
||||
cancel = refresh(payload);
|
||||
}
|
||||
};
|
||||
channel?.addEventListener('message', listener);
|
||||
return () => {
|
||||
cancel();
|
||||
channel?.removeEventListener('message', listener);
|
||||
};
|
||||
}, [state, refresh, channel]);
|
||||
|
||||
// Initialising
|
||||
if (state.status === 'not-executed') {
|
||||
return { status: 'loading' };
|
||||
}
|
||||
|
||||
// First refresh or retrying without any success before
|
||||
// Possible state transitions:
|
||||
// e.g. not-executed -> loading (first-refresh)
|
||||
// e.g. not-executed -> loading (first-refresh) -> error -> loading (manual-retry)
|
||||
if (state.status === 'loading' && !state.result) {
|
||||
return { status: 'loading' };
|
||||
}
|
||||
|
||||
// Retrying after having succeeding at least once
|
||||
// Current state is: { status: 'loading', result: {...}, error: undefined | Error }
|
||||
// e.g. not-executed -> loading (first-refresh) -> success -> loading (scheduled-refresh) -> error -> loading (manual-retry)
|
||||
if (state.status === 'loading' && state.error) {
|
||||
return { status: 'loading' };
|
||||
}
|
||||
|
||||
// Something went wrong during any situation of a refresh
|
||||
if (state.status === 'error' && state.error) {
|
||||
return { status: 'error', error: state.error, retry };
|
||||
}
|
||||
|
||||
// At this point it should be safe to assume that we have a successful refresh
|
||||
return { status: 'success', data: state.result! };
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Web library for the auth plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// In this package you might for example export components or hooks
|
||||
// that are useful to other plugins or modules.
|
||||
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
|
||||
global.BroadcastChannel = jest
|
||||
.fn()
|
||||
.mockImplementation((_channelName: string) => {
|
||||
const listeners: ((event: { data: any }) => void)[] = [];
|
||||
|
||||
return {
|
||||
addEventListener: (
|
||||
type: string,
|
||||
listener: (event: { data: any }) => void,
|
||||
) => {
|
||||
if (type === 'message') {
|
||||
listeners.push(listener);
|
||||
}
|
||||
},
|
||||
removeEventListener: (
|
||||
type: string,
|
||||
listener: (event: { data: any }) => void,
|
||||
) => {
|
||||
if (type === 'message') {
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
postMessage: (message: any) => {
|
||||
listeners.forEach(listener => {
|
||||
listener({ data: message });
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -24,7 +24,11 @@ import { act, render } from '@testing-library/react';
|
||||
|
||||
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import {
|
||||
TechDocsAddons,
|
||||
@@ -69,6 +73,22 @@ const scmIntegrationsApi = {
|
||||
fromConfig: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
const discoveryApi = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('https://backstage.example.com/api/techdocs'),
|
||||
};
|
||||
|
||||
const fetchApi = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
// Expires in 10 minutes
|
||||
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
/** @ignore */
|
||||
type TechDocsAddonTesterTestApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
@@ -204,6 +224,8 @@ export class TechDocsAddonTester {
|
||||
*/
|
||||
build() {
|
||||
const apis: TechdocsAddonTesterApis<any[]> = [
|
||||
[fetchApiRef, fetchApi],
|
||||
[discoveryApiRef, discoveryApi],
|
||||
[techdocsApiRef, techdocsApi],
|
||||
[techdocsStorageApiRef, techdocsStorageApi],
|
||||
[searchApiRef, searchApi],
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/integration-react": "workspace:^",
|
||||
"@backstage/plugin-auth-react": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-search-common": "workspace:^",
|
||||
"@backstage/plugin-search-react": "workspace:^",
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* 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, useEffect, useState, useCallback } from 'react';
|
||||
import { ErrorPanel } from '@backstage/core-components';
|
||||
import { techdocsApiRef } from '@backstage/plugin-techdocs-react';
|
||||
import { useApi, useApp } from '@backstage/core-plugin-api';
|
||||
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import Button from '@material-ui/core/Button';
|
||||
|
||||
type TechDocsRefreshCookieMessage = MessageEvent<{
|
||||
action: string;
|
||||
payload: {
|
||||
expiresAt: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
function useTechDocsCookie() {
|
||||
const techdocsApi = useApi(techdocsApiRef);
|
||||
|
||||
const { retry, ...state } = useAsyncRetry(async () => {
|
||||
return await techdocsApi.getCookie();
|
||||
}, [techdocsApi]);
|
||||
|
||||
const refresh = useCallback(
|
||||
(expiresAt: string) => {
|
||||
// Randomize the refreshing margin to avoid all tabs refreshing at the same time
|
||||
const refreshingMargin = (1 + 3 * Math.random()) * 60000;
|
||||
const delay = Date.parse(expiresAt) - Date.now() - refreshingMargin;
|
||||
const timeout = setTimeout(retry, delay);
|
||||
return () => clearTimeout(timeout);
|
||||
},
|
||||
[retry],
|
||||
);
|
||||
|
||||
return { ...state, retry, refresh };
|
||||
}
|
||||
|
||||
export function TechDocsAuthProvider({ children }: { children: ReactNode }) {
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
|
||||
const [channel] = useState(
|
||||
() => new BroadcastChannel('techdocs-cookie-refresh'),
|
||||
);
|
||||
|
||||
const { loading, error, value, retry, refresh } = useTechDocsCookie();
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) return () => {};
|
||||
|
||||
channel.postMessage({
|
||||
action: 'TECHDOCS_COOKIE_REFRESHED',
|
||||
payload: value,
|
||||
});
|
||||
|
||||
let cancel = refresh(value.expiresAt);
|
||||
|
||||
const handleMessage = (event: TechDocsRefreshCookieMessage): void => {
|
||||
const { action, payload } = event.data;
|
||||
if (action === 'TECHDOCS_COOKIE_REFRESHED') {
|
||||
cancel();
|
||||
cancel = refresh(payload.expiresAt);
|
||||
}
|
||||
};
|
||||
|
||||
channel.addEventListener('message', handleMessage);
|
||||
|
||||
return () => {
|
||||
cancel();
|
||||
channel.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}, [value, refresh, channel]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPanel error={error}>
|
||||
<Button variant="outlined" onClick={retry}>
|
||||
Retry
|
||||
</Button>
|
||||
</ErrorPanel>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
+24
-25
@@ -34,7 +34,11 @@ import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
|
||||
import { Page } from '@backstage/core-components';
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
configApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
const mockEntityMetadata = {
|
||||
locationMetadata: {
|
||||
@@ -76,6 +80,22 @@ const techdocsStorageApiMock: jest.Mocked<typeof techdocsStorageApiRef.T> = {
|
||||
syncEntityDocs: jest.fn(),
|
||||
};
|
||||
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('https://localhost:7000/api/techdocs'),
|
||||
};
|
||||
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
// Expires in 10 minutes
|
||||
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const PageMock = () => {
|
||||
const { namespace, kind, name } = useParams();
|
||||
return <>{`PageMock: ${namespace}#${kind}#${name}`}</>;
|
||||
@@ -96,6 +116,8 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
[scmIntegrationsApiRef, {}],
|
||||
[configApiRef, configApi],
|
||||
[techdocsApiRef, techdocsApiMock],
|
||||
@@ -115,29 +137,6 @@ const mountedRoutes = {
|
||||
|
||||
describe('<TechDocsReaderPage />', () => {
|
||||
beforeEach(() => {
|
||||
type Listener = (event: { data: any }) => void;
|
||||
|
||||
global.BroadcastChannel = jest
|
||||
.fn()
|
||||
.mockImplementation((_channelName: string) => {
|
||||
let listeners: Listener[] = [];
|
||||
return {
|
||||
postMessage: jest.fn((message: any) => {
|
||||
listeners.forEach(listener => listener({ data: message }));
|
||||
}),
|
||||
addEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners.push(listener);
|
||||
}
|
||||
}),
|
||||
removeEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners = listeners.filter(l => l !== listener);
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
getEntityMetadata.mockResolvedValue(mockEntityMetadata);
|
||||
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
|
||||
getCookie.mockResolvedValue({
|
||||
@@ -147,7 +146,7 @@ describe('<TechDocsReaderPage />', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -35,7 +35,8 @@ import {
|
||||
getComponentData,
|
||||
useRouteRefParams,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { TechDocsAuthProvider } from './TechDocsAuthProvider';
|
||||
|
||||
import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react';
|
||||
|
||||
/* An explanation for the multiple ways of customizing the TechDocs reader page
|
||||
|
||||
@@ -178,17 +179,17 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
|
||||
// As explained above, "page" is configuration 4 and <TechDocsReaderLayout> is 1
|
||||
return (
|
||||
<TechDocsAuthProvider>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{(page as JSX.Element) || <TechDocsReaderLayout />}
|
||||
</TechDocsReaderPageProvider>
|
||||
</TechDocsAuthProvider>
|
||||
</CookieAuthRefreshProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// As explained above, a render function is configuration 3 and React element is 2
|
||||
return (
|
||||
<TechDocsAuthProvider>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{({ metadata, entityMetadata, onReady }) => (
|
||||
<div className="techdocs-reader-page">
|
||||
@@ -205,6 +206,6 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
</div>
|
||||
)}
|
||||
</TechDocsReaderPageProvider>
|
||||
</TechDocsAuthProvider>
|
||||
</CookieAuthRefreshProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,28 +17,3 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
Element.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
type Listener = (event: { data: any }) => void;
|
||||
|
||||
global.BroadcastChannel = jest
|
||||
.fn()
|
||||
.mockImplementation((_channelName: string) => {
|
||||
let listeners: Listener[] = [];
|
||||
return {
|
||||
postMessage: jest.fn((message: any) => {
|
||||
// Simulate message event for all listeners
|
||||
listeners.forEach(listener => listener({ data: message }));
|
||||
}),
|
||||
addEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners.push(listener);
|
||||
}
|
||||
}),
|
||||
removeEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners = listeners.filter(l => l !== listener);
|
||||
}
|
||||
}),
|
||||
close: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5110,6 +5110,26 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-auth-react@workspace:^, @backstage/plugin-auth-react@workspace:plugins/auth-react":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-auth-react@workspace:plugins/auth-react"
|
||||
dependencies:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@material-ui/core": ^4.9.13
|
||||
"@react-hookz/web": ^24.0.0
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^14.0.0
|
||||
"@testing-library/user-event": ^14.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-azure-devops-backend@workspace:^, @backstage/plugin-azure-devops-backend@workspace:plugins/azure-devops-backend":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-azure-devops-backend@workspace:plugins/azure-devops-backend"
|
||||
@@ -9868,6 +9888,7 @@ __metadata:
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/integration-react": "workspace:^"
|
||||
"@backstage/plugin-auth-react": "workspace:^"
|
||||
"@backstage/plugin-catalog-react": "workspace:^"
|
||||
"@backstage/plugin-search-common": "workspace:^"
|
||||
"@backstage/plugin-search-react": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user