Merge pull request #30696 from backstage/rugvip/public-alpha
frontend-defaults: move createPublicSignInApp to alpha
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-defaults': patch
|
||||
---
|
||||
|
||||
Deprecated `createPublicSignInApp`, which has been replaced by the new `appModulePublicSignIn` from `@backstage/plugin-app/alpha` instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-app': patch
|
||||
---
|
||||
|
||||
Added a new module for implementing public sign-in apps, exported as `appModulePublicSignIn` via the `/alpha` sub-path export. This replaces the `createPublicSignInApp` export from `@backstage/frontend-defaults`, which is now deprecated.
|
||||
@@ -15,9 +15,13 @@
|
||||
*/
|
||||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createPublicSignInApp } from '@backstage/frontend-defaults';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import { appModulePublicSignIn } from '@backstage/plugin-app/alpha';
|
||||
|
||||
import '@backstage/ui/css/styles.css';
|
||||
|
||||
const app = createPublicSignInApp();
|
||||
const app = createApp({
|
||||
features: [appModulePublicSignIn],
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot());
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface CreateAppOptions {
|
||||
pluginInfoResolver?: FrontendPluginInfoResolver;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated (undocumented)
|
||||
export function createPublicSignInApp(options?: CreateAppOptions): {
|
||||
createRoot(): JSX_2;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
* Copyright 2025 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.
|
||||
@@ -14,94 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
coreExtensionData,
|
||||
createFrontendModule,
|
||||
identityApiRef,
|
||||
useApi,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
import { appModulePublicSignIn } from '@backstage/plugin-app/alpha';
|
||||
import { CreateAppOptions, createApp } from './createApp';
|
||||
import appPlugin from '@backstage/plugin-app';
|
||||
|
||||
// This is a copy of the CookieAuthRedirect component from the auth-react
|
||||
// plugin, to avoid a dependency on that package. Long-term we want this to be
|
||||
// the only implementation and remove the one in auth-react once the old frontend system is gone.
|
||||
|
||||
// TODO(Rugvip): Should this be part of the app plugin instead? since it owns the backend part of it.
|
||||
|
||||
/** @internal */
|
||||
export function InternalCookieAuthRedirect() {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
const [state, actions] = useAsync(async () => {
|
||||
const { token } = await identityApi.getCredentials();
|
||||
if (!token) {
|
||||
throw new Error('Expected Backstage token in sign-in response');
|
||||
}
|
||||
return token;
|
||||
});
|
||||
|
||||
useMountEffect(actions.execute);
|
||||
|
||||
if (state.status === 'error' && state.error) {
|
||||
return <>An error occurred: {state.error.message}</>;
|
||||
}
|
||||
|
||||
if (state.status === 'success' && state.result) {
|
||||
return (
|
||||
<form
|
||||
ref={form => form?.submit()}
|
||||
action={window.location.href}
|
||||
method="POST"
|
||||
style={{ visibility: 'hidden' }}
|
||||
>
|
||||
<input type="hidden" name="type" value="sign-in" />
|
||||
<input type="hidden" name="token" value={state.result} />
|
||||
<input type="submit" value="Continue" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an app that is suitable for the public sign-in page, for use in the `index-public-experimental.tsx` file.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This app has an override for the `app/layout` extension, which means that
|
||||
* most extension typically installed in an app will be ignored. However, you
|
||||
* can still for example install API and root element extensions.
|
||||
*
|
||||
* A typical setup of this app will only install a custom sign-in page.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const app = createPublicSignInApp({
|
||||
* features: [signInPageModule],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link @backstage/plugin-app/alpha#appModulePublicSignIn} instead.
|
||||
*/
|
||||
export function createPublicSignInApp(options?: CreateAppOptions) {
|
||||
return createApp({
|
||||
...options,
|
||||
features: [
|
||||
...(options?.features ?? []),
|
||||
// This is a rather than app plugin override in order for it to take precedence over any supplied app plugin override
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
appPlugin.getExtension('app/layout').override({
|
||||
factory: () => [
|
||||
coreExtensionData.reactElement(<InternalCookieAuthRedirect />),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
features: [...(options?.features ?? []), appModulePublicSignIn],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,6 +20,21 @@
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha/index.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha/index.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
@@ -47,12 +60,15 @@
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@react-hookz/web": "^24.0.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/frontend-defaults": "workspace:^",
|
||||
"@backstage/frontend-test-utils": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
## API Report File for "@backstage/plugin-app"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { FrontendModule } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha
|
||||
export const appModulePublicSignIn: FrontendModule;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 {
|
||||
SignInPageBlueprint,
|
||||
createFrontendModule,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { useEffect } from 'react';
|
||||
import { appModulePublicSignIn } from './appModulePublicSignIn';
|
||||
import { mockApis } from '@backstage/test-utils';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
|
||||
describe('appModulePublicSignIn', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should render a sign-in page', async () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
features: [
|
||||
appModulePublicSignIn,
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
SignInPageBlueprint.make({
|
||||
params: {
|
||||
loader: async () => () => <div>Sign in page</div>,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByText('Sign in page'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the form redirect on sign-in', async () => {
|
||||
const submitSpy = jest
|
||||
.spyOn(HTMLFormElement.prototype, 'submit')
|
||||
.mockReturnValue();
|
||||
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
features: [
|
||||
appModulePublicSignIn,
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
SignInPageBlueprint.make({
|
||||
params: {
|
||||
loader:
|
||||
async () =>
|
||||
({ onSignInSuccess }) => {
|
||||
useEffect(() => {
|
||||
onSignInSuccess(
|
||||
mockApis.identity({ token: 'mock-token' }),
|
||||
);
|
||||
}, [onSignInSuccess]);
|
||||
return <div />;
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { baseElement } = render(app.createRoot());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(baseElement).toMatchInlineSnapshot(`
|
||||
<body
|
||||
data-theme-mode="light"
|
||||
data-theme-name="backstage"
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
action="http://localhost/"
|
||||
method="POST"
|
||||
style="visibility: hidden;"
|
||||
>
|
||||
<input
|
||||
name="type"
|
||||
type="hidden"
|
||||
value="sign-in"
|
||||
/>
|
||||
<input
|
||||
name="token"
|
||||
type="hidden"
|
||||
value="mock-token"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
value="Continue"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2025 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 appPlugin from '@backstage/plugin-app';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createFrontendModule,
|
||||
identityApiRef,
|
||||
useApi,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
// This is a copy of the CookieAuthRedirect component from the auth-react
|
||||
// plugin, to avoid a dependency on that package. Long-term we want this to be
|
||||
// the only implementation and remove the one in auth-react once the old frontend system is gone.
|
||||
|
||||
// TODO(Rugvip): Should this be part of the app plugin instead? since it owns the backend part of it.
|
||||
|
||||
/** @internal */
|
||||
export function InternalCookieAuthRedirect() {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
const [state, actions] = useAsync(async () => {
|
||||
const { token } = await identityApi.getCredentials();
|
||||
if (!token) {
|
||||
throw new Error('Expected Backstage token in sign-in response');
|
||||
}
|
||||
return token;
|
||||
});
|
||||
|
||||
useMountEffect(actions.execute);
|
||||
|
||||
if (state.status === 'error' && state.error) {
|
||||
return <>An error occurred: {state.error.message}</>;
|
||||
}
|
||||
|
||||
if (state.status === 'success' && state.result) {
|
||||
return (
|
||||
<form
|
||||
ref={form => form?.submit()}
|
||||
action={window.location.href}
|
||||
method="POST"
|
||||
style={{ visibility: 'hidden' }}
|
||||
>
|
||||
<input type="hidden" name="type" value="sign-in" />
|
||||
<input type="hidden" name="token" value={state.result} />
|
||||
<input type="submit" value="Continue" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This module is intended for use in public sign-in page apps, in the
|
||||
* `index-public-experimental.tsx` file.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This module is used to enable the public sign-in flow where the build output
|
||||
* is split into one small publicly accessible app, and the full app protected
|
||||
* by auth.
|
||||
*
|
||||
* This module overrides the `app/layout` extension, which means that most
|
||||
* extension typically installed in an app will be ignored. However, you can
|
||||
* still for example install API and root element extensions.
|
||||
*
|
||||
* A typical setup of this app will only install a custom sign-in page.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
*#### In `index-public-experimental.tsx`
|
||||
*
|
||||
*```ts
|
||||
*import { createApp } from '@backstage/frontend-defaults
|
||||
*import { appModulePublicSignIn } from '@backstage/plugin-app/alpha';
|
||||
*import { appModuleSignInPage } from './appModuleSignInPage';
|
||||
*
|
||||
*const app = createApp({
|
||||
* features: [appModuleSignInPage, appModulePublicSignIn],
|
||||
*});
|
||||
*```
|
||||
*
|
||||
*#### In `appModuleSignInPage.tsx`
|
||||
*
|
||||
*```tsx
|
||||
*import { createFrontendModule, SignInPageBlueprint } from '@backstage/frontend-plugin-api';
|
||||
*
|
||||
*export const appModuleSignInPage = createFrontendModule({
|
||||
* pluginId: 'app',
|
||||
* extensions: [
|
||||
* SignInPageBlueprint.make({
|
||||
* params: {
|
||||
* ...
|
||||
* }
|
||||
* }),
|
||||
* ],
|
||||
*})
|
||||
*```
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const appModulePublicSignIn = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
appPlugin.getExtension('app/layout').override({
|
||||
factory: () => [
|
||||
coreExtensionData.reactElement(<InternalCookieAuthRedirect />),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 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 { appModulePublicSignIn } from './appModulePublicSignIn';
|
||||
@@ -4743,15 +4743,18 @@ __metadata:
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/dev-utils": "workspace:^"
|
||||
"@backstage/frontend-defaults": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/frontend-test-utils": "workspace:^"
|
||||
"@backstage/integration-react": "workspace:^"
|
||||
"@backstage/plugin-permission-react": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@material-ui/core": "npm:^4.9.13"
|
||||
"@material-ui/icons": "npm:^4.9.1"
|
||||
"@material-ui/lab": "npm:^4.0.0-alpha.61"
|
||||
"@react-hookz/web": "npm:^24.0.0"
|
||||
"@testing-library/jest-dom": "npm:^6.0.0"
|
||||
"@testing-library/react": "npm:^16.0.0"
|
||||
"@testing-library/user-event": "npm:^14.0.0"
|
||||
|
||||
Reference in New Issue
Block a user