Merge master and update toastApiRef to new ApiRef pattern
Resolve merge conflicts from master introducing the new ApiRef_2 type
pattern with `.with()` builder. Update toastApiRef to use the same
`createApiRef<T>().with({ id, pluginId })` pattern as all other API
refs, and regenerate API reports.
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
@@ -39,6 +39,7 @@ describe('AnalyticsBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "analytics",
|
||||
"name": "test",
|
||||
|
||||
@@ -43,6 +43,7 @@ describe('AppRootWrapperBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "app-root-wrapper",
|
||||
"name": undefined,
|
||||
|
||||
@@ -86,6 +86,7 @@ describe('NavContentBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "nav-content",
|
||||
"name": undefined,
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('RouterBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "app-router-component",
|
||||
"name": undefined,
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('SignInPageBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "sign-in-page",
|
||||
"name": undefined,
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('ThemeBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "theme",
|
||||
"name": "light",
|
||||
|
||||
@@ -54,6 +54,7 @@ describe('TranslationBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "translation",
|
||||
"name": "blob",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/filter-predicates": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/integration-react": "workspace:^",
|
||||
"@backstage/plugin-app-react": "workspace:^",
|
||||
|
||||
@@ -147,7 +147,7 @@ const appPlugin: OverridableFrontendPlugin<
|
||||
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
|
||||
{
|
||||
singleton: true;
|
||||
optional: false;
|
||||
optional: true;
|
||||
internal: false;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
JSX,
|
||||
} from 'react';
|
||||
import {
|
||||
ExtensionBoundary,
|
||||
coreExtensionData,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
@@ -73,6 +74,7 @@ export const AppRoot = createExtension({
|
||||
}),
|
||||
children: createExtensionInput([coreExtensionData.reactElement], {
|
||||
singleton: true,
|
||||
optional: true,
|
||||
}),
|
||||
elements: createExtensionInput([coreExtensionData.reactElement]),
|
||||
wrappers: createExtensionInput(
|
||||
@@ -83,7 +85,7 @@ export const AppRoot = createExtension({
|
||||
),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs, apis }) {
|
||||
factory({ inputs, apis, node }) {
|
||||
if (isProtectedApp()) {
|
||||
const identityApi = apis.get(identityApiRef);
|
||||
if (!identityApi) {
|
||||
@@ -105,9 +107,7 @@ export const AppRoot = createExtension({
|
||||
});
|
||||
}
|
||||
|
||||
let content: ReactNode = inputs.children.get(
|
||||
coreExtensionData.reactElement,
|
||||
);
|
||||
let content = inputs.children?.get(coreExtensionData.reactElement);
|
||||
|
||||
for (const wrapper of inputs.wrappers) {
|
||||
const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component);
|
||||
@@ -124,19 +124,21 @@ export const AppRoot = createExtension({
|
||||
|
||||
return [
|
||||
coreExtensionData.reactElement(
|
||||
<AppRouter
|
||||
SignInPageComponent={inputs.signInPage?.get(
|
||||
SignInPageBlueprint.dataRefs.component,
|
||||
)}
|
||||
RouterComponent={inputs.router?.get(
|
||||
RouterBlueprint.dataRefs.component,
|
||||
)}
|
||||
extraElements={inputs.elements?.map(el =>
|
||||
el.get(coreExtensionData.reactElement),
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</AppRouter>,
|
||||
<ExtensionBoundary node={node}>
|
||||
<AppRouter
|
||||
SignInPageComponent={inputs.signInPage?.get(
|
||||
SignInPageBlueprint.dataRefs.component,
|
||||
)}
|
||||
RouterComponent={inputs.router?.get(
|
||||
RouterBlueprint.dataRefs.component,
|
||||
)}
|
||||
extraElements={inputs.elements?.map(el =>
|
||||
el.get(coreExtensionData.reactElement),
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</AppRouter>
|
||||
</ExtensionBoundary>,
|
||||
),
|
||||
];
|
||||
},
|
||||
@@ -253,28 +255,30 @@ export function AppRouter(props: AppRouterProps) {
|
||||
|
||||
// If the app hasn't configured a sign-in page, we just continue as guest.
|
||||
if (!SignInPageComponent) {
|
||||
appIdentityProxy.setTarget(
|
||||
{
|
||||
getUserId: () => 'guest',
|
||||
getIdToken: async () => undefined,
|
||||
getProfile: () => ({
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
}),
|
||||
getProfileInfo: async () => ({
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
}),
|
||||
getBackstageIdentity: async () => ({
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/guest',
|
||||
ownershipEntityRefs: ['user:default/guest'],
|
||||
}),
|
||||
getCredentials: async () => ({}),
|
||||
signOut: async () => {},
|
||||
},
|
||||
{ signOutTargetUrl: basePath || '/' },
|
||||
);
|
||||
if (!isProtectedApp()) {
|
||||
appIdentityProxy.setTarget(
|
||||
{
|
||||
getUserId: () => 'guest',
|
||||
getIdToken: async () => undefined,
|
||||
getProfile: () => ({
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
}),
|
||||
getProfileInfo: async () => ({
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
}),
|
||||
getBackstageIdentity: async () => ({
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/guest',
|
||||
ownershipEntityRefs: ['user:default/guest'],
|
||||
}),
|
||||
getCredentials: async () => ({}),
|
||||
signOut: async () => {},
|
||||
},
|
||||
{ signOutTargetUrl: basePath || '/' },
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RouterComponent>
|
||||
|
||||
@@ -43,6 +43,7 @@ describe('CatalogFilterBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "catalog-filter",
|
||||
"name": undefined,
|
||||
|
||||
@@ -200,6 +200,7 @@ describe('EntityCardBlueprint', () => {
|
||||
},
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "entity-card",
|
||||
"name": "test",
|
||||
|
||||
@@ -215,6 +215,7 @@ describe('EntityContentBlueprint', () => {
|
||||
},
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "entity-content",
|
||||
"name": "test",
|
||||
|
||||
@@ -204,6 +204,7 @@ describe('EntityContextMenuItemBlueprint', () => {
|
||||
},
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "entity-context-menu-item",
|
||||
"name": "test",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
"dataloader": "^2.0.0",
|
||||
"swr": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2026 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 { ConfigReader } from '@backstage/config';
|
||||
import { mockApis } from '@backstage/test-utils';
|
||||
import {
|
||||
createPermission,
|
||||
PermissionClient,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { IdentityPermissionApi } from './IdentityPermissionApi';
|
||||
|
||||
describe('IdentityPermissionApi', () => {
|
||||
const permission = createPermission({
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should batch requests that arrive on the same tick', async () => {
|
||||
const authorizeSpy = jest
|
||||
.spyOn(PermissionClient.prototype, 'authorize')
|
||||
.mockResolvedValue([
|
||||
{ result: AuthorizeResult.ALLOW },
|
||||
{ result: AuthorizeResult.DENY },
|
||||
]);
|
||||
const api = IdentityPermissionApi.create({
|
||||
config: new ConfigReader({}),
|
||||
discovery: mockApis.discovery(),
|
||||
identity: mockApis.identity(),
|
||||
});
|
||||
|
||||
const firstRequest = { permission };
|
||||
const secondRequest = { permission };
|
||||
const [firstResponse, secondResponse] = await Promise.all([
|
||||
api.authorize(firstRequest),
|
||||
api.authorize(secondRequest),
|
||||
]);
|
||||
|
||||
expect(firstResponse.result).toBe(AuthorizeResult.ALLOW);
|
||||
expect(secondResponse.result).toBe(AuthorizeResult.DENY);
|
||||
expect(authorizeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(authorizeSpy).toHaveBeenCalledWith(
|
||||
[firstRequest, secondRequest],
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not cache requests across ticks', async () => {
|
||||
const authorizeSpy = jest
|
||||
.spyOn(PermissionClient.prototype, 'authorize')
|
||||
.mockResolvedValue([{ result: AuthorizeResult.ALLOW }]);
|
||||
const identityApi = mockApis.identity();
|
||||
const credentialsSpy = jest
|
||||
.spyOn(identityApi, 'getCredentials')
|
||||
.mockResolvedValueOnce({ token: 'first-token' })
|
||||
.mockResolvedValueOnce({ token: 'second-token' });
|
||||
const api = IdentityPermissionApi.create({
|
||||
config: new ConfigReader({}),
|
||||
discovery: mockApis.discovery(),
|
||||
identity: identityApi,
|
||||
});
|
||||
|
||||
const request = { permission };
|
||||
await api.authorize(request);
|
||||
await api.authorize(request);
|
||||
|
||||
expect(authorizeSpy).toHaveBeenCalledTimes(2);
|
||||
expect(credentialsSpy).toHaveBeenCalledTimes(2);
|
||||
expect(authorizeSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[request],
|
||||
expect.objectContaining({ token: 'first-token' }),
|
||||
);
|
||||
expect(authorizeSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[request],
|
||||
expect.objectContaining({ token: 'second-token' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import DataLoader from 'dataloader';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { PermissionApi } from './PermissionApi';
|
||||
import {
|
||||
@@ -24,20 +25,30 @@ import {
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
/**
|
||||
* The default implementation of the PermissionApi, which simply calls the authorize method of the given
|
||||
* {@link @backstage/plugin-permission-common#PermissionClient}.
|
||||
* The default implementation of the PermissionApi, which batches calls to
|
||||
* {@link @backstage/plugin-permission-common#PermissionClient} that are made
|
||||
* within the same microtask into a single HTTP request.
|
||||
* @public
|
||||
*/
|
||||
export class IdentityPermissionApi implements PermissionApi {
|
||||
private readonly permissionClient: PermissionClient;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly loader: DataLoader<
|
||||
AuthorizePermissionRequest,
|
||||
AuthorizePermissionResponse
|
||||
>;
|
||||
|
||||
private constructor(
|
||||
permissionClient: PermissionClient,
|
||||
identityApi: IdentityApi,
|
||||
) {
|
||||
this.permissionClient = permissionClient;
|
||||
this.identityApi = identityApi;
|
||||
this.loader = new DataLoader(
|
||||
async (requests: readonly AuthorizePermissionRequest[]) => {
|
||||
const credentials = await identityApi.getCredentials();
|
||||
return permissionClient.authorize([...requests], credentials);
|
||||
},
|
||||
{
|
||||
cache: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static create(options: {
|
||||
@@ -50,13 +61,12 @@ export class IdentityPermissionApi implements PermissionApi {
|
||||
return new IdentityPermissionApi(permissionClient, identity);
|
||||
}
|
||||
|
||||
async authorize(
|
||||
request: AuthorizePermissionRequest,
|
||||
): Promise<AuthorizePermissionResponse>;
|
||||
async authorize(
|
||||
request: AuthorizePermissionRequest,
|
||||
): Promise<AuthorizePermissionResponse> {
|
||||
const response = await this.permissionClient.authorize(
|
||||
[request],
|
||||
await this.identityApi.getCredentials(),
|
||||
);
|
||||
return response[0];
|
||||
return await this.loader.load(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export const createGitlabRepoPushAction: (options: {
|
||||
sourcePath?: string | undefined;
|
||||
targetPath?: string | undefined;
|
||||
token?: string | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'delete' | 'create' | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'create' | 'delete' | undefined;
|
||||
},
|
||||
{
|
||||
projectid: string;
|
||||
@@ -271,7 +271,7 @@ export const createPublishGitlabMergeRequestAction: (options: {
|
||||
sourcePath?: string | undefined;
|
||||
targetPath?: string | undefined;
|
||||
token?: string | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined;
|
||||
commitAction?: 'auto' | 'update' | 'create' | 'delete' | 'skip' | undefined;
|
||||
projectid?: string | undefined;
|
||||
removeSourceBranch?: boolean | undefined;
|
||||
assignee?: string | undefined;
|
||||
|
||||
@@ -200,7 +200,9 @@ export type FormFieldExtensionData<
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const formFieldsApiRef: ApiRef<ScaffolderFormFieldsApi>;
|
||||
export const formFieldsApiRef: ApiRef<ScaffolderFormFieldsApi> & {
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type FormValidation = {
|
||||
|
||||
@@ -208,7 +208,9 @@ export type ReviewStepProps = {
|
||||
export type ScaffolderApi = ScaffolderApi_2;
|
||||
|
||||
// @public (undocumented)
|
||||
export const scaffolderApiRef: ApiRef<ScaffolderApi_2>;
|
||||
export const scaffolderApiRef: ApiRef<ScaffolderApi_2> & {
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type ScaffolderDryRunOptions = ScaffolderDryRunOptions_2;
|
||||
|
||||
@@ -479,7 +479,9 @@ export const formDecoratorsApi: OverridableExtensionDefinition<{
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const formDecoratorsApiRef: ApiRef<ScaffolderFormDecoratorsApi>;
|
||||
export const formDecoratorsApiRef: ApiRef<ScaffolderFormDecoratorsApi> & {
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
};
|
||||
|
||||
export { formFieldsApiRef };
|
||||
|
||||
|
||||
@@ -528,7 +528,9 @@ export type RouterProps = {
|
||||
export type ScaffolderApi = ScaffolderApi_2;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const scaffolderApiRef: ApiRef<ScaffolderApi_2>;
|
||||
export const scaffolderApiRef: ApiRef<ScaffolderApi_2> & {
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export class ScaffolderClient extends ScaffolderClient_2 {}
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('SearchFilterBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "search-filter",
|
||||
"name": "test",
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('SearchFilterResultTypeBlueprint', () => {
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "search-filter-result-type",
|
||||
"name": "test",
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('SearchResultListItemBlueprint', () => {
|
||||
},
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"if": undefined,
|
||||
"inputs": {},
|
||||
"kind": "search-result-list-item",
|
||||
"name": "test",
|
||||
|
||||
Reference in New Issue
Block a user