From 0410fc9ce605f1495870eaee1715a33e706cf58b Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Tue, 30 Apr 2024 11:49:36 +0300 Subject: [PATCH 001/204] feat: allow setting notification read from web/snackbar when opening link from snackbar or web notification, the notification can be automatically set as read like in the notifications page Signed-off-by: Heikki Hellgren --- .changeset/ten-penguins-roll.md | 5 ++++ plugins/notifications/api-report.md | 6 ++++- .../NotificationsSideBarItem.tsx | 23 +++++++++++++++++-- .../src/hooks/useWebNotifications.ts | 23 ++++++++++++++++--- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 .changeset/ten-penguins-roll.md diff --git a/.changeset/ten-penguins-roll.md b/.changeset/ten-penguins-roll.md new file mode 100644 index 0000000000..60e0ae773a --- /dev/null +++ b/.changeset/ten-penguins-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Allow setting notification as read when opening snackbar or web notification link diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 9240bdb1c2..2547204c7b 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -103,6 +103,7 @@ export const NotificationsSidebarItem: (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; + markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; @@ -182,7 +183,10 @@ export function useTitleCounter(): { }; // @public (undocumented) -export function useWebNotifications(enabled: boolean): { +export function useWebNotifications( + enabled: boolean, + markAsReadOnLinkOpen: boolean, +): { sendWebNotification: (options: { id: string; title: string; diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 7b55c63747..6e1f7c7d56 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -84,6 +84,7 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; + markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; @@ -95,6 +96,7 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled = true, snackbarEnabled = true, snackbarAutoHideDuration = 10000, + markAsReadOnLinkOpen = false, icon = NotificationsIcon, text = 'Notifications', ...restProps @@ -103,6 +105,7 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled: true, snackbarEnabled: true, snackbarAutoHideDuration: 10000, + markAsReadOnLinkOpen: false, }; const { loading, error, value, retry } = useNotificationsApi(api => @@ -114,7 +117,10 @@ export const NotificationsSidebarItem = (props?: { const notificationsRoute = useRouteRef(rootRouteRef); // TODO: Do we want to add long polling in case signals are not available const { lastSignal } = useSignal('notifications'); - const { sendWebNotification } = useWebNotifications(webNotificationsEnabled); + const { sendWebNotification } = useWebNotifications( + webNotificationsEnabled, + markAsReadOnLinkOpen, + ); const [refresh, setRefresh] = React.useState(false); const { setNotificationCount } = useTitleCounter(); @@ -126,6 +132,19 @@ export const NotificationsSidebarItem = (props?: { component={Link} to={notification.payload.link ?? notificationsRoute()} onClick={() => { + if (markAsReadOnLinkOpen) { + notificationsApi + .updateNotifications({ + ids: [notification.id], + read: true, + }) + .catch(() => { + alertApi.post({ + message: 'Failed to mark notification as read', + severity: 'error', + }); + }); + } closeSnackbar(snackBarId); }} > @@ -156,7 +175,7 @@ export const NotificationsSidebarItem = (props?: { return { action }; }, - [notificationsRoute, notificationsApi, alertApi], + [notificationsRoute, markAsReadOnLinkOpen, notificationsApi, alertApi], ); useEffect(() => { diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 728d992130..92a0a9b806 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -15,14 +15,19 @@ */ import { useCallback, useEffect, useState } from 'react'; import { rootRouteRef } from '../routes'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; +import { notificationsApiRef } from '../api'; /** @public */ -export function useWebNotifications(enabled: boolean) { +export function useWebNotifications( + enabled: boolean, + markAsReadOnLinkOpen: boolean, +) { const [webNotificationPermission, setWebNotificationPermission] = useState('default'); const notificationsRoute = useRouteRef(rootRouteRef); + const notificationsApi = useApi(notificationsApiRef); const navigate = useNavigate(); useEffect(() => { @@ -57,6 +62,12 @@ export function useWebNotifications(enabled: boolean) { event.preventDefault(); if (options.link) { window.open(options.link, '_blank'); + if (markAsReadOnLinkOpen) { + notificationsApi.updateNotifications({ + ids: [options.id], + read: true, + }); + } } else { navigate(notificationsRoute()); } @@ -65,7 +76,13 @@ export function useWebNotifications(enabled: boolean) { return notification; }, - [webNotificationPermission, navigate, notificationsRoute], + [ + webNotificationPermission, + markAsReadOnLinkOpen, + notificationsApi, + navigate, + notificationsRoute, + ], ); return { sendWebNotification }; From a880e941aaf1382189169b3416df51ec4b2c0b6c Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 30 May 2024 15:45:38 +0300 Subject: [PATCH 002/204] fix: change notification hooks to internal Signed-off-by: Heikki Hellgren --- plugins/notifications/api-report.md | 18 ------------------ .../notifications/src/hooks/useTitleCounter.ts | 2 +- .../src/hooks/useWebNotifications.ts | 2 +- plugins/notifications/src/index.ts | 2 +- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 2547204c7b..c431664883 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -177,23 +177,5 @@ export function useNotificationsApi( value: T; }; -// @public (undocumented) -export function useTitleCounter(): { - setNotificationCount: (newCount: number) => void; -}; - -// @public (undocumented) -export function useWebNotifications( - enabled: boolean, - markAsReadOnLinkOpen: boolean, -): { - sendWebNotification: (options: { - id: string; - title: string; - description: string; - link?: string; - }) => Notification | null; -}; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/notifications/src/hooks/useTitleCounter.ts b/plugins/notifications/src/hooks/useTitleCounter.ts index 0793cd58d6..399bfb84f4 100644 --- a/plugins/notifications/src/hooks/useTitleCounter.ts +++ b/plugins/notifications/src/hooks/useTitleCounter.ts @@ -25,7 +25,7 @@ const throttledSetTitle = throttle((shownTitle: string) => { document.title = shownTitle; }, 100); -/** @public */ +/** @internal */ export function useTitleCounter() { const [title, setTitle] = useState(document.title); const [count, setCount] = useState(0); diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 92a0a9b806..3fbd3c5496 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -19,7 +19,7 @@ import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; import { notificationsApiRef } from '../api'; -/** @public */ +/** @internal */ export function useWebNotifications( enabled: boolean, markAsReadOnLinkOpen: boolean, diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index bb5d76a0cf..25565f1ae1 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -15,5 +15,5 @@ */ export { notificationsPlugin, NotificationsPage } from './plugin'; export * from './api'; -export * from './hooks'; +export { useNotificationsApi } from './hooks'; export * from './components'; From aa2d3e302acff82c078dcc40dd75fe17144fc5ef Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 30 May 2024 21:14:02 +0300 Subject: [PATCH 003/204] feat: make default functionality to mark notification read when opening link Signed-off-by: Heikki Hellgren --- .changeset/ten-penguins-roll.md | 4 +-- plugins/notifications/api-report.md | 1 - .../NotificationsSideBarItem.tsx | 13 ++++----- .../src/hooks/useWebNotifications.ts | 29 ++++++------------- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/.changeset/ten-penguins-roll.md b/.changeset/ten-penguins-roll.md index 60e0ae773a..152bb1524b 100644 --- a/.changeset/ten-penguins-roll.md +++ b/.changeset/ten-penguins-roll.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-notifications': patch +'@backstage/plugin-notifications': minor --- -Allow setting notification as read when opening snackbar or web notification link +By default, set notification as read when opening snackbar or web notification link diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index c431664883..5e06ae2616 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -103,7 +103,6 @@ export const NotificationsSidebarItem: (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; - markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 6e1f7c7d56..457893b016 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -84,7 +84,6 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; - markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; @@ -96,7 +95,6 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled = true, snackbarEnabled = true, snackbarAutoHideDuration = 10000, - markAsReadOnLinkOpen = false, icon = NotificationsIcon, text = 'Notifications', ...restProps @@ -105,7 +103,6 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled: true, snackbarEnabled: true, snackbarAutoHideDuration: 10000, - markAsReadOnLinkOpen: false, }; const { loading, error, value, retry } = useNotificationsApi(api => @@ -117,9 +114,8 @@ export const NotificationsSidebarItem = (props?: { const notificationsRoute = useRouteRef(rootRouteRef); // TODO: Do we want to add long polling in case signals are not available const { lastSignal } = useSignal('notifications'); - const { sendWebNotification } = useWebNotifications( + const { sendWebNotification, requestUserPermission } = useWebNotifications( webNotificationsEnabled, - markAsReadOnLinkOpen, ); const [refresh, setRefresh] = React.useState(false); const { setNotificationCount } = useTitleCounter(); @@ -132,7 +128,7 @@ export const NotificationsSidebarItem = (props?: { component={Link} to={notification.payload.link ?? notificationsRoute()} onClick={() => { - if (markAsReadOnLinkOpen) { + if (notification.payload.link) { notificationsApi .updateNotifications({ ids: [notification.id], @@ -175,7 +171,7 @@ export const NotificationsSidebarItem = (props?: { return { action }; }, - [notificationsRoute, markAsReadOnLinkOpen, notificationsApi, alertApi], + [notificationsRoute, notificationsApi, alertApi], ); useEffect(() => { @@ -278,6 +274,9 @@ export const NotificationsSidebarItem = (props?: { )} { + requestUserPermission(); + }} hasNotifications={!error && !!unreadCount} text={text} icon={icon} diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 3fbd3c5496..ec120a614d 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -13,24 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { rootRouteRef } from '../routes'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; import { notificationsApiRef } from '../api'; /** @internal */ -export function useWebNotifications( - enabled: boolean, - markAsReadOnLinkOpen: boolean, -) { +export function useWebNotifications(enabled: boolean) { const [webNotificationPermission, setWebNotificationPermission] = useState('default'); const notificationsRoute = useRouteRef(rootRouteRef); const notificationsApi = useApi(notificationsApiRef); const navigate = useNavigate(); - useEffect(() => { + const requestUserPermission = useCallback(() => { if ( enabled && 'Notification' in window && @@ -62,12 +59,10 @@ export function useWebNotifications( event.preventDefault(); if (options.link) { window.open(options.link, '_blank'); - if (markAsReadOnLinkOpen) { - notificationsApi.updateNotifications({ - ids: [options.id], - read: true, - }); - } + notificationsApi.updateNotifications({ + ids: [options.id], + read: true, + }); } else { navigate(notificationsRoute()); } @@ -76,14 +71,8 @@ export function useWebNotifications( return notification; }, - [ - webNotificationPermission, - markAsReadOnLinkOpen, - notificationsApi, - navigate, - notificationsRoute, - ], + [webNotificationPermission, notificationsApi, navigate, notificationsRoute], ); - return { sendWebNotification }; + return { sendWebNotification, requestUserPermission }; } From cdb630d9d860d811256535f5525c51446f9e4031 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 13 Jun 2024 14:05:27 +0300 Subject: [PATCH 004/204] feat: support for stream transport for debug purposes Signed-off-by: Heikki Hellgren --- .changeset/shy-games-poke.md | 5 +++++ .../README.md | 2 +- .../config.d.ts | 4 ++++ .../processor/NotificationsEmailProcessor.ts | 4 ++++ .../src/processor/transports/index.ts | 1 + .../src/processor/transports/stream.ts | 20 +++++++++++++++++++ 6 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .changeset/shy-games-poke.md create mode 100644 plugins/notifications-backend-module-email/src/processor/transports/stream.ts diff --git a/.changeset/shy-games-poke.md b/.changeset/shy-games-poke.md new file mode 100644 index 0000000000..1d2abff39c --- /dev/null +++ b/.changeset/shy-games-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +Add support for stream transport for debugging purposes diff --git a/plugins/notifications-backend-module-email/README.md b/plugins/notifications-backend-module-email/README.md index 7bbac05b62..1defad74b4 100644 --- a/plugins/notifications-backend-module-email/README.md +++ b/plugins/notifications-backend-module-email/README.md @@ -2,7 +2,7 @@ Adds support for sending Backstage notifications as emails to users. -Supports sending emails using `SMTP`, `SES`, or `sendmail`. +Supports sending emails using `SMTP`, `SES`, `sendmail`, or `stream` (for debugging purposes). ## Customizing email content diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index 92f3385d1b..56a238d0dd 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -79,6 +79,10 @@ export interface Config { * Newline style, defaults to 'unix' */ newline?: 'unix' | 'windows'; + } + | { + /** Only for debugging, disables the actual sending of emails */ + transport: 'stream'; }; /** * Sender email address diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index a78ebd3260..7085652b54 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -38,6 +38,7 @@ import { createSendmailTransport, createSesTransport, createSmtpTransport, + createStreamTransport, } from './transports'; import { UserEntity } from '@backstage/catalog-model'; import { compact } from 'lodash'; @@ -129,6 +130,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ); } else if (transport === 'sendmail') { this.transporter = createSendmailTransport(this.transportConfig); + } else if (transport === 'stream') { + this.transporter = createStreamTransport(); } else { throw new Error(`Unsupported transport: ${transport}`); } @@ -229,6 +232,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private async sendMail(options: Mail.Options) { try { + this.logger.debug(`Sending notification email to ${options.to}`); await this.transporter.sendMail(options); } catch (e) { this.logger.error(`Failed to send email to ${options.to}: ${e}`); diff --git a/plugins/notifications-backend-module-email/src/processor/transports/index.ts b/plugins/notifications-backend-module-email/src/processor/transports/index.ts index ccea9f77cc..9a679f3723 100644 --- a/plugins/notifications-backend-module-email/src/processor/transports/index.ts +++ b/plugins/notifications-backend-module-email/src/processor/transports/index.ts @@ -16,3 +16,4 @@ export { createSmtpTransport } from './smtp'; export { createSesTransport } from './ses'; export { createSendmailTransport } from './sendmail'; +export { createStreamTransport } from './stream'; diff --git a/plugins/notifications-backend-module-email/src/processor/transports/stream.ts b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts new file mode 100644 index 0000000000..26ed401366 --- /dev/null +++ b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts @@ -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. + */ +import { createTransport } from 'nodemailer'; + +export const createStreamTransport = () => { + return createTransport({ streamTransport: true }); +}; From 579afd0d3205548261334d476900721e19413561 Mon Sep 17 00:00:00 2001 From: Frank Kong Date: Fri, 14 Jun 2024 16:55:00 -0400 Subject: [PATCH 005/204] chore(auth-node): allow declarative signin resolvers to take precedence Signed-off-by: Frank Kong --- .changeset/neat-socks-cheer.md | 5 +++++ plugins/auth-node/src/oauth/createOAuthProviderFactory.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/neat-socks-cheer.md diff --git a/.changeset/neat-socks-cheer.md b/.changeset/neat-socks-cheer.md new file mode 100644 index 0000000000..5e2879b0a3 --- /dev/null +++ b/.changeset/neat-socks-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-node': patch +--- + +allow declarative sign in resolvers declared in `signInResolverFactories` to take precedence over the statically defined sign in resolvers in `signInResolver` for the `createOAuthProviderFactory`. diff --git a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts index a3387b7038..fccfcb94de 100644 --- a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts +++ b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts @@ -43,11 +43,10 @@ export function createOAuthProviderFactory(options: { return ctx => { return OAuthEnvironmentHandler.mapConfig(ctx.config, envConfig => { const signInResolver = - options.signInResolver ?? readDeclarativeSignInResolver({ config: envConfig, signInResolverFactories: options.signInResolverFactories ?? {}, - }); + }) ?? options.signInResolver; return createOAuthRouteHandlers({ authenticator: options.authenticator, From 54a0cc779b39f12aecb1c4e9439e18f2007ae83a Mon Sep 17 00:00:00 2001 From: Tavi Nolan Date: Mon, 24 Jun 2024 18:09:57 +0100 Subject: [PATCH 006/204] Creating dry run documentation Signed-off-by: Tavi Nolan --- docs/plugins/dry-run-testing.md | 135 ++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 136 insertions(+) create mode 100644 docs/plugins/dry-run-testing.md diff --git a/docs/plugins/dry-run-testing.md b/docs/plugins/dry-run-testing.md new file mode 100644 index 0000000000..18b7343920 --- /dev/null +++ b/docs/plugins/dry-run-testing.md @@ -0,0 +1,135 @@ +--- +id: dry run testing +title: Dry Run Testing +description: Documentation on how to enable and implement dry run testing in actions +--- + +Scaffolder templates can be tested using the dry run feature of scaffolder actions. This allows you to simulate the effects of running a scaffolder action without making any actual changes to your environment, for example creating a webhook in Github. Once dry run is enabled in the scaffolder action, you can add handling to actions you use in your scaffolder templates to define how an action should operate in a dry run scenario. + +## Enabling dry run testing + +To enable dry run for your scaffolder action you need to add 'supportsDryRun: true' to the configuration object of 'createTemplateAction' in the function where the behavior of your action is defined: + +```typescript +export function exampleAction() { + return createTemplateAction<{ + example: string; + }>({ + id: 'action:example', + description: 'Example action', + schema: { + input: { + type: 'object', + properties: { + example: { + title: 'example', + type: 'string', + }, + }, + }, + }, + supportsDryRun: true, + async handler(ctx) { + ... + }, + }); +} +``` + +## Adding handling for dry run + +To add handling for dry run functionality you need to add a check for 'ctx.isDryRun' inside the handler of the configuration object which is being passed into 'createTemplateAction' in the function where the behavior of your action is defined. Once the check is successful, you can perform the desired actions expected in a dry run, e.g. outputting non-sensitive inputs. + +```typescript +async handler(ctx) { + ... + + // If this is a dry run, log and return + if (ctx.isDryRun) { + ctx.logger.info(`Dry run complete`); + return; + } + + ... + }, +``` + +## Testing dry run handling + +You will also need to add tests for the dry run handling, for example: + +```typescript + it('should not perform action during dry run', async () => { + ... + + // Create the context object with the necessary properties for a dry run + const ctx = { + ...mockContext, + isDryRun: true, + input: { + ... + }, + }; + + // Call the handler with the context + await action.handler(ctx); + + expect(...); + }); +``` + +## Dry run via API call + +Dry run can be performed using the dry-run API, which allows the dry run to be completed in code. +This [command line script](https://github.com/backstage/backstage/blob/master/contrib/scaffolder/template-testing-dry-run.md) offers a way for you to do work with dry-run API. You run it against a running instance, either locally or remote, and use it like: + +```bash +scaffolder-dry http://localhost:7007/ template-directory values.yml output-directory +``` + +If you're using backend permissions, pass a front-end auth token from a current browser session via --token $FRONTEND_TOKEN. + +You can also query the dry run endpoint directly in code, for example: + +`dry-run.js` + +```javascript +const template = yaml.load( + await fs.readFile('path/to/templates/template.yaml', 'utf-8'), +); +const values = JSON.parse( + await fs.readFile('path/to/templates/template_values.json', 'utf-8'), +); + +// Prepare the request body +const body = { + template, + values, + directoryContents, +}; + +// Send the request to the dry-run endpoint +const response = await fetch( + 'http://localhost:7000/api/scaffolder/v2/dry-run', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ', // replace 'YOUR_API_KEY' with your actual API key + }, + body: JSON.stringify(body), + }, +); +``` + +`template_values.json` + +```json +{ + "example": "test", + "name": "helloworld" +} +``` + +In this example `template.yaml` is your template yaml file, `template_values.json` would be a json file containing key value pairs for your template inputs, and `directoryContents` is an array that is populated with objects representing the contents of a specified directory (e.g. the same directory where you have your template yaml). Each object in the array corresponds to a file within the directory and contains two properties; the name of the file and the content of the file, encoded in Base64. +This is needed if any other files were required by the actions your template is using. diff --git a/mkdocs.yml b/mkdocs.yml index b9f6d41d46..045238e97e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -139,6 +139,7 @@ nav: - URL Reader: 'plugins/url-reader.md' - Testing: - Testing with Jest: 'plugins/testing.md' + - Dry Run Testing: 'plugins/dry-run-testing.md' - Publishing: - Publish private: 'plugins/publish-private.md' - Add to Directory: 'plugins/add-to-directory.md' From 5177f6f0e1ce1b453f0975eea0c0a49e6c3783c2 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Fri, 31 May 2024 12:35:40 +0200 Subject: [PATCH 007/204] User documentation for the Notifications and Signals Signed-off-by: Marek Libra --- docs/notifications/getting-started.md | 168 +++++++++++++++++++++++ docs/notifications/notificationsPage.png | Bin 0 -> 137787 bytes 2 files changed, 168 insertions(+) create mode 100644 docs/notifications/getting-started.md create mode 100644 docs/notifications/notificationsPage.png diff --git a/docs/notifications/getting-started.md b/docs/notifications/getting-started.md new file mode 100644 index 0000000000..6a7ea6314e --- /dev/null +++ b/docs/notifications/getting-started.md @@ -0,0 +1,168 @@ +--- +id: getting-started +title: Getting Started +description: How to get started with the notifications and signals +--- + +The Backstage Notifications System provides a way for plugins and external services to send notifications to Backstage users. +These notifications are displayed in the dedicated page of the Backstage frontend UI or by frontend plugins per specific scenarios. +Additionally, plugins can implement processors to send notifications through external channels like email, Slack, or MS Teams. + +Notifications can be optionally integrated with the signals (a push mechanism) to ensure users receive them immediately. + +### Upgrade to the latest version of Backstage + +To ensure your version of Backstage has all the latest notifications and signals related functionality, it’s important to upgrade to the latest version. The [Backstage upgrade helper](https://backstage.github.io/upgrade-helper/) is a great tool to help ensure that you’ve made all the necessary changes during the upgrade! + +## About notifications + +Notifications are messages sent to either individual users or groups. +They are not intended for inter-process communication of any kind. + +There are two basic types of notifications: + +- **Broadcast**: Messages sent to all users of Backstage. +- **Entity**: Messages delivered to specific listed entities, such as Users or Groups. + +Example of use-cases: + +- System-wide announcements or alerts +- Notifications for component owners: e.g., build failures, successful deployments, new vulnerabilities +- Notifications for individuals: e.g., updates you have subscribed to, new required training courses +- Notifications pertaining to a particular entity in the catalog: A notification might apply to an entity and the owning team. + +## Configuration + +### Notifications Backend + +The Notifications backend plugin provides an API to create notifications, list notifications per logged-in user, and search based on parameters. + +The plugin uses a relational [database](https://backstage.io/docs/getting-started/config/database) for persistence, no specifics are introduced in this context. + +No additional configuration in the app-config is needed, except for optional additional modules for `processors`. + +### Notifications Frontend + +The recipients of notifications have to be entities in the catalog, e.g. of the User or Group kind. + +Otherwise no specific configuration is needed for the front-end notifications plugin. + +All parametrization is done through component properties, such as the `NotificationsSidebarItem`, which can be used as an active left-side menu item in the front-end. + +![Notifications Page](notificationsPage.png) + +## Use + +New notifications can be sent either by a backend plugin or an external service through the REST API. + +### Backend + +Regardless of technical feasibility, a backend plugin should avoid directly accessing the notifications REST API. +Instead, it should integrate with the `@backstage/plugin-notifications-node` to `send` (create) a new notification. + +The reasons for this approach include the propagation of authorization in the API request and improved maintenance and backward compatibility in the future. + +```ts +import { notificationService } from '@backstage/plugin-notifications-node'; + +export const myPlugin = createBackendPlugin({ + pluginId: 'myPlugin', + register(env) { + env.registerInit({ + deps: { + // ... + notificationService: notificationService, + }, + async init({ config, logger, httpRouter, notificationService }) { + httpRouter.use( + await createRouter({ + // ... + notificationService, + }), + ); + }, + }); + }, +}); +``` + +To emit a new notification: + +```ts +notificationService.send({ + recipients /* of the broadcast or entity type */, + payload /* actual message */, +}); +``` + +Refer the [API documentation](https://github.com/backstage/backstage/blob/master/plugins/notifications-node/api-report.md) for further details. + +### Signals + +The use of signals with notifications is optional but generally enhances user experience and performance. + +When a notification is created, a new signal is emitted to a general-purpose message bus to announce it to subscribed listeners. + +The frontend maintains a persistent connection (web socket) to receive these announcements from the notifications channel. +The specific details of the updated or created notification should be retrieved via a request to the notifications API, except for new notifications, where the payload is included in the signal for performance reasons. + +In a frontend plugin, to subscribe for notifications' signals: + +```ts +import { useSignal } from '@backstage/plugin-signals-react'; + +const { lastSignal } = useSignal('notifications'); + +React.useEffect(() => { + /* ... */ +}, [lastSignal, notificationsApi]); +``` + +### Consuming Notifications + +In a front-end plugin, the simplest way to query a notification is by its ID: + +```ts +import { useApi } from '@backstage/core-plugin-api'; +import { notificationsApiRef } from '@backstage/plugin-notifications'; + +const notificationsApi = useApi(notificationsApiRef); + +notificationsApi.getNotification(yourId); + +// or with connection to signals: +notificationsApi.getNotification(lastSignal.notification_id); +``` + +### External Services + +When the emitter of a notification is a Backstage backend plugin, it is mandatory to use the integration via `@backstage/plugin-notifications-node` as described above. + +If the emitter is a service external to Backstage, an HTTP POST request can be issued directly to the API, assuming that authentication is properly configured. +Refer to the service-to-service auth documentation for more details, focusing on the Static Tokens section for the simplest setup option. + +An example request for creating a broadcast notification might look like: + +```bash +curl -X POST https://[BACKSTAGE_BACKEND]/api/notifications -H "Content-Type: application/json" -H "Authorization: Bearer YOUR_BASE64_SHARED_KEY_TOKEN" -d '{"recipients":{"type":"broadcast"},"payload": {"title": "Title of broadcast message","link": "http://foo.com/bar","severity": "high","topic": "The topic"}}' +``` + +## Additional info + +Up-to-date documentation can be found in the plugins' README files: + +- https://github.com/backstage/backstage/blob/master/plugins/notifications/README.md + +- https://github.com/backstage/backstage/blob/master/plugins/notifications-backend/README.md + +- https://github.com/backstage/backstage/blob/master/plugins/notifications-node/README.md + +- https://github.com/backstage/backstage/blob/master/plugins/signals-react/README.md + +## Processors + +The default backend behavior when emitting a new notification can be tweaked using processors. + +Depending on the needs, a processor can modify the content of a notification or route it to different systems like email, Slack, or other services. + +A good example of how to write a processor is the [Email Processor](https://github.com/backstage/backstage/tree/master/plugins/notifications-backend-module-email). diff --git a/docs/notifications/notificationsPage.png b/docs/notifications/notificationsPage.png new file mode 100644 index 0000000000000000000000000000000000000000..7c96ab77af2d2d9527acefa99027468e4d5b3782 GIT binary patch literal 137787 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

nobr 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

-ZZF}2&0dyTfeHG1VxJoZ)&u~SsmQN;u%#Z`Xd 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 Date: Mon, 17 Jun 2024 14:12:38 +0200 Subject: [PATCH 008/204] feat: add listBranchesByRepository method to BitbucketCloudClient Signed-off-by: Benjamin Janssens --- .../src/BitbucketCloudClient.ts | 20 +++++++++++++++++++ .../src/models/index.ts | 11 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts index f1b7d60e41..86f56dd70d 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts @@ -95,6 +95,26 @@ export class BitbucketCloudClient { ); } + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + const workspaceEnc = encodeURIComponent(workspace); + + return new WithPagination( + paginationOptions => + this.createUrl( + `/repositories/${workspaceEnc}/${repository}/refs/branches`, + { + ...paginationOptions, + ...options, + }, + ), + url => this.getTypeMapped(url), + ); + } + private createUrl(endpoint: string, options?: RequestOptions): URL { const request = new URL(this.config.apiBaseUrl + endpoint); for (const key in options) { diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 2cdc143d06..28b1b45e1b 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -275,6 +275,17 @@ export namespace Models { values?: Set; } + /** + * A paginated list of branches. + * @public + */ + export interface PaginatedBranches extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + /** * Object describing a user's role on resources like commits or pull requests. * @public From 7ee5682ad467090c3cb99866968a1efddfef1eb9 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Jun 2024 14:18:07 +0200 Subject: [PATCH 009/204] feat: add branches resource to autocomplete endpoint for bitbucket Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 31a2103088..70824995ac 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -77,6 +77,23 @@ export async function handleAutocompleteRequest({ return { results: result.map(title => ({ title })) }; } + case 'branches': { + if (!parameters.workspace || !parameters.repository) + throw new InputError( + 'Missing workspace and/or repository query parameter', + ); + + const result: string[] = []; + + for await (const page of client + .listBranchesByRepository(parameters.repository, parameters.workspace) + .iteratePages()) { + const names = [...page.values!].map(p => p.name!); + result.push(...names); + } + + return result; + } default: throw new InputError(`Invalid resource: ${resource}`); } From f6fea341737fd8e1820d882b5c4eeb2c2d4abb25 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Jun 2024 16:32:15 +0200 Subject: [PATCH 010/204] feat: add BitbucketRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 145 ++++++++++++++++++ .../fields/BitbucketRepoBranchPicker/index.ts | 20 +++ .../BitbucketRepoBranchPicker/schema.ts | 82 ++++++++++ plugins/scaffolder/src/extensions/default.ts | 7 + plugins/scaffolder/src/index.ts | 1 + plugins/scaffolder/src/plugin.tsx | 15 ++ 6 files changed, 270 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx new file mode 100644 index 0000000000..df496516af --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -0,0 +1,145 @@ +/* + * 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 { + scaffolderApiRef, + useTemplateSecrets, +} from '@backstage/plugin-scaffolder-react'; +import FormControl from '@material-ui/core/FormControl'; +import React, { useEffect, useMemo, useState } from 'react'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import useDebounce from 'react-use/esm/useDebounce'; +import { useApi } from '@backstage/core-plugin-api'; +import { scmAuthApiRef } from '@backstage/integration-react'; +import { BitbucketRepoBranchPickerProps } from './schema'; + +export const BitbucketRepoBranchPicker = ({ + onChange, + rawErrors, + required, + formData, + formContext: { + formData: { repoUrl }, + }, + uiSchema, +}: BitbucketRepoBranchPickerProps) => { + const { secrets, setSecrets } = useTemplateSecrets(); + + const accessToken = useMemo( + () => + uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && + secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey], + [secrets, uiSchema], + ); + + const [host, setHost] = useState(); + const [workspace, setWorkspace] = useState(null); + const [repository, setRepository] = useState(null); + const [availableBranches, setAvailableBranches] = useState([]); + + const scmAuthApi = useApi(scmAuthApiRef); + const scaffolderApi = useApi(scaffolderApiRef); + + useEffect(() => { + if (repoUrl) { + const url = new URL(`https://${repoUrl}`); + setHost(url.host); + setWorkspace(url.searchParams.get('workspace')); + setRepository(url.searchParams.get('repo')); + } + }, [repoUrl]); + + useDebounce( + async () => { + const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; + + if (!requestUserCredentials || !host) { + return; + } + + // don't show login prompt if secret value is already in state + if (secrets[requestUserCredentials.secretsKey]) { + return; + } + + // user has requested that we use the users credentials + // so lets grab them using the scmAuthApi and pass through + // any additional scopes from the ui:options + const { token } = await scmAuthApi.getCredentials({ + url: `https://${host}`, + additionalScope: { + repoWrite: true, + customScopes: requestUserCredentials.additionalScopes, + }, + }); + + // set the secret using the key provided in the ui:options for use + // in the templating the manifest with ${{ secrets[secretsKey] }} + setSecrets({ [requestUserCredentials.secretsKey]: token }); + }, + 500, + [host, uiSchema], + ); + + useDebounce( + () => { + const updateAvailableBranches = async () => { + if ( + host === 'bitbucket.org' && + accessToken && + workspace && + repository + ) { + const result = await scaffolderApi.autocomplete( + accessToken, + 'bitbucketCloud', + 'branches', + { workspace, repository }, + ); + + setAvailableBranches(result); + } else { + setAvailableBranches([]); + } + }; + + updateAvailableBranches().catch(() => setAvailableBranches([])); + }, + 500, + [host, accessToken, workspace, repository], + ); + + return ( + 0 && !formData} + > + { + onChange(newValue || ''); + }} + options={availableBranches} + renderInput={params => ( + + )} + freeSolo + autoSelect + /> + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts new file mode 100644 index 0000000000..df9acbd8b9 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts @@ -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 { + BitbucketRepoBranchPickerFieldSchema, + type BitbucketRepoBranchPickerUiOptions, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts new file mode 100644 index 0000000000..3e14888ed5 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts @@ -0,0 +1,82 @@ +/* + * 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 { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const BitbucketRepoBranchPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + requestUserCredentials: z + .object({ + secretsKey: z + .string() + .describe( + 'Key used within the template secrets context to store the credential', + ), + additionalScopes: z + .object({ + gitea: z + .array(z.string()) + .optional() + .describe('Additional Gitea scopes to request'), + gerrit: z + .array(z.string()) + .optional() + .describe('Additional Gerrit scopes to request'), + github: z + .array(z.string()) + .optional() + .describe('Additional GitHub scopes to request'), + gitlab: z + .array(z.string()) + .optional() + .describe('Additional GitLab scopes to request'), + bitbucket: z + .array(z.string()) + .optional() + .describe('Additional BitBucket scopes to request'), + azure: z + .array(z.string()) + .optional() + .describe('Additional Azure scopes to request'), + }) + .optional() + .describe('Additional permission scopes to request'), + }) + .optional() + .describe( + 'If defined will request user credentials to auth against the given SCM platform', + ), + }), +); + +/** + * The input props that can be specified under `ui:options` for the + * `BitbucketRepoBranchPicker` field extension. + * + * @public + */ +export type BitbucketRepoBranchPickerUiOptions = + typeof BitbucketRepoBranchPickerFieldSchema.uiOptionsType; + +export type BitbucketRepoBranchPickerProps = + typeof BitbucketRepoBranchPickerFieldSchema.type; + +export const BitbucketRepoBranchPickerSchema = + BitbucketRepoBranchPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index a59f3e08e3..13a719e9f9 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -50,6 +50,8 @@ import { MultiEntityPickerSchema, validateMultiEntityPickerValidation, } from '../components/fields/MultiEntityPicker/MultiEntityPicker'; +import { BitbucketRepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; +import { BitbucketRepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -99,4 +101,9 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ schema: MultiEntityPickerSchema, validation: validateMultiEntityPickerValidation, }, + { + component: BitbucketRepoBranchPicker, + name: 'BitbucketRepoBranchPicker', + schema: BitbucketRepoBranchPickerSchema, + }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index d809ec00f9..d343311db9 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -30,6 +30,7 @@ export { MyGroupsPickerFieldExtension, RepoUrlPickerFieldExtension, MultiEntityPickerFieldExtension, + BitbucketRepoBranchPickerFieldExtension, ScaffolderPage, scaffolderPlugin, } from './plugin'; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index c053bfacd9..453e5ea63a 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -73,6 +73,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; +import { BitbucketRepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; +import { BitbucketRepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; /** * The main plugin export for the scaffolder. @@ -231,3 +233,16 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( schema: EntityTagsPickerSchema, }), ); + +/** + * A field extension to select a branch from a repository. + * + * @public + */ +export const BitbucketRepoBranchPickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: BitbucketRepoBranchPicker, + name: 'BitbucketRepoBranchPicker', + schema: BitbucketRepoBranchPickerSchema, + }), +); From 741837a560f16369b3aadce4294e02726e5732e8 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 11:42:20 +0200 Subject: [PATCH 011/204] refactor: add generic RepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 109 ++++----------- .../RepoBranchPicker.tsx | 125 ++++++++++++++++++ .../fields/BitbucketRepoBranchPicker/index.ts | 4 +- .../BitbucketRepoBranchPicker/schema.ts | 14 +- .../fields/BitbucketRepoBranchPicker/types.ts | 23 ++++ plugins/scaffolder/src/extensions/default.ts | 10 +- plugins/scaffolder/src/index.ts | 2 +- plugins/scaffolder/src/plugin.tsx | 12 +- 8 files changed, 194 insertions(+), 105 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index df496516af..736841b7a6 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -14,128 +14,71 @@ * limitations under the License. */ -import { - scaffolderApiRef, - useTemplateSecrets, -} from '@backstage/plugin-scaffolder-react'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; import { useApi } from '@backstage/core-plugin-api'; -import { scmAuthApiRef } from '@backstage/integration-react'; -import { BitbucketRepoBranchPickerProps } from './schema'; +import { RepoBranchPickerState } from './types'; export const BitbucketRepoBranchPicker = ({ onChange, + state, rawErrors, - required, - formData, - formContext: { - formData: { repoUrl }, - }, - uiSchema, -}: BitbucketRepoBranchPickerProps) => { - const { secrets, setSecrets } = useTemplateSecrets(); - - const accessToken = useMemo( - () => - uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && - secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey], - [secrets, uiSchema], - ); - - const [host, setHost] = useState(); - const [workspace, setWorkspace] = useState(null); - const [repository, setRepository] = useState(null); - const [availableBranches, setAvailableBranches] = useState([]); - - const scmAuthApi = useApi(scmAuthApiRef); + accessToken, +}: { + onChange: (state: RepoBranchPickerState) => void; + state: RepoBranchPickerState; + rawErrors: string[]; + accessToken?: string; +}) => { const scaffolderApi = useApi(scaffolderApiRef); - useEffect(() => { - if (repoUrl) { - const url = new URL(`https://${repoUrl}`); - setHost(url.host); - setWorkspace(url.searchParams.get('workspace')); - setRepository(url.searchParams.get('repo')); - } - }, [repoUrl]); - - useDebounce( - async () => { - const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; - - if (!requestUserCredentials || !host) { - return; - } - - // don't show login prompt if secret value is already in state - if (secrets[requestUserCredentials.secretsKey]) { - return; - } - - // user has requested that we use the users credentials - // so lets grab them using the scmAuthApi and pass through - // any additional scopes from the ui:options - const { token } = await scmAuthApi.getCredentials({ - url: `https://${host}`, - additionalScope: { - repoWrite: true, - customScopes: requestUserCredentials.additionalScopes, - }, - }); - - // set the secret using the key provided in the ui:options for use - // in the templating the manifest with ${{ secrets[secretsKey] }} - setSecrets({ [requestUserCredentials.secretsKey]: token }); - }, - 500, - [host, uiSchema], - ); - useDebounce( () => { const updateAvailableBranches = async () => { if ( - host === 'bitbucket.org' && + state.host === 'bitbucket.org' && accessToken && - workspace && - repository + state.workspace && + state.repository ) { const result = await scaffolderApi.autocomplete( accessToken, 'bitbucketCloud', 'branches', - { workspace, repository }, + { workspace: state.workspace, repository: state.repository }, ); - setAvailableBranches(result); + onChange({ availableBranches: result }); } else { - setAvailableBranches([]); + onChange({ availableBranches: [] }); } }; - updateAvailableBranches().catch(() => setAvailableBranches([])); + updateAvailableBranches().catch(() => + onChange({ availableBranches: [] }), + ); }, 500, - [host, accessToken, workspace, repository], + [state, accessToken], ); return ( 0 && !formData} + required + error={rawErrors?.length > 0 && !state.branch} > { - onChange(newValue || ''); + onChange({ branch: newValue || undefined }); }} - options={availableBranches} + options={state.availableBranches || []} renderInput={params => ( - + )} freeSolo autoSelect diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx new file mode 100644 index 0000000000..1f75090e3b --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx @@ -0,0 +1,125 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import { + scmIntegrationsApiRef, + scmAuthApiRef, +} from '@backstage/integration-react'; +import React, { useEffect, useState, useCallback } from 'react'; +import { RepoBranchPickerProps } from './schema'; +import { RepoBranchPickerState } from './types'; +import useDebounce from 'react-use/esm/useDebounce'; +import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import Typography from '@material-ui/core/Typography'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; + +/** + * The underlying component that is rendered in the form for the `RepoBranchPicker` + * field extension. + * + * @public + */ +export const RepoBranchPicker = (props: RepoBranchPickerProps) => { + const { uiSchema, onChange, rawErrors, schema, formContext } = props; + const [state, setState] = useState({}); + const integrationApi = useApi(scmIntegrationsApiRef); + const scmAuthApi = useApi(scmAuthApiRef); + const { secrets, setSecrets } = useTemplateSecrets(); + + useEffect(() => { + if (formContext.formData.repoUrl) { + const url = new URL(`https://${formContext.formData.repoUrl}`); + + setState({ + host: url.host, + workspace: url.searchParams.get('workspace') || undefined, + repository: url.searchParams.get('repo') || undefined, + }); + } + }, [formContext]); + + useEffect(() => { + onChange(state.branch); + }, [state, onChange]); + + const updateLocalState = useCallback( + (newState: RepoBranchPickerState) => { + setState(prevState => ({ ...prevState, ...newState })); + }, + [setState], + ); + + useDebounce( + async () => { + const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; + + if (!requestUserCredentials || !state.host) { + return; + } + + // don't show login prompt if secret value is already in state + if (secrets[requestUserCredentials.secretsKey]) { + return; + } + + // user has requested that we use the users credentials + // so lets grab them using the scmAuthApi and pass through + // any additional scopes from the ui:options + const { token } = await scmAuthApi.getCredentials({ + url: `https://${state.host}`, + additionalScope: { + repoWrite: true, + customScopes: requestUserCredentials.additionalScopes, + }, + }); + + // set the secret using the key provided in the ui:options for use + // in the templating the manifest with ${{ secrets[secretsKey] }} + setSecrets({ [requestUserCredentials.secretsKey]: token }); + }, + 500, + [state, uiSchema], + ); + + const hostType = + (state.host && integrationApi.byHost(state.host)?.type) ?? null; + return ( + <> + {schema.title && ( + + {schema.title} + + + )} + {schema.description && ( + {schema.description} + )} + {hostType === 'bitbucket' && ( + + )} + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts index df9acbd8b9..2b87b62ebd 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts @@ -15,6 +15,6 @@ */ export { - BitbucketRepoBranchPickerFieldSchema, - type BitbucketRepoBranchPickerUiOptions, + RepoBranchPickerFieldSchema, + type RepoBranchPickerUiOptions, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts index 3e14888ed5..12ed07fd80 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts @@ -19,7 +19,7 @@ import { makeFieldSchemaFromZod } from '../utils'; /** * @public */ -export const BitbucketRepoBranchPickerFieldSchema = makeFieldSchemaFromZod( +export const RepoBranchPickerFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ requestUserCredentials: z @@ -68,15 +68,13 @@ export const BitbucketRepoBranchPickerFieldSchema = makeFieldSchemaFromZod( /** * The input props that can be specified under `ui:options` for the - * `BitbucketRepoBranchPicker` field extension. + * `RepoBranchPicker` field extension. * * @public */ -export type BitbucketRepoBranchPickerUiOptions = - typeof BitbucketRepoBranchPickerFieldSchema.uiOptionsType; +export type RepoBranchPickerUiOptions = + typeof RepoBranchPickerFieldSchema.uiOptionsType; -export type BitbucketRepoBranchPickerProps = - typeof BitbucketRepoBranchPickerFieldSchema.type; +export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type; -export const BitbucketRepoBranchPickerSchema = - BitbucketRepoBranchPickerFieldSchema.schema; +export const RepoBranchPickerSchema = RepoBranchPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts new file mode 100644 index 0000000000..4843ff767e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts @@ -0,0 +1,23 @@ +/* + * 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 interface RepoBranchPickerState { + host?: string; + workspace?: string; + repository?: string; + branch?: string; + availableBranches?: string[]; +} diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 13a719e9f9..409372e421 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -50,8 +50,8 @@ import { MultiEntityPickerSchema, validateMultiEntityPickerValidation, } from '../components/fields/MultiEntityPicker/MultiEntityPicker'; -import { BitbucketRepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; -import { BitbucketRepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -102,8 +102,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ validation: validateMultiEntityPickerValidation, }, { - component: BitbucketRepoBranchPicker, - name: 'BitbucketRepoBranchPicker', - schema: BitbucketRepoBranchPickerSchema, + component: RepoBranchPicker, + name: 'RepoBranchPicker', + schema: RepoBranchPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index d343311db9..c5709f1080 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -30,7 +30,7 @@ export { MyGroupsPickerFieldExtension, RepoUrlPickerFieldExtension, MultiEntityPickerFieldExtension, - BitbucketRepoBranchPickerFieldExtension, + RepoBranchPickerFieldExtension, ScaffolderPage, scaffolderPlugin, } from './plugin'; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 453e5ea63a..6f4e28c9b0 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -73,8 +73,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; -import { BitbucketRepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; -import { BitbucketRepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; /** * The main plugin export for the scaffolder. @@ -239,10 +239,10 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( * * @public */ -export const BitbucketRepoBranchPickerFieldExtension = scaffolderPlugin.provide( +export const RepoBranchPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ - component: BitbucketRepoBranchPicker, - name: 'BitbucketRepoBranchPicker', - schema: BitbucketRepoBranchPickerSchema, + component: RepoBranchPicker, + name: 'RepoBranchPicker', + schema: RepoBranchPickerSchema, }), ); From 3a272ac051baf5be2417be598a591859f4ef30b3 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 11:54:00 +0200 Subject: [PATCH 012/204] chore: code cleanup Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 14 ++--- .../RepoBranchPicker.tsx | 54 ++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index 736841b7a6..70f4ea8c36 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -34,22 +34,24 @@ export const BitbucketRepoBranchPicker = ({ rawErrors: string[]; accessToken?: string; }) => { + const { host, workspace, repository, branch, availableBranches } = state; + const scaffolderApi = useApi(scaffolderApiRef); useDebounce( () => { const updateAvailableBranches = async () => { if ( - state.host === 'bitbucket.org' && + host === 'bitbucket.org' && accessToken && - state.workspace && - state.repository + workspace && + repository ) { const result = await scaffolderApi.autocomplete( accessToken, 'bitbucketCloud', 'branches', - { workspace: state.workspace, repository: state.repository }, + { workspace, repository }, ); onChange({ availableBranches: result }); @@ -70,13 +72,13 @@ export const BitbucketRepoBranchPicker = ({ 0 && !state.branch} + error={rawErrors?.length > 0 && !branch} > { onChange({ branch: newValue || undefined }); }} - options={state.availableBranches || []} + options={availableBranches || []} renderInput={params => ( )} diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx index 1f75090e3b..c690a58f4a 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx @@ -36,34 +36,15 @@ import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; */ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { const { uiSchema, onChange, rawErrors, schema, formContext } = props; + const [state, setState] = useState({}); + const { host, branch } = state; + const integrationApi = useApi(scmIntegrationsApiRef); const scmAuthApi = useApi(scmAuthApiRef); + const { secrets, setSecrets } = useTemplateSecrets(); - useEffect(() => { - if (formContext.formData.repoUrl) { - const url = new URL(`https://${formContext.formData.repoUrl}`); - - setState({ - host: url.host, - workspace: url.searchParams.get('workspace') || undefined, - repository: url.searchParams.get('repo') || undefined, - }); - } - }, [formContext]); - - useEffect(() => { - onChange(state.branch); - }, [state, onChange]); - - const updateLocalState = useCallback( - (newState: RepoBranchPickerState) => { - setState(prevState => ({ ...prevState, ...newState })); - }, - [setState], - ); - useDebounce( async () => { const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; @@ -96,8 +77,31 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { [state, uiSchema], ); - const hostType = - (state.host && integrationApi.byHost(state.host)?.type) ?? null; + useEffect(() => { + if (formContext.formData.repoUrl) { + const url = new URL(`https://${formContext.formData.repoUrl}`); + + setState({ + host: url.host, + workspace: url.searchParams.get('workspace') || undefined, + repository: url.searchParams.get('repo') || undefined, + }); + } + }, [formContext]); + + useEffect(() => { + onChange(branch); + }, [branch, onChange]); + + const updateLocalState = useCallback( + (newState: RepoBranchPickerState) => { + setState(prevState => ({ ...prevState, ...newState })); + }, + [setState], + ); + + const hostType = (host && integrationApi.byHost(host)?.type) ?? null; + return ( <> {schema.title && ( From 4e3ba7ca74ed3e675e607ceb88d0c1759b65442d Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 13:37:38 +0200 Subject: [PATCH 013/204] chore: code cleanup Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 7 ++-- .../RepoBranchPicker.tsx | 32 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index 70f4ea8c36..27a0b5c027 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -22,6 +22,7 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; import { useApi } from '@backstage/core-plugin-api'; import { RepoBranchPickerState } from './types'; +import FormHelperText from '@material-ui/core/FormHelperText'; export const BitbucketRepoBranchPicker = ({ onChange, @@ -65,7 +66,7 @@ export const BitbucketRepoBranchPicker = ({ ); }, 500, - [state, accessToken], + [host, workspace, repository, accessToken], ); return ( @@ -75,8 +76,9 @@ export const BitbucketRepoBranchPicker = ({ error={rawErrors?.length > 0 && !branch} > { - onChange({ branch: newValue || undefined }); + onChange({ branch: newValue || '' }); }} options={availableBranches || []} renderInput={params => ( @@ -85,6 +87,7 @@ export const BitbucketRepoBranchPicker = ({ freeSolo autoSelect /> + The branch of the repository ); }; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx index c690a58f4a..2ba1eddbfc 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { useApi } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef, @@ -35,9 +36,15 @@ import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; * @public */ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { - const { uiSchema, onChange, rawErrors, schema, formContext } = props; + const { uiSchema, onChange, rawErrors, formData, schema, formContext } = + props; + const { + formData: { repoUrl }, + } = formContext; - const [state, setState] = useState({}); + const [state, setState] = useState({ + branch: formData || '', + }); const { host, branch } = state; const integrationApi = useApi(scmIntegrationsApiRef); @@ -49,7 +56,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { async () => { const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; - if (!requestUserCredentials || !state.host) { + if (!requestUserCredentials || !host) { return; } @@ -62,7 +69,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { // so lets grab them using the scmAuthApi and pass through // any additional scopes from the ui:options const { token } = await scmAuthApi.getCredentials({ - url: `https://${state.host}`, + url: `https://${host}`, additionalScope: { repoWrite: true, customScopes: requestUserCredentials.additionalScopes, @@ -74,20 +81,21 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { setSecrets({ [requestUserCredentials.secretsKey]: token }); }, 500, - [state, uiSchema], + [host, uiSchema], ); useEffect(() => { - if (formContext.formData.repoUrl) { - const url = new URL(`https://${formContext.formData.repoUrl}`); + if (repoUrl) { + const url = new URL(`https://${repoUrl}`); - setState({ + setState(prevState => ({ + ...prevState, host: url.host, - workspace: url.searchParams.get('workspace') || undefined, - repository: url.searchParams.get('repo') || undefined, - }); + workspace: url.searchParams.get('workspace') || '', + repository: url.searchParams.get('repo') || '', + })); } - }, [formContext]); + }, [repoUrl]); useEffect(() => { onChange(branch); From fb528a9196450b09667950fa293108ce7f83dca4 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 14:44:12 +0200 Subject: [PATCH 014/204] refactor: move availableBranches from RepoBranchPickerState to BitbucketRepoBranchPicker; add tests for BitbucketRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.test.tsx | 105 ++++++++++++++++++ .../BitbucketRepoBranchPicker.tsx | 16 +-- .../fields/BitbucketRepoBranchPicker/types.ts | 1 - 3 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..6cbc561bc9 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx @@ -0,0 +1,105 @@ +/* + * 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 { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; + +describe('BitbucketRepoBranchPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest.fn().mockResolvedValue(['branch1']), + }; + + it('renders an input field', () => { + const { getByRole } = render( + + + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + + + , + ); + + const input = getByRole('textbox'); + + act(() => { + input.focus(); + fireEvent.change(input, { + target: { value: 'develop' }, + }); + input.blur(); + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); + + it('should populate branches', async () => { + const onChange = jest.fn(); + + const { getByRole, getByText } = render( + + + , + ); + + // Open the Autcomplete dropdown + const input = getByRole('textbox'); + await userEvent.click(input); + + // Verify that the available workspaces are shown + await waitFor(() => expect(getByText('branch1')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('branch1')); + expect(onChange).toHaveBeenCalledWith({ + branch: 'branch1', + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index 27a0b5c027..3af7eb7e2c 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -16,7 +16,7 @@ import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; -import React from 'react'; +import React, { useState } from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; @@ -35,7 +35,9 @@ export const BitbucketRepoBranchPicker = ({ rawErrors: string[]; accessToken?: string; }) => { - const { host, workspace, repository, branch, availableBranches } = state; + const { host, workspace, repository, branch } = state; + + const [availableBranches, setAvailableBranches] = useState([]); const scaffolderApi = useApi(scaffolderApiRef); @@ -55,15 +57,13 @@ export const BitbucketRepoBranchPicker = ({ { workspace, repository }, ); - onChange({ availableBranches: result }); + setAvailableBranches(result); } else { - onChange({ availableBranches: [] }); + setAvailableBranches([]); } }; - updateAvailableBranches().catch(() => - onChange({ availableBranches: [] }), - ); + updateAvailableBranches().catch(() => setAvailableBranches([])); }, 500, [host, workspace, repository, accessToken], @@ -80,7 +80,7 @@ export const BitbucketRepoBranchPicker = ({ onChange={(_, newValue) => { onChange({ branch: newValue || '' }); }} - options={availableBranches || []} + options={availableBranches} renderInput={params => ( )} diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts index 4843ff767e..1d54ea7e8b 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts @@ -19,5 +19,4 @@ export interface RepoBranchPickerState { workspace?: string; repository?: string; branch?: string; - availableBranches?: string[]; } From a781cd8513b9ec3a86bcb2379be7fbc5b5a49d2c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 14:46:20 +0200 Subject: [PATCH 015/204] chore: rename BitbucketRepoBranchPicker folder to RepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.test.tsx | 0 .../BitbucketRepoBranchPicker.tsx | 0 .../RepoBranchPicker.tsx | 0 .../{BitbucketRepoBranchPicker => RepoBranchPicker}/index.ts | 0 .../{BitbucketRepoBranchPicker => RepoBranchPicker}/schema.ts | 0 .../{BitbucketRepoBranchPicker => RepoBranchPicker}/types.ts | 0 plugins/scaffolder/src/extensions/default.ts | 4 ++-- plugins/scaffolder/src/plugin.tsx | 4 ++-- 8 files changed, 4 insertions(+), 4 deletions(-) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/BitbucketRepoBranchPicker.test.tsx (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/BitbucketRepoBranchPicker.tsx (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/RepoBranchPicker.tsx (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/index.ts (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/schema.ts (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/types.ts (100%) diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 409372e421..e77d968c80 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -50,8 +50,8 @@ import { MultiEntityPickerSchema, validateMultiEntityPickerValidation, } from '../components/fields/MultiEntityPicker/MultiEntityPicker'; -import { RepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; -import { RepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from '../components/fields/RepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from '../components/fields/RepoBranchPicker/schema'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 6f4e28c9b0..c6c858f6b1 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -73,8 +73,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; -import { RepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; -import { RepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from './components/fields/RepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from './components/fields/RepoBranchPicker/schema'; /** * The main plugin export for the scaffolder. From 426bd26366c0f649a36a89f70acd0e97e9da428b Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 15:00:14 +0200 Subject: [PATCH 016/204] test: add tests for listBranchesByRepository to BitbucketCloudClient Signed-off-by: Benjamin Janssens --- .../src/BitbucketCloudClient.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts index f625f0daf8..7558c92138 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts @@ -162,4 +162,33 @@ describe('BitbucketCloudClient', () => { expect(results).toHaveLength(1); expect(results[0].slug).toEqual('workspace1'); }); + + it('listBranchesByRepository', async () => { + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace1/repo1/refs/branches', + (_, res, ctx) => { + const response = { + values: [ + { + type: 'branch', + name: 'branch1', + } as Models.Branch, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + const pagination = client.listBranchesByRepository('repo1', 'workspace1'); + + const results = []; + for await (const result of pagination.iterateResults()) { + results.push(result); + } + + expect(results).toHaveLength(1); + expect(results[0].name).toEqual('branch1'); + }); }); From 5588a64e80e324ab97cb9647b477cee2793cff49 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 15:09:46 +0200 Subject: [PATCH 017/204] test: add tests for branches for handleBitbucketCloudRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts index 0447ff2d28..b124c65f78 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -35,6 +35,11 @@ describe('handleAutocompleteRequest', () => { .fn() .mockReturnValue([{ values: [{ slug: 'repository1' }] }]), }), + listBranchesByRepository: jest.fn().mockReturnValue({ + iteratePages: jest + .fn() + .mockReturnValue([{ values: [{ name: 'branch1' }] }]), + }), }; const fromConfig = jest @@ -89,6 +94,15 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [{ title: 'repository1' }] }); }); + it('should return branches', async () => { + const result = await handleBitbucketCloudRequest('foo', 'branches', { + workspace: 'workspace1', + repository: 'repository1', + }); + + expect(result).toEqual(['branch1']); + }); + it('should throw an error when passing an invalid resource', async () => { await expect( handleAutocompleteRequest({ From 8329e930c6f7e7f0b3654b2e7af8d098d246527a Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 19 Jun 2024 09:52:17 +0200 Subject: [PATCH 018/204] test: add tests for RepoBranchPicker Signed-off-by: Benjamin Janssens --- .../RepoBranchPicker.test.tsx | 324 ++++++++++++++++++ .../RepoBranchPicker/RepoBranchPicker.tsx | 5 +- 2 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx new file mode 100644 index 0000000000..eb5ebd1730 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx @@ -0,0 +1,324 @@ +/* + * 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 { Form } from '@backstage/plugin-scaffolder-react/alpha'; +import validator from '@rjsf/validator-ajv8'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + scmIntegrationsApiRef, + ScmIntegrationsApi, + scmAuthApiRef, + ScmAuthApi, +} from '@backstage/integration-react'; + +import { + SecretsContextProvider, + scaffolderApiRef, + useTemplateSecrets, + ScaffolderRJSFField, +} from '@backstage/plugin-scaffolder-react'; +import { act, fireEvent } from '@testing-library/react'; +import { RepoBranchPicker } from './RepoBranchPicker'; + +describe('RepoBranchPicker', () => { + const mockIntegrationsApi: Partial = { + byHost: () => ({ type: 'bitbucket' }), + }; + + let mockScmAuthApi: Partial; + + beforeEach(() => { + mockScmAuthApi = { + getCredentials: jest.fn().mockResolvedValue({ token: 'abc123' }), + }; + }); + + describe('happy path rendering', () => { + it('should render the repo branch picker with minimal props', async () => { + const onSubmit = jest.fn(); + + const { getByRole } = await renderInTestApp( + + +

, + }} + onSubmit={onSubmit} + formContext={{ + formData: { + repoUrl: 'bitbucket.org', + }, + }} + /> + + , + ); + + const input = getByRole('textbox'); + const submitButton = getByRole('button'); + + act(() => { + input.focus(); + fireEvent.change(input, { target: { value: 'branch1' } }); + input.blur(); + }); + + fireEvent.click(submitButton); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + formData: 'branch1', + }), + expect.anything(), + ); + }); + + it('should render properly with title and description', async () => { + const { getByText } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'bitbucket.org', + }, + }} + /> + + , + ); + + expect(getByText('test title')).toBeInTheDocument(); + expect(getByText('test description')).toBeInTheDocument(); + }); + }); + + describe('requestUserCredentials', () => { + it('should call the scmAuthApi with the correct params', async () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + const { getByTestId } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'github.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: 'https://github.com', + additionalScope: { + repoWrite: true, + customScopes: { + github: ['workflow'], + }, + }, + }); + + const currentSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + + expect(currentSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + }); + + it('should call the scmAuthApi with the correct params if workspace is nested', async () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'gitlab.example.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: 'https://gitlab.example.com', + additionalScope: { + repoWrite: true, + }, + }); + }); + + it('should not call the scmAuthApi if secret is available in the state', async () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + const { getByTestId } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'github.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + // as we already have a secret in the state, getCredentials should not be called again. + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); + + const currentSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + + expect(currentSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx index 2ba1eddbfc..8ab83bd717 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx @@ -20,8 +20,6 @@ import { scmAuthApiRef, } from '@backstage/integration-react'; import React, { useEffect, useState, useCallback } from 'react'; -import { RepoBranchPickerProps } from './schema'; -import { RepoBranchPickerState } from './types'; import useDebounce from 'react-use/esm/useDebounce'; import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; import Box from '@material-ui/core/Box'; @@ -29,6 +27,9 @@ import Divider from '@material-ui/core/Divider'; import Typography from '@material-ui/core/Typography'; import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { RepoBranchPickerProps } from './schema'; +import { RepoBranchPickerState } from './types'; + /** * The underlying component that is rendered in the form for the `RepoBranchPicker` * field extension. From 80738256c37e4ace961dbf31fe0e02b04371075d Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 19 Jun 2024 13:48:49 +0200 Subject: [PATCH 019/204] chore: copy comments from RepoUrlPicker to stay consistent Signed-off-by: Benjamin Janssens --- .../fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx | 7 +++++++ .../src/components/fields/RepoBranchPicker/schema.ts | 3 +++ 2 files changed, 10 insertions(+) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 3af7eb7e2c..23e8f01617 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -24,6 +24,13 @@ import { useApi } from '@backstage/core-plugin-api'; import { RepoBranchPickerState } from './types'; import FormHelperText from '@material-ui/core/FormHelperText'; +/** + * The underlying component that is rendered in the form for the `BitbucketRepoBranchPicker` + * field extension. + * + * @public + * + */ export const BitbucketRepoBranchPicker = ({ onChange, state, diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts index 12ed07fd80..6ee67bd1c7 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts @@ -77,4 +77,7 @@ export type RepoBranchPickerUiOptions = export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type; +// NOTE: There is a bug with this failing validation in the custom field explorer due +// to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if +// requestUserCredentials is not defined export const RepoBranchPickerSchema = RepoBranchPickerFieldSchema.schema; From 37d52e4f2830ff15af55a60bfd5d8afd7ea36c2b Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 19 Jun 2024 16:37:33 +0200 Subject: [PATCH 020/204] feat: fall back on DefaultRepoBranchPicker if host is not bitbucket Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 6 +- .../DefaultRepoBranchPicker.test.tsx | 55 +++++++++++++++++ .../DefaultRepoBranchPicker.tsx | 60 +++++++++++++++++++ .../RepoBranchPicker.test.tsx | 10 +--- .../RepoBranchPicker/RepoBranchPicker.tsx | 53 +++++++++++----- 5 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 23e8f01617..76de25dba0 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -36,11 +36,13 @@ export const BitbucketRepoBranchPicker = ({ state, rawErrors, accessToken, + required, }: { onChange: (state: RepoBranchPickerState) => void; state: RepoBranchPickerState; rawErrors: string[]; accessToken?: string; + required?: boolean; }) => { const { host, workspace, repository, branch } = state; @@ -79,7 +81,7 @@ export const BitbucketRepoBranchPicker = ({ return ( 0 && !branch} > ( - + )} freeSolo autoSelect diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..77589e4478 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.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 from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; + +describe('DefaultRepoBranchPicker', () => { + it('renders an input field', () => { + const { getByRole } = render( + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + , + ); + + const input = getByRole('textbox'); + + fireEvent.change(input, { + target: { value: 'develop' }, + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx new file mode 100644 index 0000000000..52a4a15e23 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx @@ -0,0 +1,60 @@ +/* + * 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 FormControl from '@material-ui/core/FormControl'; +import React from 'react'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; + +import { RepoBranchPickerState } from './types'; + +/** + * The underlying component that is rendered in the form for the `DefaultRepoBranchPicker` + * field extension. + * + * @public + * + */ +export const DefaultRepoBranchPicker = ({ + onChange, + state, + rawErrors, + required, +}: { + onChange: (state: RepoBranchPickerState) => void; + state: RepoBranchPickerState; + rawErrors: string[]; + required?: boolean; +}) => { + const { branch } = state; + + return ( + 0 && !branch} + > + Branch + onChange({ branch: e.target.value })} + value={branch} + /> + The branch of the repository + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx index eb5ebd1730..ece73670f6 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx @@ -70,9 +70,7 @@ describe('RepoBranchPicker', () => { }} onSubmit={onSubmit} formContext={{ - formData: { - repoUrl: 'bitbucket.org', - }, + formData: {}, }} /> @@ -82,11 +80,7 @@ describe('RepoBranchPicker', () => { const input = getByRole('textbox'); const submitButton = getByRole('button'); - act(() => { - input.focus(); - fireEvent.change(input, { target: { value: 'branch1' } }); - input.blur(); - }); + fireEvent.change(input, { target: { value: 'branch1' } }); fireEvent.click(submitButton); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx index 8ab83bd717..4ca22c8f80 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx @@ -25,10 +25,11 @@ import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; import Box from '@material-ui/core/Box'; import Divider from '@material-ui/core/Divider'; import Typography from '@material-ui/core/Typography'; -import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; import { RepoBranchPickerProps } from './schema'; import { RepoBranchPickerState } from './types'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; /** * The underlying component that is rendered in the form for the `RepoBranchPicker` @@ -37,8 +38,15 @@ import { RepoBranchPickerState } from './types'; * @public */ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { - const { uiSchema, onChange, rawErrors, formData, schema, formContext } = - props; + const { + uiSchema, + onChange, + rawErrors, + formData, + schema, + formContext, + required, + } = props; const { formData: { repoUrl }, } = formContext; @@ -111,6 +119,33 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { const hostType = (host && integrationApi.byHost(host)?.type) ?? null; + const renderRepoBranchPicker = () => { + switch (hostType) { + case 'bitbucket': + return ( + + ); + default: + return ( + + ); + } + }; + return ( <> {schema.title && ( @@ -122,17 +157,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { {schema.description && ( {schema.description} )} - {hostType === 'bitbucket' && ( - - )} + {renderRepoBranchPicker()} ); }; From 7d82969f10086d0d062af813476df906b278e438 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 11:56:51 +0200 Subject: [PATCH 021/204] chore: update BitbucketRepoBranchPicker and handleAutocompleteRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 12 ++++++++---- .../src/autocomplete/autocomplete.ts | 8 ++++---- .../BitbucketRepoBranchPicker.test.tsx | 4 +++- .../BitbucketRepoBranchPicker.tsx | 17 +++++++++-------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts index b124c65f78..d99a8358b9 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -95,12 +95,16 @@ describe('handleAutocompleteRequest', () => { }); it('should return branches', async () => { - const result = await handleBitbucketCloudRequest('foo', 'branches', { - workspace: 'workspace1', - repository: 'repository1', + const result = await handleAutocompleteRequest({ + token: 'foo', + resource: 'branches', + context: { + workspace: 'workspace1', + repository: 'repository1', + }, }); - expect(result).toEqual(['branch1']); + expect(result).toEqual({ results: [{ title: 'branch1' }] }); }); it('should throw an error when passing an invalid resource', async () => { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 70824995ac..58d2bc9c6d 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -78,21 +78,21 @@ export async function handleAutocompleteRequest({ return { results: result.map(title => ({ title })) }; } case 'branches': { - if (!parameters.workspace || !parameters.repository) + if (!context.workspace || !context.repository) throw new InputError( - 'Missing workspace and/or repository query parameter', + 'Missing workspace and/or repository context parameter', ); const result: string[] = []; for await (const page of client - .listBranchesByRepository(parameters.repository, parameters.workspace) + .listBranchesByRepository(context.repository, context.workspace) .iteratePages()) { const names = [...page.values!].map(p => p.name!); result.push(...names); } - return result; + return { results: result.map(title => ({ title })) }; } default: throw new InputError(`Invalid resource: ${resource}`); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx index 6cbc561bc9..638a05b4ce 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx @@ -26,7 +26,9 @@ import userEvent from '@testing-library/user-event'; describe('BitbucketRepoBranchPicker', () => { const scaffolderApiMock: Partial = { - autocomplete: jest.fn().mockResolvedValue(['branch1']), + autocomplete: jest + .fn() + .mockResolvedValue({ results: [{ title: 'branch1' }] }), }; it('renders an input field', () => { diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 76de25dba0..ed54cddc5e 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -57,16 +57,17 @@ export const BitbucketRepoBranchPicker = ({ host === 'bitbucket.org' && accessToken && workspace && - repository + repository && + scaffolderApi.autocomplete ) { - const result = await scaffolderApi.autocomplete( - accessToken, - 'bitbucketCloud', - 'branches', - { workspace, repository }, - ); + const { results } = await scaffolderApi.autocomplete({ + token: accessToken, + resource: 'branches', + context: { workspace, repository }, + provider: 'bitbucket-cloud', + }); - setAvailableBranches(result); + setAvailableBranches(results.map(r => r.title)); } else { setAvailableBranches([]); } From caf7fc23c3f172a9744b9bbf72ff9a92f8094e10 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 13:16:50 +0200 Subject: [PATCH 022/204] docs: extend documentation Signed-off-by: Benjamin Janssens --- .../software-templates/ui-options-examples.md | 27 ++++++++++++++++++ .../software-templates/writing-templates.md | 28 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/docs/features/software-templates/ui-options-examples.md b/docs/features/software-templates/ui-options-examples.md index 7f0e5b225f..1a024569a6 100644 --- a/docs/features/software-templates/ui-options-examples.md +++ b/docs/features/software-templates/ui-options-examples.md @@ -441,3 +441,30 @@ repoUrl: `secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +## RepoBranchPicker + +The input props that can be specified under `ui:options` for the `RepoBranchPicker` field extension. + +### `requestUserCredentials` + +If defined will request user credentials to auth against the given SCM platform. + +```yaml +repoUrl: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN + additionalScopes: + github: + - workflow:write +``` + +`secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. + +The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +If you're also using the `RepoUrlPicker` field extension, you should simply duplicate this part from there. diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 15591eeb67..12bf65765a 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -477,6 +477,34 @@ template can be published to multiple providers. Note, that you will need to configure an [authentication provider](../../auth/index.md#configuring-authentication-providers), alongside the [`ScmAuthApi`](../../auth/index.md#scaffolder-configuration-software-templates) for your source code management (SCM) service to make this feature work. +### The Repository Branch Picker + +Similar to the repository picker, there is a picker for branches to support autocompletion. A full example could look like this: + +```yaml +- title: Choose a branch + required: + - repoBranch + properties: + repoBranch: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN +``` + +Passing the `requestUserCredentials` object is required for autocompletion to work. +If you're also using the repository picker, you should simply duplicate this part from there. +For more information regarding the `requestUserCredentials` object, please refer to the [Using the Users `oauth` token](#using-the-users-oauth-token) section under [The Repository Picker](#the-repository-picker). + +For a list of all possible `ui:options` input props for `RepoBranchPicker`, please visit [here](./ui-options-examples.md#repobranchpicker). + +The `RepoBranchPicker` is a custom field that we provide part of the +`plugin-scaffolder`. You can provide your own custom fields by +[writing your own Custom Field Extensions](./writing-custom-field-extensions.md) + ### Accessing the signed-in users details Sometimes when authoring templates, you'll want to access the user that is running the template, and get details from the profile or the users `Entity` in the Catalog. From 3fca64320d18d8093557ccc666a3c3769b8d9535 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 13:24:57 +0200 Subject: [PATCH 023/204] chore: add changesets Signed-off-by: Benjamin Janssens --- .changeset/fast-bulldogs-relax.md | 5 +++++ .changeset/tough-goats-hang.md | 5 +++++ .changeset/witty-timers-marry.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/fast-bulldogs-relax.md create mode 100644 .changeset/tough-goats-hang.md create mode 100644 .changeset/witty-timers-marry.md diff --git a/.changeset/fast-bulldogs-relax.md b/.changeset/fast-bulldogs-relax.md new file mode 100644 index 0000000000..65895345e9 --- /dev/null +++ b/.changeset/fast-bulldogs-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +--- + +Added autocompletion support for resource `branches` diff --git a/.changeset/tough-goats-hang.md b/.changeset/tough-goats-hang.md new file mode 100644 index 0000000000..6090c582d0 --- /dev/null +++ b/.changeset/tough-goats-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added field extension `RepoBranchPicker` diff --git a/.changeset/witty-timers-marry.md b/.changeset/witty-timers-marry.md new file mode 100644 index 0000000000..4193acb6fc --- /dev/null +++ b/.changeset/witty-timers-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bitbucket-cloud-common': patch +--- + +Added method `listBranchesByRepository` to `BitbucketCloudClient` From 1ae05800608f8e39c2b1e4eaec8efeeff724ff5c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 13:26:31 +0200 Subject: [PATCH 024/204] chore: update API reports Signed-off-by: Benjamin Janssens --- plugins/bitbucket-cloud-common/api-report.md | 9 ++++++++ plugins/scaffolder/api-report.md | 22 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 34c245facc..833a367c03 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -12,6 +12,12 @@ export class BitbucketCloudClient { config: BitbucketCloudIntegrationConfig, ): BitbucketCloudClient; // (undocumented) + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination; + // (undocumented) listProjectsByWorkspace( workspace: string, options?: FilterAndSortOptions & PartialResponseOptions, @@ -209,6 +215,9 @@ export namespace Models { size?: number; values?: Array | Set; } + export interface PaginatedBranches extends Paginated { + values?: Set; + } export interface PaginatedProjects extends Paginated { values?: Set; } diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index f479b13ec4..6641cda26b 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -384,6 +384,28 @@ export const OwnerPickerFieldSchema: FieldSchema< // @public export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; +// @public +export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2< + string, + { + requestUserCredentials?: + | { + secretsKey: string; + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + gitea?: string[] | undefined; + } + | undefined; + } + | undefined; + } +>; + // @public export const repoPickerValidation: ( value: string, From 362ae34cb4b9a87a8a3e41f5a7c94bcbee7eb346 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 13:53:18 +0200 Subject: [PATCH 025/204] refactor: extract BaseRepoBranchPickerProps Signed-off-by: Benjamin Janssens --- .../RepoBranchPicker/BitbucketRepoBranchPicker.tsx | 10 +++------- .../RepoBranchPicker/DefaultRepoBranchPicker.tsx | 9 ++------- .../src/components/fields/RepoBranchPicker/types.ts | 7 +++++++ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index ed54cddc5e..f6e6634ed8 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -21,7 +21,7 @@ import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; import { useApi } from '@backstage/core-plugin-api'; -import { RepoBranchPickerState } from './types'; +import { BaseRepoBranchPickerProps } from './types'; import FormHelperText from '@material-ui/core/FormHelperText'; /** @@ -37,13 +37,9 @@ export const BitbucketRepoBranchPicker = ({ rawErrors, accessToken, required, -}: { - onChange: (state: RepoBranchPickerState) => void; - state: RepoBranchPickerState; - rawErrors: string[]; +}: BaseRepoBranchPickerProps<{ accessToken?: string; - required?: boolean; -}) => { +}>) => { const { host, workspace, repository, branch } = state; const [availableBranches, setAvailableBranches] = useState([]); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx index 52a4a15e23..9350e232f8 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx @@ -20,7 +20,7 @@ import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; -import { RepoBranchPickerState } from './types'; +import { BaseRepoBranchPickerProps } from './types'; /** * The underlying component that is rendered in the form for the `DefaultRepoBranchPicker` @@ -34,12 +34,7 @@ export const DefaultRepoBranchPicker = ({ state, rawErrors, required, -}: { - onChange: (state: RepoBranchPickerState) => void; - state: RepoBranchPickerState; - rawErrors: string[]; - required?: boolean; -}) => { +}: BaseRepoBranchPickerProps) => { const { branch } = state; return ( diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts index 1d54ea7e8b..bd218d9e28 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts @@ -20,3 +20,10 @@ export interface RepoBranchPickerState { repository?: string; branch?: string; } + +export type BaseRepoBranchPickerProps = T & { + onChange: (state: RepoBranchPickerState) => void; + state: RepoBranchPickerState; + rawErrors: string[]; + required?: boolean; +}; From 3cea09fcf2c9cb8a990ff7ddd9be42174e8958c6 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 14:09:27 +0200 Subject: [PATCH 026/204] chore: remove unnecessary mapping from handleAutocompleteRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 58d2bc9c6d..75cc170a30 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -34,29 +34,29 @@ export async function handleAutocompleteRequest({ switch (resource) { case 'workspaces': { - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client.listWorkspaces().iteratePages()) { - const slugs = [...page.values!].map(p => p.slug!); - result.push(...slugs); + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'projects': { if (!context.workspace) throw new InputError('Missing workspace context parameter'); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listProjectsByWorkspace(context.workspace) .iteratePages()) { - const keys = [...page.values!].map(p => p.key!); - result.push(...keys); + const keys = [...page.values!].map(p => ({ title: p.key! })); + results.push(...keys); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'repositories': { if (!context.workspace || !context.project) @@ -64,18 +64,18 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or project context parameter', ); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listRepositoriesByWorkspace(context.workspace, { q: `project.key="${context.project}"`, }) .iteratePages()) { - const slugs = [...page.values!].map(p => p.slug!); - result.push(...slugs); + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'branches': { if (!context.workspace || !context.repository) @@ -83,16 +83,16 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or repository context parameter', ); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listBranchesByRepository(context.repository, context.workspace) .iteratePages()) { - const names = [...page.values!].map(p => p.name!); - result.push(...names); + const names = [...page.values!].map(p => ({ title: p.name! })); + results.push(...names); } - return { results: result.map(title => ({ title })) }; + return { results }; } default: throw new InputError(`Invalid resource: ${resource}`); From 618aa1454890c9c2c499f3a2c178993978308a95 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 14:14:20 +0200 Subject: [PATCH 027/204] refactor: use useCallback for updateAvailableBranches Co-authored-by: Camila Belo Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index f6e6634ed8..76b8d071ea 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -46,34 +46,34 @@ export const BitbucketRepoBranchPicker = ({ const scaffolderApi = useApi(scaffolderApiRef); - useDebounce( - () => { - const updateAvailableBranches = async () => { - if ( - host === 'bitbucket.org' && - accessToken && - workspace && - repository && - scaffolderApi.autocomplete - ) { - const { results } = await scaffolderApi.autocomplete({ - token: accessToken, - resource: 'branches', - context: { workspace, repository }, - provider: 'bitbucket-cloud', - }); + const updateAvailableBranches = useCallback(() => { + if ( + !scaffolderApi.autocomplete || + !workspace || + !repository || + !accessToken || + host !== 'bitbucket.org' + ) { + setAvailableBranches([]); + return; + } - setAvailableBranches(results.map(r => r.title)); - } else { - setAvailableBranches([]); - } - }; + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'branches', + context: { workspace, repository }, + provider: 'bitbucket-cloud', + }) + .then(({ results }) => { + setAvailableBranches(results.map(r => r.title)); + }) + .catch(() => { + setAvailableBranches([]); + }); + }, [host, workspace, repository, accessToken, scaffolderApi]); - updateAvailableBranches().catch(() => setAvailableBranches([])); - }, - 500, - [host, workspace, repository, accessToken], - ); + useDebounce(updateAvailableBranches, 500, [updateAvailableBranches]); return ( Date: Thu, 4 Jul 2024 16:08:13 +0200 Subject: [PATCH 028/204] test: simplify checking for the secret in the document; remove SecretsComponent from test that didn't use it Signed-off-by: Benjamin Janssens --- .../RepoBranchPicker.test.tsx | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx index ece73670f6..f9c23be407 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx @@ -133,13 +133,15 @@ describe('RepoBranchPicker', () => { describe('requestUserCredentials', () => { it('should call the scmAuthApi with the correct params', async () => { + const secretsKey = 'testKey'; + const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); + const secret = secrets[secretsKey]; + return secret ?
{secret}
: null; }; - const { getByTestId } = await renderInTestApp( + + const { getByText } = await renderInTestApp( { 'ui:field': 'RepoBranchPicker', 'ui:options': { requestUserCredentials: { - secretsKey: 'testKey', + secretsKey, additionalScopes: { github: ['workflow'] }, }, }, @@ -190,22 +192,10 @@ describe('RepoBranchPicker', () => { }, }); - const currentSecrets = JSON.parse( - getByTestId('current-secrets').textContent!, - ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, - }); + expect(getByText('abc123')).toBeInTheDocument(); }); it('should call the scmAuthApi with the correct params if workspace is nested', async () => { - const SecretsComponent = () => { - const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); - }; await renderInTestApp( { }, }} /> - , ); @@ -256,13 +245,15 @@ describe('RepoBranchPicker', () => { }); it('should not call the scmAuthApi if secret is available in the state', async () => { + const secretsKey = 'testKey'; + const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); + const secret = secrets[secretsKey]; + return secret ?
{secret}
: null; }; - const { getByTestId } = await renderInTestApp( + + const { getByText } = await renderInTestApp( { [scaffolderApiRef, {}], ]} > - + { 'ui:field': 'RepoBranchPicker', 'ui:options': { requestUserCredentials: { - secretsKey: 'testKey', + secretsKey, additionalScopes: { github: ['workflow'] }, }, }, @@ -306,13 +297,7 @@ describe('RepoBranchPicker', () => { // as we already have a secret in the state, getCredentials should not be called again. expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); - const currentSecrets = JSON.parse( - getByTestId('current-secrets').textContent!, - ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, - }); + expect(getByText('abc123')).toBeInTheDocument(); }); }); }); From 3956205ad85fb808386edfac1dab7ff273216225 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 16:31:01 +0200 Subject: [PATCH 029/204] fix: import useCallback in BitbucketRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 76b8d071ea..26853d6d05 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -16,7 +16,7 @@ import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; From e2d34cfcdcf41900d2351b20dcc370082e2cfb88 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Mon, 8 Jul 2024 13:04:33 +0200 Subject: [PATCH 030/204] Prioritize user documentation over README (notifications, signals) Selected content of notifications and signals README files has been moved upwards to the user documentation. Signed-off-by: Marek Libra --- docs/notifications/getting-started.md | 80 ++++++++++++++++++++----- plugins/notifications-backend/README.md | 58 ++---------------- plugins/notifications/README.md | 8 ++- 3 files changed, 78 insertions(+), 68 deletions(-) diff --git a/docs/notifications/getting-started.md b/docs/notifications/getting-started.md index 6a7ea6314e..583be64cc8 100644 --- a/docs/notifications/getting-started.md +++ b/docs/notifications/getting-started.md @@ -51,6 +51,8 @@ All parametrization is done through component properties, such as the `Notificat ![Notifications Page](notificationsPage.png) +In the `packages/app/src/components/Root/Root.tsx`, tweak the [properties](https://backstage.io/docs/reference/plugin-notifications.notificationssidebaritem) of the `` per specific needs. + ## Use New notifications can be sent either by a backend plugin or an external service through the REST API. @@ -134,12 +136,70 @@ notificationsApi.getNotification(yourId); notificationsApi.getNotification(lastSignal.notification_id); ``` +### Extending Notifications via Processors + +The notifications can be extended with `NotificationProcessor`. These processors allow to decorate notifications before they are sent or/and send the notifications to external services. + +Depending on the needs, a processor can modify the content of a notification or route it to different systems like email, Slack, or other services. + +A good example of how to write a processor is the [Email Processor](https://github.com/backstage/backstage/tree/master/plugins/notifications-backend-module-email). + +Start off by creating a notification processor: + +```ts +import { Notification } from '@backstage/plugin-notifications-common'; +import { NotificationProcessor } from '@backstage/plugin-notifications-node'; + +class MyNotificationProcessor implements NotificationProcessor { + async decorate(notification: Notification): Promise { + if (notification.origin === 'plugin-my-plugin') { + notification.payload.icon = 'my-icon'; + } + return notification; + } + + async send(notification: Notification): Promise { + nodemailer.sendEmail({ + from: 'backstage', + to: 'user', + subject: notification.payload.title, + text: notification.payload.description, + }); + } +} +``` + +Both of the processing functions are optional, and you can implement only one of them. + +Add the notification processor to the notification system by: + +```ts +import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; +import { Notification } from '@backstage/plugin-notifications-common'; + +export const myPlugin = createBackendPlugin({ + pluginId: 'myPlugin', + register(env) { + env.registerInit({ + deps: { + notifications: notificationsProcessingExtensionPoint, + // ... + }, + async init({ notifications }) { + // ... + notifications.addProcessor(new MyNotificationProcessor()); + }, + }); + }, +}); +``` + ### External Services When the emitter of a notification is a Backstage backend plugin, it is mandatory to use the integration via `@backstage/plugin-notifications-node` as described above. If the emitter is a service external to Backstage, an HTTP POST request can be issued directly to the API, assuming that authentication is properly configured. -Refer to the service-to-service auth documentation for more details, focusing on the Static Tokens section for the simplest setup option. +Refer to the [service-to-service auth documentation](https://backstage.io/docs/auth/service-to-service-auth) for more details, focusing on the Static Tokens section for the simplest setup option. An example request for creating a broadcast notification might look like: @@ -149,20 +209,12 @@ curl -X POST https://[BACKSTAGE_BACKEND]/api/notifications -H "Content-Type: app ## Additional info -Up-to-date documentation can be found in the plugins' README files: +Additional details can be found in the plugins' implementation: -- https://github.com/backstage/backstage/blob/master/plugins/notifications/README.md +- https://github.com/backstage/backstage/blob/master/plugins/notifications -- https://github.com/backstage/backstage/blob/master/plugins/notifications-backend/README.md +- https://github.com/backstage/backstage/blob/master/plugins/notifications-backend -- https://github.com/backstage/backstage/blob/master/plugins/notifications-node/README.md +- https://github.com/backstage/backstage/blob/master/plugins/notifications-node -- https://github.com/backstage/backstage/blob/master/plugins/signals-react/README.md - -## Processors - -The default backend behavior when emitting a new notification can be tweaked using processors. - -Depending on the needs, a processor can modify the content of a notification or route it to different systems like email, Slack, or other services. - -A good example of how to write a processor is the [Email Processor](https://github.com/backstage/backstage/tree/master/plugins/notifications-backend-module-email). +- https://github.com/backstage/backstage/blob/master/plugins/signals-react diff --git a/plugins/notifications-backend/README.md b/plugins/notifications-backend/README.md index 60c9a4e078..ebd129f572 100644 --- a/plugins/notifications-backend/README.md +++ b/plugins/notifications-backend/README.md @@ -4,6 +4,10 @@ Welcome to the notifications backend plugin! ## Getting started +```bash +yarn workspace backend add @backstage/notifications-backend +``` + Add the notifications to your backend: ```ts @@ -18,58 +22,8 @@ the signals plugin (`@backstage/plugin-signals-node`, `@backstage/plugin-signals ## Extending Notifications -The notifications can be extended with `NotificationProcessor`. These processors allow to decorate notifications -before they are sent or/and send the notifications to external services. - -Start off by creating a notification processor: - -```ts -import { Notification } from '@backstage/plugin-notifications-common'; -import { NotificationProcessor } from '@backstage/plugin-notifications-node'; - -class MyNotificationProcessor implements NotificationProcessor { - async decorate(notification: Notification): Promise { - if (notification.origin === 'plugin-my-plugin') { - notification.payload.icon = 'my-icon'; - } - return notification; - } - - async send(notification: Notification): Promise { - nodemailer.sendEmail({ - from: 'backstage', - to: 'user', - subject: notification.payload.title, - text: notification.payload.description, - }); - } -} -``` - -Both of the processing functions are optional, and you can implement only one of them. - -Add the notification processor to the notification system by: - -```ts -import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; -import { Notification } from '@backstage/plugin-notifications-common'; - -export const myPlugin = createBackendPlugin({ - pluginId: 'myPlugin', - register(env) { - env.registerInit({ - deps: { - notifications: notificationsProcessingExtensionPoint, - // ... - }, - async init({ notifications }) { - // ... - notifications.addProcessor(new MyNotificationProcessor()); - }, - }); - }, -}); -``` +When a notification is created, it's processing can be customized via `processors`. +Please refer [Backstage documentation](https://backstage.io/docs/notifications) for further details. ## Sending Notifications By Backend Plugins diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md index 63212d2c16..32fbb8c701 100644 --- a/plugins/notifications/README.md +++ b/plugins/notifications/README.md @@ -2,13 +2,17 @@ Welcome to the notifications plugin! -_This plugin was created through the Backstage CLI_ - ## Getting started First, install the `@backstage/plugin-notifications-backend` and `@backstage/plugin-notifications-node` packages. See the documentation for installation instructions. +Then add this frontend package: + +```bash +yarn workspace app add @backstage/notifications +``` + To add the notifications main menu, add the following to your `packages/app/src/components/Root/Root.tsx`: ```tsx From ba9abf4fbac58b94508ce7360a318313832e1cfd Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 12 Jul 2024 15:24:05 -0600 Subject: [PATCH 031/204] Allow manual trigger frequency in backend-tasks Signed-off-by: Tim Hansen --- .changeset/hip-fishes-guess.md | 6 +++ .changeset/metal-planes-nail.md | 5 +++ .../20240712211735_nullable_next_run.js | 37 +++++++++++++++++++ .../lib/PluginTaskSchedulerImpl.test.ts | 3 ++ .../scheduler/lib/PluginTaskSchedulerImpl.ts | 3 ++ .../scheduler/lib/TaskWorker.test.ts | 28 ++++++++++++++ .../entrypoints/scheduler/lib/TaskWorker.ts | 17 +++++++-- .../src/entrypoints/scheduler/lib/types.ts | 9 +++++ .../services/definitions/SchedulerService.ts | 22 ++++++++--- .../src/tasks/PluginTaskSchedulerImpl.ts | 3 ++ .../src/tasks/TaskWorker.test.ts | 28 ++++++++++++++ .../backend-tasks/src/tasks/TaskWorker.ts | 17 +++++++-- .../readTaskScheduleDefinitionFromConfig.ts | 12 ++++-- packages/backend-tasks/src/tasks/types.ts | 7 +++- 14 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 .changeset/hip-fishes-guess.md create mode 100644 .changeset/metal-planes-nail.md create mode 100644 packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js diff --git a/.changeset/hip-fishes-guess.md b/.changeset/hip-fishes-guess.md new file mode 100644 index 0000000000..909448c673 --- /dev/null +++ b/.changeset/hip-fishes-guess.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-defaults': patch +--- + +The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. diff --git a/.changeset/metal-planes-nail.md b/.changeset/metal-planes-nail.md new file mode 100644 index 0000000000..2cfa12a435 --- /dev/null +++ b/.changeset/metal-planes-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. diff --git a/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js new file mode 100644 index 0000000000..b636df35d7 --- /dev/null +++ b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js @@ -0,0 +1,37 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.setNullable('next_run_start_at'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.dropNullable('next_run_start_at'); + }); +}; diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts index c5c4c8708a..0c9219e98e 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -347,6 +347,9 @@ describe('PluginTaskManagerImpl', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + expect(parseDuration({ trigger: 'manual' })).toEqual({ + trigger: 'manual', + }); }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts index 40aa9d521c..f8d06baf56 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts @@ -155,6 +155,9 @@ export function parseDuration( if (typeof frequency === 'object' && 'cron' in frequency) { return frequency.cron; } + if (typeof frequency === 'object' && 'trigger' in frequency) { + return frequency.trigger; + } const parsed = Duration.isDuration(frequency) ? frequency diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts index 1cafe00670..5db82f3ece 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts @@ -503,4 +503,32 @@ describe('TaskWorker', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is not set for manually-triggered tasks, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'manual', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + const row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.next_run_start_at).toBeNull(); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts index a1c4ec44fd..c5570a8e30 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts @@ -53,8 +53,8 @@ export class TaskWorker { ); let workCheckFrequency = this.workCheckFrequency; - const isCron = !settings?.cadence.startsWith('P'); - if (!isCron) { + const isDuration = settings?.cadence.startsWith('P'); + if (isDuration) { const cadence = Duration.fromISO(settings.cadence); if (cadence < workCheckFrequency) { workCheckFrequency = cadence; @@ -175,7 +175,9 @@ export class TaskWorker { // read it back again. taskSettingsV2Schema.parse(settings); - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let startAt: Knex.Raw | undefined; let nextStartAt: Knex.Raw | undefined; @@ -194,6 +196,9 @@ export class TaskWorker { nextStartAt = this.nextRunAtRaw(time); startAt ||= nextStartAt; + } else if (isManual) { + nextStartAt = this.knex.raw('null'); + startAt ||= nextStartAt; } else { startAt ||= this.knex.fn.now(); nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex); @@ -317,7 +322,9 @@ export class TaskWorker { ticket: string, settings: TaskSettingsV2, ): Promise { - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let nextRun: Knex.Raw; if (isCron) { @@ -325,6 +332,8 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); nextRun = this.nextRunAtRaw(time); + } else if (isManual) { + nextRun = this.knex.raw('null'); } else { const dt = Duration.fromISO(settings.cadence).as('seconds'); this.logger.debug( diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts index 61e6c1d9a8..173c989543 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts @@ -40,6 +40,10 @@ function isValidCronFormat(c: string | undefined): boolean { } } +function isValidTrigger(t: string): boolean { + return t === 'manual'; +} + export const taskSettingsV1Schema = z.object({ version: z.literal(1), initialDelayDuration: z @@ -68,6 +72,11 @@ export const taskSettingsV2Schema = z.object({ cadence: z .string() .refine(isValidCronFormat, { message: 'Invalid cron' }) + .or( + z.string().refine(isValidTrigger, { + message: "Invalid trigger, expecting 'manual'", + }), + ) .or( z.string().refine(isValidOptionalDurationString, { message: 'Invalid duration, expecting ISO Period', diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts index a7ec79a336..a8b84b9084 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts @@ -96,7 +96,8 @@ export interface SchedulerServiceTaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -193,7 +194,12 @@ export interface SchedulerServiceTaskScheduleDefinitionConfig { cron: string; } | string - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -361,14 +367,20 @@ function readDuration(config: Config, key: string): HumanDuration { return readDurationFromConfig(config, { key }); } -function readCronOrDuration( +function readFrequency( config: Config, key: string, -): { cron: string } | HumanDuration { +): { cron: string } | HumanDuration | { trigger: 'manual' } { const value = config.get(key); if (typeof value === 'object' && (value as { cron?: string }).cron) { return value as { cron: string }; } + if ( + typeof value === 'object' && + (value as { trigger?: string }).trigger === 'manual' + ) { + return { trigger: 'manual' }; + } return readDuration(config, key); } @@ -383,7 +395,7 @@ function readCronOrDuration( export function readSchedulerServiceTaskScheduleDefinitionFromConfig( config: Config, ): SchedulerServiceTaskScheduleDefinition { - const frequency = readCronOrDuration(config, 'frequency'); + const frequency = readFrequency(config, 'frequency'); const timeout = readDuration(config, 'timeout'); const initialDelay = config.has('initialDelay') diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index b1647f4edf..0ccc77a6c1 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -152,6 +152,9 @@ export function parseDuration( if ('cron' in frequency) { return frequency.cron; } + if ('trigger' in frequency) { + return frequency.trigger; + } const parsed = Duration.isDuration(frequency) ? frequency diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 10ebfde5b4..829d7754f8 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -503,4 +503,32 @@ describe('TaskWorker', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is not set for manually-triggered tasks, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'manual', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + const row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.next_run_start_at).toBeNull(); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 57cbf2203e..a50b988522 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -52,8 +52,8 @@ export class TaskWorker { ); let workCheckFrequency = this.workCheckFrequency; - const isCron = !settings?.cadence.startsWith('P'); - if (!isCron) { + const isDuration = settings?.cadence.startsWith('P'); + if (isDuration) { const cadence = Duration.fromISO(settings.cadence); if (cadence < workCheckFrequency) { workCheckFrequency = cadence; @@ -174,7 +174,9 @@ export class TaskWorker { // read it back again. taskSettingsV2Schema.parse(settings); - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let startAt: Knex.Raw | undefined; let nextStartAt: Knex.Raw | undefined; @@ -193,6 +195,9 @@ export class TaskWorker { nextStartAt = this.nextRunAtRaw(time); startAt ||= nextStartAt; + } else if (isManual) { + nextStartAt = this.knex.raw('null'); + startAt ||= nextStartAt; } else { startAt ||= this.knex.fn.now(); nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex); @@ -316,7 +321,9 @@ export class TaskWorker { ticket: string, settings: TaskSettingsV2, ): Promise { - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let nextRun: Knex.Raw; if (isCron) { @@ -324,6 +331,8 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); nextRun = this.nextRunAtRaw(time); + } else if (isManual) { + nextRun = this.knex.raw('null'); } else { const dt = Duration.fromISO(settings.cadence).as('seconds'); this.logger.debug( diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts index e94d06d5c3..85099d82db 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts @@ -32,14 +32,20 @@ function readDuration(config: Config, key: string): HumanDuration { return readDurationFromConfig(config, { key }); } -function readCronOrDuration( +function readFrequency( config: Config, key: string, -): { cron: string } | Duration | HumanDuration { +): { cron: string } | Duration | HumanDuration | { trigger: 'manual' } { const value = config.get(key); if (typeof value === 'object' && (value as { cron?: string }).cron) { return value as { cron: string }; } + if ( + typeof value === 'object' && + (value as { trigger?: string }).trigger === 'manual' + ) { + return { trigger: 'manual' }; + } return readDuration(config, key); } @@ -56,7 +62,7 @@ function readCronOrDuration( export function readTaskScheduleDefinitionFromConfig( config: Config, ): TaskScheduleDefinition { - const frequency = readCronOrDuration(config, 'frequency'); + const frequency = readFrequency(config, 'frequency'); const timeout = readDuration(config, 'timeout'); const initialDelay = config.has('initialDelay') diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index e6a31873be..aaa6ca8bd2 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -100,7 +100,12 @@ export interface TaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before From be632623e5e3978660099002bc0f12dac4bf8ec3 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 09:50:42 +0200 Subject: [PATCH 032/204] chore: improve changeset for @backstage/plugin-scaffolder Signed-off-by: Benjamin Janssens --- .changeset/tough-goats-hang.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tough-goats-hang.md b/.changeset/tough-goats-hang.md index 6090c582d0..2608c03b2e 100644 --- a/.changeset/tough-goats-hang.md +++ b/.changeset/tough-goats-hang.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': minor --- -Added field extension `RepoBranchPicker` +Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket From 5436d971d92dd9d60645e4bdcc4869de5405bb5f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 09:51:52 +0200 Subject: [PATCH 033/204] chore: add comment to schema to link duplicated comments together Signed-off-by: Benjamin Janssens --- .../scaffolder/src/components/fields/RepoBranchPicker/schema.ts | 1 + plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts index 6ee67bd1c7..fae5e6435e 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts @@ -77,6 +77,7 @@ export type RepoBranchPickerUiOptions = export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type; +// This has been duplicated from /plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts index 2a4691380f..2ec72bc5a6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -97,6 +97,7 @@ export type RepoUrlPickerUiOptions = export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type; +// This has been duplicated to /plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined From 1e6c3ac445c32330cc25f52234ad007daa8842a7 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 16 Jul 2024 08:30:02 -0400 Subject: [PATCH 034/204] Move migration tests Signed-off-by: Tim Hansen --- packages/backend-defaults/migrations.test.ts | 128 ++++++++++++++++++ .../20240712211735_nullable_next_run.js | 37 +++++ packages/backend-tasks/src/migrations.test.ts | 44 ++++++ 3 files changed, 209 insertions(+) create mode 100644 packages/backend-defaults/migrations.test.ts create mode 100644 packages/backend-tasks/migrations/20240712211735_nullable_next_run.js diff --git a/packages/backend-defaults/migrations.test.ts b/packages/backend-defaults/migrations.test.ts new file mode 100644 index 0000000000..98f4e41706 --- /dev/null +++ b/packages/backend-defaults/migrations.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { Knex } from 'knex'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import fs from 'fs'; + +const migrationsDir = `${__dirname}/../migrations`; +const migrationsFiles = fs.readdirSync(migrationsDir).sort(); + +async function migrateUpOnce(knex: Knex): Promise { + await knex.migrate.up({ directory: migrationsDir }); +} + +async function migrateDownOnce(knex: Knex): Promise { + await knex.migrate.down({ directory: migrationsDir }); +} + +async function migrateUntilBefore(knex: Knex, target: string): Promise { + const index = migrationsFiles.indexOf(target); + if (index === -1) { + throw new Error(`Migration ${target} not found`); + } + for (let i = 0; i < index; i++) { + await migrateUpOnce(knex); + } +} + +jest.setTimeout(60_000); + +describe('migrations', () => { + const databases = TestDatabases.create(); + + it.each(databases.eachSupportedId())( + '20210928160613_init.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20210928160613_init.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.fn.now(), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: expect.anything(), + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await migrateDownOnce(knex); + + // This looks odd - you might expect a .toThrow at the end but that + // actually is flaky for some reason specifically on sqlite when + // performing multiple runs in sequence + await expect(knex('backstage_backend_tasks__tasks')).rejects.toEqual( + expect.anything(), + ); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + '20240712211735_nullable_next_run.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20240712211735_nullable_next_run.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: null, + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); + + await migrateDownOnce(knex); + + await expect( + knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }), + ).rejects.toEqual(expect.anything()); + + await knex.destroy(); + }, + ); +}); diff --git a/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js new file mode 100644 index 0000000000..b636df35d7 --- /dev/null +++ b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js @@ -0,0 +1,37 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.setNullable('next_run_start_at'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.dropNullable('next_run_start_at'); + }); +}; diff --git a/packages/backend-tasks/src/migrations.test.ts b/packages/backend-tasks/src/migrations.test.ts index 82208da975..49576d8a58 100644 --- a/packages/backend-tasks/src/migrations.test.ts +++ b/packages/backend-tasks/src/migrations.test.ts @@ -81,4 +81,48 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20240712211735_nullable_next_run.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20240712211735_nullable_next_run.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: null, + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); + + await migrateDownOnce(knex); + + await expect( + knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }), + ).rejects.toEqual(expect.anything()); + + await knex.destroy(); + }, + ); }); From 03692ff5396e5ca8c071320deca3bf446ad85880 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 16 Jul 2024 09:34:27 -0400 Subject: [PATCH 035/204] api-reports Signed-off-by: Tim Hansen --- packages/backend-plugin-api/api-report.md | 14 ++++++++++++-- packages/backend-tasks/api-report.md | 9 ++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 7b1e6ef2d3..0938ae4e7e 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -608,7 +608,10 @@ export interface SchedulerServiceTaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + | { + trigger: 'manual'; + }; initialDelay?: Duration | HumanDuration; scope?: 'global' | 'local'; timeout: Duration | HumanDuration; @@ -621,7 +624,14 @@ export interface SchedulerServiceTaskScheduleDefinitionConfig { cron: string; } | string - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { + trigger: 'manual'; + }; initialDelay?: string | HumanDuration; scope?: 'global' | 'local'; timeout: string | HumanDuration; diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 0d5ced044a..c0f9da3a01 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -62,7 +62,14 @@ export interface TaskScheduleDefinition { cron: string; } | Duration - | HumanDuration_2; + | HumanDuration_2 + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { + trigger: 'manual'; + }; initialDelay?: Duration | HumanDuration_2; scope?: 'global' | 'local'; timeout: Duration | HumanDuration_2; From c8b0732b5cf798b703181adaf1ce70a0ea367d5c Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 16 Jul 2024 09:59:15 -0400 Subject: [PATCH 036/204] Fix parseDuration test Signed-off-by: Tim Hansen --- .../entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts | 4 +--- .../backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts index 0c9219e98e..3e0782d4a3 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -347,9 +347,7 @@ describe('PluginTaskManagerImpl', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); - expect(parseDuration({ trigger: 'manual' })).toEqual({ - trigger: 'manual', - }); + expect(parseDuration({ trigger: 'manual' })).toEqual('manual'); }); }); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 77965632e9..eca899098f 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -345,6 +345,7 @@ describe('PluginTaskManagerImpl', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + expect(parseDuration({ trigger: 'manual' })).toEqual('manual'); }); }); }); From 11aaaa496a97a740ef52f080cc06e82b3625752b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 8 Jul 2024 16:08:04 +0200 Subject: [PATCH 037/204] feat: start implementing multiple service factories Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- packages/backend-app-api/api-report-alpha.md | 1 + packages/backend-app-api/api-report.md | 21 +- .../src/wiring/ServiceRegistry.ts | 265 +++++++++++------- packages/backend-defaults/api-report-auth.md | 1 + packages/backend-defaults/api-report-cache.md | 1 + .../backend-defaults/api-report-database.md | 1 + .../backend-defaults/api-report-discovery.md | 1 + .../backend-defaults/api-report-httpAuth.md | 1 + .../backend-defaults/api-report-httpRouter.md | 1 + .../backend-defaults/api-report-lifecycle.md | 1 + .../backend-defaults/api-report-logger.md | 1 + .../api-report-permissions.md | 1 + .../backend-defaults/api-report-rootConfig.md | 4 +- .../backend-defaults/api-report-rootHealth.md | 1 + .../api-report-rootHttpRouter.md | 4 +- .../api-report-rootLifecycle.md | 1 + .../backend-defaults/api-report-rootLogger.md | 1 + .../backend-defaults/api-report-scheduler.md | 1 + .../backend-defaults/api-report-urlReader.md | 1 + .../backend-defaults/api-report-userInfo.md | 1 + .../urlReader/urlReaderServiceFactory.ts | 29 +- .../api-report.md | 7 +- .../backend-plugin-api/api-report-alpha.md | 3 +- packages/backend-plugin-api/api-report.md | 125 ++++++--- packages/backend-plugin-api/src/deprecated.ts | 9 +- .../src/services/system/types.ts | 149 +++++++--- packages/backend-test-utils/api-report.md | 123 +++++--- .../src/next/wiring/ServiceFactoryTester.ts | 37 ++- plugins/catalog-node/api-report-alpha.md | 2 +- plugins/events-node/api-report.md | 3 +- plugins/notifications-node/api-report.md | 6 +- .../search-backend-node/api-report-alpha.md | 6 +- plugins/signals-node/api-report.md | 4 +- 33 files changed, 563 insertions(+), 250 deletions(-) diff --git a/packages/backend-app-api/api-report-alpha.md b/packages/backend-app-api/api-report-alpha.md index e4769f4e72..b811208845 100644 --- a/packages/backend-app-api/api-report-alpha.md +++ b/packages/backend-app-api/api-report-alpha.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const featureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', + true, undefined >; diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 7f9b54dd8f..4019ad3086 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -49,6 +49,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', + true, undefined >; @@ -80,6 +81,7 @@ export interface Backend { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', + true, undefined >; @@ -113,6 +115,7 @@ export interface CreateSpecializedBackendOptions { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', + true, undefined >; @@ -135,6 +138,7 @@ export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', + true, undefined >; @@ -161,6 +165,7 @@ export class HostDiscovery implements DiscoveryService { export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', + true, undefined >; @@ -168,6 +173,7 @@ export const httpAuthServiceFactory: ServiceFactoryCompat< export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', + true, undefined >; @@ -191,6 +197,7 @@ export type IdentityFactoryOptions = { export const identityServiceFactory: ServiceFactoryCompat< IdentityService, 'plugin', + true, IdentityFactoryOptions >; @@ -203,6 +210,7 @@ export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', + true, undefined >; @@ -220,6 +228,7 @@ export function loadBackendConfig(options: { export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', + true, undefined >; @@ -248,6 +257,7 @@ export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', + true, undefined >; @@ -278,6 +288,7 @@ export interface RootConfigFactoryOptions { export const rootConfigServiceFactory: ServiceFactoryCompat< RootConfigService, 'root', + true, RootConfigFactoryOptions >; @@ -294,13 +305,14 @@ export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; // @public @deprecated (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // @public @deprecated export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', + true, undefined >; @@ -308,6 +320,7 @@ export const rootLifecycleServiceFactory: ServiceFactoryCompat< export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + true, undefined >; @@ -315,6 +328,7 @@ export const rootLoggerServiceFactory: ServiceFactoryCompat< export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', + true, undefined >; @@ -322,6 +336,7 @@ export const schedulerServiceFactory: ServiceFactoryCompat< export const tokenManagerServiceFactory: ServiceFactoryCompat< TokenManagerService, 'plugin', + true, undefined >; @@ -329,6 +344,7 @@ export const tokenManagerServiceFactory: ServiceFactoryCompat< export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', + true, undefined >; @@ -336,6 +352,7 @@ export const urlReaderServiceFactory: ServiceFactoryCompat< export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', + true, undefined >; diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 56d8d611bb..e1644dad89 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -58,12 +58,24 @@ function createPluginMetadataServiceFactory(pluginId: string) { export class ServiceRegistry { static create(factories: Array): ServiceRegistry { - const registry = new ServiceRegistry(factories); + const factoryMap = new Map(); + for (const factory of factories) { + if (factory.service.singleton) { + factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]); + } else { + const existing = factoryMap.get(factory.service.id) ?? []; + factoryMap.set( + factory.service.id, + existing.concat(toInternalServiceFactory(factory)), + ); + } + } + const registry = new ServiceRegistry(factoryMap); registry.checkForCircularDeps(); return registry; } - readonly #providedFactories: Map; + readonly #providedFactories: Map; readonly #loadedDefaultFactories: Map< Function, Promise @@ -82,10 +94,8 @@ export class ServiceRegistry { readonly #addedFactoryIds = new Set(); readonly #instantiatedFactories = new Set(); - private constructor(factories: Array) { - this.#providedFactories = new Map( - factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]), - ); + private constructor(factories: Map) { + this.#providedFactories = factories; this.#loadedDefaultFactories = new Map(); this.#implementations = new Map(); } @@ -93,17 +103,17 @@ export class ServiceRegistry { #resolveFactory( ref: ServiceRef, pluginId: string, - ): Promise | undefined { + ): Promise | undefined { // Special case handling of the plugin metadata service, generating a custom factory for it each time if (ref.id === coreServices.pluginMetadata.id) { - return Promise.resolve( + return Promise.resolve([ toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId)), - ); + ]); } let resolvedFactory: - | Promise - | InternalServiceFactory + | Promise + | InternalServiceFactory[] | undefined = this.#providedFactories.get(ref.id); const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; if (!resolvedFactory && !defaultFactory) { @@ -120,15 +130,18 @@ export class ServiceRegistry { ); this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); } - resolvedFactory = loadedFactory.catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the default factory loader threw an error, ${stringifyError( - error, - )}`, - ); - }); + resolvedFactory = loadedFactory.then( + factory => [factory], + error => { + throw new Error( + `Failed to instantiate service '${ + ref.id + }' because the default factory loader threw an error, ${stringifyError( + error, + )}`, + ); + }, + ); } return Promise.resolve(resolvedFactory); @@ -142,6 +155,9 @@ export class ServiceRegistry { if (this.#providedFactories.get(ref.id)) { return false; } + if (!ref.singleton) { + return false; + } return !(ref as InternalServiceRef).__defaultFactory; }); @@ -156,13 +172,13 @@ export class ServiceRegistry { checkForCircularDeps(): void { const graph = DependencyGraph.fromIterable( - Array.from(this.#providedFactories).map( - ([serviceId, serviceFactory]) => ({ - value: serviceId, - provides: [serviceId], - consumes: Object.values(serviceFactory.deps).map(d => d.id), - }), - ), + Array.from(this.#providedFactories).map(([serviceId, factories]) => ({ + value: serviceId, + provides: [serviceId], + consumes: factories.flatMap(factory => + Object.values(factory.deps).map(d => d.id), + ), + })), ); const circularDependencies = Array.from(graph.detectCircularDependencies()); @@ -183,27 +199,36 @@ export class ServiceRegistry { ); } - if (this.#addedFactoryIds.has(factoryId)) { - throw new Error( - `Duplicate service implementations provided for ${factoryId}`, - ); - } - if (this.#instantiatedFactories.has(factoryId)) { throw new Error( `Unable to set service factory with id ${factoryId}, service has already been instantiated`, ); } - this.#addedFactoryIds.add(factoryId); - this.#providedFactories.set(factoryId, toInternalServiceFactory(factory)); + if (factory.service.singleton) { + if (this.#addedFactoryIds.has(factoryId)) { + throw new Error( + `Duplicate service implementations provided for ${factoryId}`, + ); + } + + this.#addedFactoryIds.add(factoryId); + this.#providedFactories.set(factoryId, [ + toInternalServiceFactory(factory), + ]); + } else { + const newFactories = ( + this.#providedFactories.get(factoryId) ?? [] + ).concat(toInternalServiceFactory(factory)); + this.#providedFactories.set(factoryId, newFactories); + } } async initializeEagerServicesWithScope( scope: 'root' | 'plugin', pluginId: string = 'root', ) { - for (const factory of this.#providedFactories.values()) { + for (const [factory] of this.#providedFactories.values()) { if (factory.service.scope === scope) { // Root-scoped services are eager by default, plugin-scoped are lazy by default if (scope === 'root' && factory.initialization !== 'lazy') { @@ -215,88 +240,112 @@ export class ServiceRegistry { } } - get(ref: ServiceRef, pluginId: string): Promise | undefined { + get( + ref: ServiceRef, + pluginId: string, + ): Promise | undefined { this.#instantiatedFactories.add(ref.id); - return this.#resolveFactory(ref, pluginId)?.then(factory => { - if (factory.service.scope === 'root') { - let existing = this.#rootServiceImplementations.get(factory); - if (!existing) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + const resolvedFactory = this.#resolveFactory(ref, pluginId); - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope !== 'root') { - throw new Error( - `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, - ); + if (!resolvedFactory) { + return ref.singleton + ? undefined + : (Promise.resolve([]) as + | Promise + | undefined); + } + + return resolvedFactory + .then(factories => { + return Promise.all( + factories.map(factory => { + if (factory.service.scope === 'root') { + let existing = this.#rootServiceImplementations.get(factory); + if (!existing) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); + + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope !== 'root') { + throw new Error( + `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, + ); + } + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + + existing = Promise.all(rootDeps).then(entries => + factory.factory(Object.fromEntries(entries), undefined), + ); + this.#rootServiceImplementations.set(factory, existing); + } + return existing as Promise; } - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - existing = Promise.all(rootDeps).then(entries => - factory.factory(Object.fromEntries(entries), undefined), - ); - this.#rootServiceImplementations.set(factory, existing); - } - return existing as Promise; - } + let implementation = this.#implementations.get(factory); + if (!implementation) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let implementation = this.#implementations.get(factory); - if (!implementation) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope === 'root') { + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope === 'root') { - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - } + implementation = { + context: Promise.all(rootDeps) + .then(entries => + factory.createRootContext?.(Object.fromEntries(entries)), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`, + ); + }), + byPlugin: new Map(), + }; - implementation = { - context: Promise.all(rootDeps) - .then(entries => - factory.createRootContext?.(Object.fromEntries(entries)), - ) - .catch(error => { - const cause = stringifyError(error); - throw new Error( - `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`, - ); - }), - byPlugin: new Map(), - }; + this.#implementations.set(factory, implementation); + } - this.#implementations.set(factory, implementation); - } + let result = implementation.byPlugin.get(pluginId) as Promise; + if (!result) { + const allDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let result = implementation.byPlugin.get(pluginId) as Promise; - if (!result) { - const allDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + const target = this.get(serviceRef, pluginId)!; + allDeps.push(target.then(impl => [name, impl])); + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - const target = this.get(serviceRef, pluginId)!; - allDeps.push(target.then(impl => [name, impl])); - } - - result = implementation.context - .then(context => - Promise.all(allDeps).then(entries => - factory.factory(Object.fromEntries(entries), context), - ), - ) - .catch(error => { - const cause = stringifyError(error); - throw new Error( - `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, - ); - }); - implementation.byPlugin.set(pluginId, result); - } - - return result; - }); + result = implementation.context + .then(context => + Promise.all(allDeps).then(entries => + factory.factory(Object.fromEntries(entries), context), + ), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, + ); + }); + implementation.byPlugin.set(pluginId, result); + } + return result; + }), + ); + }) + .then(results => (ref.singleton ? results[0] : results)); } } diff --git a/packages/backend-defaults/api-report-auth.md b/packages/backend-defaults/api-report-auth.md index 7df0b394e3..85287c524e 100644 --- a/packages/backend-defaults/api-report-auth.md +++ b/packages/backend-defaults/api-report-auth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-cache.md b/packages/backend-defaults/api-report-cache.md index 0b2d8706df..c6debd6a2b 100644 --- a/packages/backend-defaults/api-report-cache.md +++ b/packages/backend-defaults/api-report-cache.md @@ -28,6 +28,7 @@ export type CacheManagerOptions = { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-database.md b/packages/backend-defaults/api-report-database.md index 0d54b43777..d7585cc791 100644 --- a/packages/backend-defaults/api-report-database.md +++ b/packages/backend-defaults/api-report-database.md @@ -35,6 +35,7 @@ export type DatabaseManagerOptions = { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-discovery.md b/packages/backend-defaults/api-report-discovery.md index dbb469f758..dc0d9b2bbe 100644 --- a/packages/backend-defaults/api-report-discovery.md +++ b/packages/backend-defaults/api-report-discovery.md @@ -11,6 +11,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-httpAuth.md b/packages/backend-defaults/api-report-httpAuth.md index 613e6a4452..d8d283c01e 100644 --- a/packages/backend-defaults/api-report-httpAuth.md +++ b/packages/backend-defaults/api-report-httpAuth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md index a2bf1cd39b..7673367f18 100644 --- a/packages/backend-defaults/api-report-httpRouter.md +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -18,6 +18,7 @@ export function createLifecycleMiddleware( export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-lifecycle.md b/packages/backend-defaults/api-report-lifecycle.md index 36adbbf6e7..ad41fd4fcb 100644 --- a/packages/backend-defaults/api-report-lifecycle.md +++ b/packages/backend-defaults/api-report-lifecycle.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-logger.md b/packages/backend-defaults/api-report-logger.md index c66f886bf4..c881d20344 100644 --- a/packages/backend-defaults/api-report-logger.md +++ b/packages/backend-defaults/api-report-logger.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-permissions.md b/packages/backend-defaults/api-report-permissions.md index 86b9605606..92e3feaeaf 100644 --- a/packages/backend-defaults/api-report-permissions.md +++ b/packages/backend-defaults/api-report-permissions.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 61f5c9dc9d..60fed5734b 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -28,8 +28,8 @@ export interface RootConfigFactoryOptions { // @public (undocumented) export const rootConfigServiceFactory: (( options?: RootConfigFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootHealth.md b/packages/backend-defaults/api-report-rootHealth.md index c69f66e747..ca104ecf37 100644 --- a/packages/backend-defaults/api-report-rootHealth.md +++ b/packages/backend-defaults/api-report-rootHealth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootHealthServiceFactory: ServiceFactoryCompat< RootHealthService, 'root', + true, undefined >; diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index 329398a7b8..a6e5a31428 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -143,8 +143,8 @@ export type RootHttpRouterFactoryOptions = { // @public (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootLifecycle.md b/packages/backend-defaults/api-report-rootLifecycle.md index faa83706a7..52eaa3fe0e 100644 --- a/packages/backend-defaults/api-report-rootLifecycle.md +++ b/packages/backend-defaults/api-report-rootLifecycle.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', + true, undefined >; diff --git a/packages/backend-defaults/api-report-rootLogger.md b/packages/backend-defaults/api-report-rootLogger.md index 075a8b4aa9..d91aaf0ae8 100644 --- a/packages/backend-defaults/api-report-rootLogger.md +++ b/packages/backend-defaults/api-report-rootLogger.md @@ -14,6 +14,7 @@ import { transport } from 'winston'; export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + true, undefined >; diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md index a3467b97de..7d8706def2 100644 --- a/packages/backend-defaults/api-report-scheduler.md +++ b/packages/backend-defaults/api-report-scheduler.md @@ -21,6 +21,7 @@ export class DefaultSchedulerService { export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md index 0980e3cd67..431e2af701 100644 --- a/packages/backend-defaults/api-report-urlReader.md +++ b/packages/backend-defaults/api-report-urlReader.md @@ -429,6 +429,7 @@ export class UrlReaders { export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-userInfo.md b/packages/backend-defaults/api-report-userInfo.md index b77878ff34..31ae1dd8bb 100644 --- a/packages/backend-defaults/api-report-userInfo.md +++ b/packages/backend-defaults/api-report-userInfo.md @@ -10,6 +10,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts index 810882edf8..9859166ba0 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts @@ -14,12 +14,37 @@ * limitations under the License. */ +import { ReaderFactory } from './lib'; import { UrlReaders } from './lib/UrlReaders'; import { coreServices, createServiceFactory, + createServiceRef, } from '@backstage/backend-plugin-api'; +/** + * @public + * A non-singleton reference to URL Reader factory services. + * + * @example + * Creating a service factory implementation for a Custom URL Reader. + * ```ts + * createServiceFactory({ + * service: urlReaderProviderFactoriesServiceRef, + * deps: {}, + * async factory() { + * return CustomUrlReader.factory; + * }, + * }); + * ``` + */ +export const urlReaderProviderFactoriesServiceRef = + createServiceRef({ + id: 'core.urlReader.factories', + scope: 'plugin', + singleton: false, + }); + /** * Reading content from external systems. * @@ -34,11 +59,13 @@ export const urlReaderServiceFactory = createServiceFactory({ deps: { config: coreServices.rootConfig, logger: coreServices.logger, + factories: urlReaderProviderFactoriesServiceRef, }, - async factory({ config, logger }) { + async factory({ config, logger, factories }) { return UrlReaders.default({ config, logger, + factories, }); }, }); diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 39aa60b09b..b74bbcdc95 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -117,6 +117,7 @@ export interface DynamicPluginsFactoryOptions { export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', + true, undefined >; @@ -127,6 +128,7 @@ export const dynamicPluginsFrontendSchemas: BackendFeatureCompat; export const dynamicPluginsRootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + true, undefined >; @@ -147,6 +149,7 @@ export interface DynamicPluginsSchemasService { export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< DynamicPluginsSchemasService, 'root', + true, DynamicPluginsSchemasOptions >; @@ -154,13 +157,15 @@ export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< export const dynamicPluginsServiceFactory: ServiceFactoryCompat< DynamicPluginProvider, 'root', + true, DynamicPluginsFactoryOptions >; // @public (undocumented) export const dynamicPluginsServiceRef: ServiceRef< DynamicPluginProvider, - 'root' + 'root', + true >; // @public (undocumented) diff --git a/packages/backend-plugin-api/api-report-alpha.md b/packages/backend-plugin-api/api-report-alpha.md index 81b378a671..9a82d3adbe 100644 --- a/packages/backend-plugin-api/api-report-alpha.md +++ b/packages/backend-plugin-api/api-report-alpha.md @@ -17,7 +17,8 @@ export interface FeatureDiscoveryService { // @alpha export const featureDiscoveryServiceRef: ServiceRef< FeatureDiscoveryService, - 'root' + 'root', + true >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index b3c35d3699..3925d3b2f4 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -189,28 +189,28 @@ export type CacheServiceSetOptions = { // @public export namespace coreServices { - const auth: ServiceRef; - const userInfo: ServiceRef; - const cache: ServiceRef; - const rootConfig: ServiceRef; - const database: ServiceRef; - const discovery: ServiceRef; - const rootHealth: ServiceRef; - const httpAuth: ServiceRef; - const httpRouter: ServiceRef; - const lifecycle: ServiceRef; - const logger: ServiceRef; - const permissions: ServiceRef; - const pluginMetadata: ServiceRef; - const rootHttpRouter: ServiceRef; - const rootLifecycle: ServiceRef; - const rootLogger: ServiceRef; - const scheduler: ServiceRef; + const auth: ServiceRef; + const userInfo: ServiceRef; + const cache: ServiceRef; + const rootConfig: ServiceRef; + const database: ServiceRef; + const discovery: ServiceRef; + const rootHealth: ServiceRef; + const httpAuth: ServiceRef; + const httpRouter: ServiceRef; + const lifecycle: ServiceRef; + const logger: ServiceRef; + const permissions: ServiceRef; + const pluginMetadata: ServiceRef; + const rootHttpRouter: ServiceRef; + const rootLifecycle: ServiceRef; + const rootLogger: ServiceRef; + const scheduler: ServiceRef; const // @deprecated - tokenManager: ServiceRef; - const urlReader: ServiceRef; + tokenManager: ServiceRef; + const urlReader: ServiceRef; const // @deprecated - identity: ServiceRef; + identity: ServiceRef; } // @public @@ -251,18 +251,20 @@ export interface CreateExtensionPointOptions { // @public export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -271,12 +273,13 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -284,12 +287,19 @@ export function createServiceFactory< TContext = undefined, TOpts extends object | undefined = undefined, >( - options: PluginServiceFactoryOptions, -): ServiceFactoryCompat; + options: PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -299,18 +309,34 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => PluginServiceFactoryOptions, -): ServiceFactoryCompat; + ) => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; // @public export interface DatabaseService { @@ -447,16 +473,18 @@ export interface PluginMetadataService { // @public @deprecated (undocumented) export type PluginServiceFactoryConfig< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; // @public (undocumented) export interface PluginServiceFactoryOptions< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { @@ -476,7 +504,7 @@ export interface PluginServiceFactoryOptions< ): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -538,15 +566,17 @@ export interface RootLoggerService extends LoggerService {} // @public @deprecated (undocumented) export type RootServiceFactoryConfig< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; // @public (undocumented) export interface RootServiceFactoryOptions< - TService, + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -558,7 +588,7 @@ export interface RootServiceFactoryOptions< factory(deps: ServiceRefsToInstances): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -639,21 +669,23 @@ export type SearchResponseFile = UrlReaderServiceSearchResponseFile; export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, > extends BackendFeature { // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @deprecated (undocumented) export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { // @deprecated (undocumented) ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } // @public @deprecated @@ -663,9 +695,11 @@ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TSingleton extends boolean = boolean, > = { id: string; scope: TScope; + singleton: TSingleton; T: TService; $$type: '@backstage/ServiceRef'; }; @@ -674,10 +708,15 @@ export type ServiceRef< export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', -> = ServiceRefOptions; + TSingleton extends boolean, +> = ServiceRefOptions; // @public (undocumented) -export interface ServiceRefOptions { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean, +> { // (undocumented) defaultFactory?( service: ServiceRef, @@ -690,6 +729,8 @@ export interface ServiceRefOptions { id: string; // (undocumented) scope?: TScope; + // (undocumented) + singleton?: TSingleton; } // @public @deprecated diff --git a/packages/backend-plugin-api/src/deprecated.ts b/packages/backend-plugin-api/src/deprecated.ts index 17ccccf320..e68affa0fa 100644 --- a/packages/backend-plugin-api/src/deprecated.ts +++ b/packages/backend-plugin-api/src/deprecated.ts @@ -28,7 +28,8 @@ import { export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', -> = ServiceRefOptions; + TSingleton extends boolean, +> = ServiceRefOptions; /** * @public @@ -36,9 +37,10 @@ export type ServiceRefConfig< */ export type RootServiceFactoryConfig< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; /** * @public @@ -46,7 +48,8 @@ export type RootServiceFactoryConfig< */ export type PluginServiceFactoryConfig< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index d31350806d..e856136ddc 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -24,6 +24,7 @@ import { BackendFeature } from '../../types'; export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TSingleton extends boolean = boolean, > = { id: string; @@ -38,6 +39,8 @@ export type ServiceRef< */ scope: TScope; + singleton: TSingleton; + /** * Utility for getting the type of the service, using `typeof serviceRef.T`. * Attempting to actually read this value will result in an exception. @@ -51,8 +54,9 @@ export type ServiceRef< export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, > extends BackendFeature { - service: ServiceRef; + service: ServiceRef; } /** @@ -62,21 +66,23 @@ export interface ServiceFactory< export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { /** * @deprecated Callable service factories will be removed in a future release, please re-implement the service factory using the available APIs instead. If no options are being passed, you can simply remove the trailing `()`. */ ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } /** @internal */ export interface InternalServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', -> extends ServiceFactory { + TSingleton extends boolean = boolean, +> extends ServiceFactory { version: 'v1'; initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; @@ -96,9 +102,14 @@ export interface InternalServiceFactory< export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); /** @public */ -export interface ServiceRefOptions { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean, +> { id: string; scope?: TScope; + singleton?: TSingleton; defaultFactory?( service: ServiceRef, ): Promise; @@ -116,8 +127,8 @@ export interface ServiceRefOptions { * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; /** * Creates a new service definition. This overload is used to create root scoped services. @@ -125,16 +136,34 @@ export function createServiceRef( * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; +/** + * Creates a new service definition. This overload is used to create plugin scoped services. + * + * @public + */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef { - const { id, scope = 'plugin', defaultFactory } = options; + options: ServiceRefOptions, +): ServiceRef; + +/** + * Creates a new service definition. This overload is used to create root scoped services. + * + * @public + */ +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef { + const { id, scope = 'plugin', singleton = true, defaultFactory } = options; return { id, scope, + singleton, get T(): TService { throw new Error(`tried to read ServiceRef.T of ${this}`); }, @@ -143,7 +172,7 @@ export function createServiceRef( }, $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, - } as ServiceRef & { + } as ServiceRef & { __defaultFactory?: ( service: ServiceRef, ) => Promise | (() => ServiceFactory)>; @@ -155,12 +184,17 @@ type ServiceRefsToInstances< T extends { [key in string]: ServiceRef }, TScope extends 'root' | 'plugin' = 'root' | 'plugin', > = { - [key in keyof T as T[key]['scope'] extends TScope ? key : never]: T[key]['T']; + [key in keyof T as T[key]['scope'] extends TScope + ? key + : never]: T[key]['singleton'] extends true + ? T[key]['T'] + : Array; }; /** @public */ export interface RootServiceFactoryOptions< - TService, + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { @@ -175,7 +209,7 @@ export interface RootServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; factory(deps: ServiceRefsToInstances): TImpl | Promise; } @@ -183,6 +217,7 @@ export interface RootServiceFactoryOptions< /** @public */ export interface PluginServiceFactoryOptions< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, @@ -198,7 +233,7 @@ export interface PluginServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; createRootContext?( deps: ServiceRefsToInstances, @@ -217,12 +252,13 @@ export interface PluginServiceFactoryOptions< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a root scoped service factory with optional options. * @@ -235,14 +271,15 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory without options. * @@ -251,13 +288,20 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, TOpts extends object | undefined = undefined, >( - options: PluginServiceFactoryOptions, -): ServiceFactoryCompat; + options: PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory with optional options. * @@ -270,6 +314,7 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, @@ -277,25 +322,46 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => PluginServiceFactoryOptions, -): ServiceFactoryCompat; + ) => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext, TOpts extends object | undefined = undefined, >( options: - | RootServiceFactoryOptions - | PluginServiceFactoryOptions - | ((options: TOpts) => RootServiceFactoryOptions) + | RootServiceFactoryOptions + | PluginServiceFactoryOptions | (( options: TOpts, - ) => PluginServiceFactoryOptions) - | (() => RootServiceFactoryOptions) - | (() => PluginServiceFactoryOptions), -): ServiceFactoryCompat { + ) => RootServiceFactoryOptions) + | (( + options: TOpts, + ) => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >) + | (() => RootServiceFactoryOptions) + | (() => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >), +): ServiceFactoryCompat { const configCallback = typeof options === 'function' ? options : () => options; const factory = ( @@ -303,18 +369,25 @@ export function createServiceFactory< ): InternalServiceFactory => { const anyConf = configCallback(o!); if (anyConf.service.scope === 'root') { - const c = anyConf as RootServiceFactoryOptions; + const c = anyConf as RootServiceFactoryOptions< + TService, + TSingleton, + TImpl, + TDeps + >; return { $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, initialization: c.initialization, deps: c.deps, - factory: async (deps: TDeps) => c.factory(deps), + factory: async (deps: ServiceRefsToInstances) => + c.factory(deps), }; } const c = anyConf as PluginServiceFactoryOptions< TService, + TSingleton, TContext, TImpl, TDeps @@ -326,12 +399,14 @@ export function createServiceFactory< initialization: c.initialization, ...('createRootContext' in c ? { - createRootContext: async (deps: TDeps) => - c?.createRootContext?.(deps), + createRootContext: async ( + deps: ServiceRefsToInstances, + ) => c?.createRootContext?.(deps), } : {}), deps: c.deps, - factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx), + factory: async (deps: ServiceRefsToInstances, ctx: TContext) => + c.factory(deps, ctx), }; }; diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 1f93573564..5a9e016dcd 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -156,7 +156,7 @@ export namespace mockServices { // (undocumented) export namespace auth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -165,7 +165,7 @@ export namespace mockServices { // (undocumented) export namespace cache { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -174,7 +174,7 @@ export namespace mockServices { // (undocumented) export namespace database { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -185,7 +185,12 @@ export namespace mockServices { // (undocumented) export namespace discovery { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + DiscoveryService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -194,7 +199,7 @@ export namespace mockServices { // (undocumented) export namespace events { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -208,8 +213,8 @@ export namespace mockServices { export namespace httpAuth { const factory: ((options?: { defaultCredentials?: BackstageCredentials; - }) => ServiceFactory) & - ServiceFactory; + }) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -218,7 +223,12 @@ export namespace mockServices { // (undocumented) export namespace httpRouter { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + HttpRouterService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -229,7 +239,7 @@ export namespace mockServices { // (undocumented) export namespace identity { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -238,7 +248,12 @@ export namespace mockServices { // (undocumented) export namespace lifecycle { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + LifecycleService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -247,7 +262,7 @@ export namespace mockServices { // (undocumented) export namespace logger { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -256,7 +271,12 @@ export namespace mockServices { // (undocumented) export namespace permissions { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + PermissionsService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -271,15 +291,15 @@ export namespace mockServices { data?: JsonObject; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory); } // (undocumented) export namespace rootHealth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -290,8 +310,8 @@ export namespace mockServices { const // (undocumented) factory: (( options?: RootHttpRouterFactoryOptions | undefined, - ) => ServiceFactory) & - ServiceFactory; + ) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -300,7 +320,12 @@ export namespace mockServices { // (undocumented) export namespace rootLifecycle { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + RootLifecycleService, + 'root', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -315,10 +340,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory); const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -327,7 +352,12 @@ export namespace mockServices { // (undocumented) export namespace scheduler { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + SchedulerService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -338,7 +368,12 @@ export namespace mockServices { // (undocumented) export namespace tokenManager { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + TokenManagerService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -347,7 +382,12 @@ export namespace mockServices { // (undocumented) export namespace urlReader { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + UrlReaderService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -358,7 +398,12 @@ export namespace mockServices { ): UserInfoService; // (undocumented) export namespace userInfo { - const factory: ServiceFactoryCompat; + const factory: ServiceFactoryCompat< + UserInfoService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -374,22 +419,34 @@ export function registerMswTestHooks(worker: { }): void; // @public -export class ServiceFactoryTester { - static from( - subject: ServiceFactory, +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, +> { + static from< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ): ServiceFactoryTester; + ): ServiceFactoryTester; // @deprecated get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; - getService( - service: ServiceRef, + ): Promise; + getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetSingleton extends boolean, + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; } // @public diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index aea0d9d270..94092aff68 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -43,8 +43,12 @@ export interface ServiceFactoryTesterOptions { * * @public */ -export class ServiceFactoryTester { - readonly #subject: ServiceRef; +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, +> { + readonly #subject: ServiceRef; readonly #registry: ServiceRegistry; /** @@ -54,10 +58,14 @@ export class ServiceFactoryTester { * @param options - Additional options * @returns A new tester instance for the provided subject. */ - static from( - subject: ServiceFactory, + static from< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ) { + ): ServiceFactoryTester { const registry = ServiceRegistry.create([ ...defaultServiceFactories, ...(options?.dependencies ?? []), @@ -67,7 +75,7 @@ export class ServiceFactoryTester { } private constructor( - subject: ServiceRef, + subject: ServiceRef, registry: ServiceRegistry, ) { this.#subject = subject; @@ -81,7 +89,7 @@ export class ServiceFactoryTester { */ async get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { return this.getSubject(...args); } @@ -97,9 +105,10 @@ export class ServiceFactoryTester { */ async getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; - return this.#registry.get(this.#subject, pluginId ?? 'test')!; + const instance = this.#registry.get(this.#subject, pluginId ?? 'test')!; + return instance; } /** @@ -109,10 +118,14 @@ export class ServiceFactoryTester { * * A plugin ID can optionally be provided for plugin scoped services, otherwise the plugin ID 'test' is used. */ - async getService( - service: ServiceRef, + async getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetSingleton extends boolean, + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; const instance = await this.#registry.get(service, pluginId ?? 'test'); if (instance === undefined) { diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index b61d1b1d27..a2579e29f4 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -88,7 +88,7 @@ export interface CatalogProcessingExtensionPoint { export const catalogProcessingExtensionPoint: ExtensionPoint; // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 5b45b4419e..e159446103 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -66,11 +66,12 @@ export type EventsServiceEventHandler = (params: EventParams) => Promise; export const eventsServiceFactory: ServiceFactoryCompat< EventsService, 'plugin', + true, undefined >; // @public -export const eventsServiceRef: ServiceRef; +export const eventsServiceRef: ServiceRef; // @public (undocumented) export type EventsServiceSubscribeOptions = { diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 37f28fec35..fd356131ee 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -65,7 +65,11 @@ export interface NotificationService { } // @public (undocumented) -export const notificationService: ServiceRef; +export const notificationService: ServiceRef< + NotificationService, + 'plugin', + true +>; // @public (undocumented) export type NotificationServiceOptions = { diff --git a/plugins/search-backend-node/api-report-alpha.md b/plugins/search-backend-node/api-report-alpha.md index 4f3015a0e4..dc41fa6400 100644 --- a/plugins/search-backend-node/api-report-alpha.md +++ b/plugins/search-backend-node/api-report-alpha.md @@ -46,7 +46,11 @@ export type SearchIndexServiceInitOptions = { }; // @alpha -export const searchIndexServiceRef: ServiceRef; +export const searchIndexServiceRef: ServiceRef< + SearchIndexService, + 'plugin', + true +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/signals-node/api-report.md b/plugins/signals-node/api-report.md index cf4ab84aff..b2d5a2b210 100644 --- a/plugins/signals-node/api-report.md +++ b/plugins/signals-node/api-report.md @@ -37,7 +37,7 @@ export type SignalPayload = { export interface SignalService extends SignalsService {} // @public @deprecated (undocumented) -export const signalService: ServiceRef; +export const signalService: ServiceRef; // @public (undocumented) export interface SignalsService { @@ -52,7 +52,7 @@ export type SignalsServiceOptions = { }; // @public (undocumented) -export const signalsServiceRef: ServiceRef; +export const signalsServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ``` From 7c5f3b02972b1ce53e39f0dd04f6fce4e5511c43 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jul 2024 11:22:31 +0200 Subject: [PATCH 038/204] refactor: use string types for service instance occurrences Signed-off-by: Camila Belo --- .changeset/mighty-apricots-taste.md | 5 + .changeset/purple-carrots-crash.md | 5 + .changeset/shy-waves-share.md | 5 + .changeset/small-bottles-cough.md | 58 ++++++++ .changeset/thirty-adults-grab.md | 6 + packages/backend-app-api/api-report-alpha.md | 2 +- packages/backend-app-api/api-report.md | 38 +++--- .../src/wiring/ServiceRegistry.ts | 38 +++--- packages/backend-defaults/api-report-auth.md | 2 +- packages/backend-defaults/api-report-cache.md | 2 +- .../backend-defaults/api-report-database.md | 2 +- .../backend-defaults/api-report-discovery.md | 2 +- .../backend-defaults/api-report-httpAuth.md | 2 +- .../backend-defaults/api-report-httpRouter.md | 2 +- .../backend-defaults/api-report-lifecycle.md | 2 +- .../backend-defaults/api-report-logger.md | 2 +- .../api-report-permissions.md | 2 +- .../backend-defaults/api-report-rootConfig.md | 4 +- .../backend-defaults/api-report-rootHealth.md | 2 +- .../api-report-rootHttpRouter.md | 4 +- .../api-report-rootLifecycle.md | 2 +- .../backend-defaults/api-report-rootLogger.md | 2 +- .../backend-defaults/api-report-scheduler.md | 2 +- .../backend-defaults/api-report-urlReader.md | 2 +- .../backend-defaults/api-report-userInfo.md | 2 +- .../urlReader/urlReaderServiceFactory.ts | 2 +- .../api-report.md | 10 +- .../backend-plugin-api/api-report-alpha.md | 2 +- packages/backend-plugin-api/api-report.md | 124 +++++++++--------- packages/backend-plugin-api/src/deprecated.ts | 12 +- .../src/services/system/types.ts | 116 ++++++++-------- packages/backend-test-utils/api-report.md | 109 ++++++++++----- .../src/next/wiring/ServiceFactoryTester.ts | 22 ++-- plugins/catalog-node/api-report-alpha.md | 2 +- plugins/events-node/api-report.md | 4 +- plugins/notifications-node/api-report.md | 2 +- .../search-backend-node/api-report-alpha.md | 2 +- plugins/signals-node/api-report.md | 8 +- 38 files changed, 374 insertions(+), 236 deletions(-) create mode 100644 .changeset/mighty-apricots-taste.md create mode 100644 .changeset/purple-carrots-crash.md create mode 100644 .changeset/shy-waves-share.md create mode 100644 .changeset/small-bottles-cough.md create mode 100644 .changeset/thirty-adults-grab.md diff --git a/.changeset/mighty-apricots-taste.md b/.changeset/mighty-apricots-taste.md new file mode 100644 index 0000000000..80b70020cb --- /dev/null +++ b/.changeset/mighty-apricots-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. diff --git a/.changeset/purple-carrots-crash.md b/.changeset/purple-carrots-crash.md new file mode 100644 index 0000000000..62f90677b8 --- /dev/null +++ b/.changeset/purple-carrots-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. diff --git a/.changeset/shy-waves-share.md b/.changeset/shy-waves-share.md new file mode 100644 index 0000000000..861590fd01 --- /dev/null +++ b/.changeset/shy-waves-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. diff --git a/.changeset/small-bottles-cough.md b/.changeset/small-bottles-cough.md new file mode 100644 index 0000000000..40098a84c7 --- /dev/null +++ b/.changeset/small-bottles-cough.md @@ -0,0 +1,58 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + +We're looking for ways to make it possible to augment services without the need to replace the entire service. + +Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + +```diff +// @backstage/backend-defaults + ++ export const urlReaderFactoriesServiceRef = createServiceRef({ ++ id: 'core.urlReader.factories', ++ scope: 'plugin', ++ multiton: true, ++ }); + +... + +export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, ++ factories: urlReaderFactoriesServiceRef, + }, +- async factory({ config, logger }) { ++ async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, ++ factories, + }); + }, +}); +``` + +With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + +```ts +// packages/backend/index.ts +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; +... + +backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, +})); + +... + +``` diff --git a/.changeset/thirty-adults-grab.md b/.changeset/thirty-adults-grab.md new file mode 100644 index 0000000000..391e7a39bb --- /dev/null +++ b/.changeset/thirty-adults-grab.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-catalog-node': patch +--- + +Explicit declare if the service ref accepts `single` ou `multiple` implementations. diff --git a/packages/backend-app-api/api-report-alpha.md b/packages/backend-app-api/api-report-alpha.md index b811208845..8616eec29a 100644 --- a/packages/backend-app-api/api-report-alpha.md +++ b/packages/backend-app-api/api-report-alpha.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const featureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', - true, + 'singleton', undefined >; diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 4019ad3086..dea309ebb0 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -49,7 +49,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', - true, + 'singleton', undefined >; @@ -81,7 +81,7 @@ export interface Backend { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', - true, + 'singleton', undefined >; @@ -115,7 +115,7 @@ export interface CreateSpecializedBackendOptions { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', - true, + 'singleton', undefined >; @@ -138,7 +138,7 @@ export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', - true, + 'singleton', undefined >; @@ -165,7 +165,7 @@ export class HostDiscovery implements DiscoveryService { export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', - true, + 'singleton', undefined >; @@ -173,7 +173,7 @@ export const httpAuthServiceFactory: ServiceFactoryCompat< export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', - true, + 'singleton', undefined >; @@ -197,7 +197,7 @@ export type IdentityFactoryOptions = { export const identityServiceFactory: ServiceFactoryCompat< IdentityService, 'plugin', - true, + 'singleton', IdentityFactoryOptions >; @@ -210,7 +210,7 @@ export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', - true, + 'singleton', undefined >; @@ -228,7 +228,7 @@ export function loadBackendConfig(options: { export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', - true, + 'singleton', undefined >; @@ -257,7 +257,7 @@ export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', - true, + 'singleton', undefined >; @@ -288,7 +288,7 @@ export interface RootConfigFactoryOptions { export const rootConfigServiceFactory: ServiceFactoryCompat< RootConfigService, 'root', - true, + 'singleton', RootConfigFactoryOptions >; @@ -305,14 +305,14 @@ export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; // @public @deprecated (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // @public @deprecated export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', - true, + 'singleton', undefined >; @@ -320,7 +320,7 @@ export const rootLifecycleServiceFactory: ServiceFactoryCompat< export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', - true, + 'singleton', undefined >; @@ -328,7 +328,7 @@ export const rootLoggerServiceFactory: ServiceFactoryCompat< export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', - true, + 'singleton', undefined >; @@ -336,7 +336,7 @@ export const schedulerServiceFactory: ServiceFactoryCompat< export const tokenManagerServiceFactory: ServiceFactoryCompat< TokenManagerService, 'plugin', - true, + 'singleton', undefined >; @@ -344,7 +344,7 @@ export const tokenManagerServiceFactory: ServiceFactoryCompat< export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', - true, + 'singleton', undefined >; @@ -352,7 +352,7 @@ export const urlReaderServiceFactory: ServiceFactoryCompat< export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index e1644dad89..0abd5605ae 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -60,14 +60,14 @@ export class ServiceRegistry { static create(factories: Array): ServiceRegistry { const factoryMap = new Map(); for (const factory of factories) { - if (factory.service.singleton) { - factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]); - } else { + if (factory.service.multiton) { const existing = factoryMap.get(factory.service.id) ?? []; factoryMap.set( factory.service.id, existing.concat(toInternalServiceFactory(factory)), ); + } else { + factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]); } } const registry = new ServiceRegistry(factoryMap); @@ -155,7 +155,7 @@ export class ServiceRegistry { if (this.#providedFactories.get(ref.id)) { return false; } - if (!ref.singleton) { + if (ref.multiton) { return false; } @@ -205,7 +205,12 @@ export class ServiceRegistry { ); } - if (factory.service.singleton) { + if (factory.service.multiton) { + const newFactories = ( + this.#providedFactories.get(factoryId) ?? [] + ).concat(toInternalServiceFactory(factory)); + this.#providedFactories.set(factoryId, newFactories); + } else { if (this.#addedFactoryIds.has(factoryId)) { throw new Error( `Duplicate service implementations provided for ${factoryId}`, @@ -216,11 +221,6 @@ export class ServiceRegistry { this.#providedFactories.set(factoryId, [ toInternalServiceFactory(factory), ]); - } else { - const newFactories = ( - this.#providedFactories.get(factoryId) ?? [] - ).concat(toInternalServiceFactory(factory)); - this.#providedFactories.set(factoryId, newFactories); } } @@ -240,20 +240,20 @@ export class ServiceRegistry { } } - get( - ref: ServiceRef, + get( + ref: ServiceRef, pluginId: string, - ): Promise | undefined { + ): Promise | undefined { this.#instantiatedFactories.add(ref.id); const resolvedFactory = this.#resolveFactory(ref, pluginId); if (!resolvedFactory) { - return ref.singleton - ? undefined - : (Promise.resolve([]) as - | Promise - | undefined); + return ref.multiton + ? (Promise.resolve([]) as + | Promise + | undefined) + : undefined; } return resolvedFactory @@ -346,6 +346,6 @@ export class ServiceRegistry { }), ); }) - .then(results => (ref.singleton ? results[0] : results)); + .then(results => (ref.multiton ? results : results[0])); } } diff --git a/packages/backend-defaults/api-report-auth.md b/packages/backend-defaults/api-report-auth.md index 85287c524e..5ceca7f72e 100644 --- a/packages/backend-defaults/api-report-auth.md +++ b/packages/backend-defaults/api-report-auth.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-cache.md b/packages/backend-defaults/api-report-cache.md index c6debd6a2b..e8b6f075ab 100644 --- a/packages/backend-defaults/api-report-cache.md +++ b/packages/backend-defaults/api-report-cache.md @@ -28,7 +28,7 @@ export type CacheManagerOptions = { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-database.md b/packages/backend-defaults/api-report-database.md index d7585cc791..8d204db025 100644 --- a/packages/backend-defaults/api-report-database.md +++ b/packages/backend-defaults/api-report-database.md @@ -35,7 +35,7 @@ export type DatabaseManagerOptions = { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-discovery.md b/packages/backend-defaults/api-report-discovery.md index dc0d9b2bbe..1b19a42aa5 100644 --- a/packages/backend-defaults/api-report-discovery.md +++ b/packages/backend-defaults/api-report-discovery.md @@ -11,7 +11,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-httpAuth.md b/packages/backend-defaults/api-report-httpAuth.md index d8d283c01e..a65a6e157d 100644 --- a/packages/backend-defaults/api-report-httpAuth.md +++ b/packages/backend-defaults/api-report-httpAuth.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md index 7673367f18..300e75a44d 100644 --- a/packages/backend-defaults/api-report-httpRouter.md +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -18,7 +18,7 @@ export function createLifecycleMiddleware( export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-lifecycle.md b/packages/backend-defaults/api-report-lifecycle.md index ad41fd4fcb..54ffb77fed 100644 --- a/packages/backend-defaults/api-report-lifecycle.md +++ b/packages/backend-defaults/api-report-lifecycle.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-logger.md b/packages/backend-defaults/api-report-logger.md index c881d20344..40ca4e21b4 100644 --- a/packages/backend-defaults/api-report-logger.md +++ b/packages/backend-defaults/api-report-logger.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-permissions.md b/packages/backend-defaults/api-report-permissions.md index 92e3feaeaf..eea11ed7c9 100644 --- a/packages/backend-defaults/api-report-permissions.md +++ b/packages/backend-defaults/api-report-permissions.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 60fed5734b..2935ad1128 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -28,8 +28,8 @@ export interface RootConfigFactoryOptions { // @public (undocumented) export const rootConfigServiceFactory: (( options?: RootConfigFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootHealth.md b/packages/backend-defaults/api-report-rootHealth.md index ca104ecf37..c7580cacd6 100644 --- a/packages/backend-defaults/api-report-rootHealth.md +++ b/packages/backend-defaults/api-report-rootHealth.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootHealthServiceFactory: ServiceFactoryCompat< RootHealthService, 'root', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index a6e5a31428..94c22960ef 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -143,8 +143,8 @@ export type RootHttpRouterFactoryOptions = { // @public (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootLifecycle.md b/packages/backend-defaults/api-report-rootLifecycle.md index 52eaa3fe0e..e522471d04 100644 --- a/packages/backend-defaults/api-report-rootLifecycle.md +++ b/packages/backend-defaults/api-report-rootLifecycle.md @@ -10,7 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-rootLogger.md b/packages/backend-defaults/api-report-rootLogger.md index d91aaf0ae8..3878420192 100644 --- a/packages/backend-defaults/api-report-rootLogger.md +++ b/packages/backend-defaults/api-report-rootLogger.md @@ -14,7 +14,7 @@ import { transport } from 'winston'; export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md index 7d8706def2..9953518e09 100644 --- a/packages/backend-defaults/api-report-scheduler.md +++ b/packages/backend-defaults/api-report-scheduler.md @@ -21,7 +21,7 @@ export class DefaultSchedulerService { export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md index 431e2af701..ad732101dd 100644 --- a/packages/backend-defaults/api-report-urlReader.md +++ b/packages/backend-defaults/api-report-urlReader.md @@ -429,7 +429,7 @@ export class UrlReaders { export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-userInfo.md b/packages/backend-defaults/api-report-userInfo.md index 31ae1dd8bb..27f73d06e1 100644 --- a/packages/backend-defaults/api-report-userInfo.md +++ b/packages/backend-defaults/api-report-userInfo.md @@ -10,7 +10,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', - true, + 'singleton', undefined >; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts index 9859166ba0..b5c22402ff 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts @@ -42,7 +42,7 @@ export const urlReaderProviderFactoriesServiceRef = createServiceRef({ id: 'core.urlReader.factories', scope: 'plugin', - singleton: false, + multiton: true, }); /** diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index b74bbcdc95..97dea60170 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -117,7 +117,7 @@ export interface DynamicPluginsFactoryOptions { export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', - true, + 'singleton', undefined >; @@ -128,7 +128,7 @@ export const dynamicPluginsFrontendSchemas: BackendFeatureCompat; export const dynamicPluginsRootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', - true, + 'singleton', undefined >; @@ -149,7 +149,7 @@ export interface DynamicPluginsSchemasService { export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< DynamicPluginsSchemasService, 'root', - true, + 'singleton', DynamicPluginsSchemasOptions >; @@ -157,7 +157,7 @@ export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< export const dynamicPluginsServiceFactory: ServiceFactoryCompat< DynamicPluginProvider, 'root', - true, + 'singleton', DynamicPluginsFactoryOptions >; @@ -165,7 +165,7 @@ export const dynamicPluginsServiceFactory: ServiceFactoryCompat< export const dynamicPluginsServiceRef: ServiceRef< DynamicPluginProvider, 'root', - true + 'singleton' >; // @public (undocumented) diff --git a/packages/backend-plugin-api/api-report-alpha.md b/packages/backend-plugin-api/api-report-alpha.md index 9a82d3adbe..0f4348336a 100644 --- a/packages/backend-plugin-api/api-report-alpha.md +++ b/packages/backend-plugin-api/api-report-alpha.md @@ -18,7 +18,7 @@ export interface FeatureDiscoveryService { export const featureDiscoveryServiceRef: ServiceRef< FeatureDiscoveryService, 'root', - true + 'singleton' >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 3925d3b2f4..99c7b1b9fc 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -189,28 +189,32 @@ export type CacheServiceSetOptions = { // @public export namespace coreServices { - const auth: ServiceRef; - const userInfo: ServiceRef; - const cache: ServiceRef; - const rootConfig: ServiceRef; - const database: ServiceRef; - const discovery: ServiceRef; - const rootHealth: ServiceRef; - const httpAuth: ServiceRef; - const httpRouter: ServiceRef; - const lifecycle: ServiceRef; - const logger: ServiceRef; - const permissions: ServiceRef; - const pluginMetadata: ServiceRef; - const rootHttpRouter: ServiceRef; - const rootLifecycle: ServiceRef; - const rootLogger: ServiceRef; - const scheduler: ServiceRef; + const auth: ServiceRef; + const userInfo: ServiceRef; + const cache: ServiceRef; + const rootConfig: ServiceRef; + const database: ServiceRef; + const discovery: ServiceRef; + const rootHealth: ServiceRef; + const httpAuth: ServiceRef; + const httpRouter: ServiceRef; + const lifecycle: ServiceRef; + const logger: ServiceRef; + const permissions: ServiceRef; + const pluginMetadata: ServiceRef< + PluginMetadataService, + 'plugin', + 'singleton' + >; + const rootHttpRouter: ServiceRef; + const rootLifecycle: ServiceRef; + const rootLogger: ServiceRef; + const scheduler: ServiceRef; const // @deprecated - tokenManager: ServiceRef; - const urlReader: ServiceRef; + tokenManager: ServiceRef; + const urlReader: ServiceRef; const // @deprecated - identity: ServiceRef; + identity: ServiceRef; } // @public @@ -251,20 +255,20 @@ export interface CreateExtensionPointOptions { // @public export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -273,13 +277,13 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -289,17 +293,17 @@ export function createServiceFactory< >( options: PluginServiceFactoryOptions< TService, - TSingleton, + TInstances, TContext, TImpl, TDeps >, -): ServiceFactoryCompat; +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -311,32 +315,32 @@ export function createServiceFactory< options?: TOpts, ) => PluginServiceFactoryOptions< TService, - TSingleton, + TInstances, TContext, TImpl, TDeps >, -): ServiceFactoryCompat; +): ServiceFactoryCompat; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export interface DatabaseService { @@ -473,18 +477,18 @@ export interface PluginMetadataService { // @public @deprecated (undocumented) export type PluginServiceFactoryConfig< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; // @public (undocumented) export interface PluginServiceFactoryOptions< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { @@ -504,7 +508,7 @@ export interface PluginServiceFactoryOptions< ): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -566,17 +570,17 @@ export interface RootLoggerService extends LoggerService {} // @public @deprecated (undocumented) export type RootServiceFactoryConfig< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; // @public (undocumented) export interface RootServiceFactoryOptions< TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -588,7 +592,7 @@ export interface RootServiceFactoryOptions< factory(deps: ServiceRefsToInstances): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -669,23 +673,23 @@ export type SearchResponseFile = UrlReaderServiceSearchResponseFile; export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', - TSingleton extends boolean = boolean, + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > extends BackendFeature { // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @deprecated (undocumented) export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', - TSingleton extends boolean = boolean, + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { // @deprecated (undocumented) ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } // @public @deprecated @@ -695,11 +699,11 @@ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', - TSingleton extends boolean = boolean, + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > = { id: string; scope: TScope; - singleton: TSingleton; + multiton: TInstances extends 'multiton' ? true : false; T: TService; $$type: '@backstage/ServiceRef'; }; @@ -708,14 +712,14 @@ export type ServiceRef< export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean, -> = ServiceRefOptions; + TInstances extends 'singleton' | 'multiton', +> = ServiceRefOptions; // @public (undocumented) export interface ServiceRefOptions< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', > { // (undocumented) defaultFactory?( @@ -728,9 +732,9 @@ export interface ServiceRefOptions< // (undocumented) id: string; // (undocumented) - scope?: TScope; + multiton?: TInstances extends 'multiton' ? true : false; // (undocumented) - singleton?: TSingleton; + scope?: TScope; } // @public @deprecated diff --git a/packages/backend-plugin-api/src/deprecated.ts b/packages/backend-plugin-api/src/deprecated.ts index e68affa0fa..0d02f9ed7e 100644 --- a/packages/backend-plugin-api/src/deprecated.ts +++ b/packages/backend-plugin-api/src/deprecated.ts @@ -28,8 +28,8 @@ import { export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean, -> = ServiceRefOptions; + TInstances extends 'singleton' | 'multiton', +> = ServiceRefOptions; /** * @public @@ -37,10 +37,10 @@ export type ServiceRefConfig< */ export type RootServiceFactoryConfig< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; /** * @public @@ -48,8 +48,8 @@ export type RootServiceFactoryConfig< */ export type PluginServiceFactoryConfig< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index e856136ddc..7cd18da4d2 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -24,7 +24,7 @@ import { BackendFeature } from '../../types'; export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', - TSingleton extends boolean = boolean, + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > = { id: string; @@ -39,7 +39,7 @@ export type ServiceRef< */ scope: TScope; - singleton: TSingleton; + multiton: TInstances extends 'multiton' ? true : false; /** * Utility for getting the type of the service, using `typeof serviceRef.T`. @@ -54,9 +54,9 @@ export type ServiceRef< export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', - TSingleton extends boolean = boolean, + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > extends BackendFeature { - service: ServiceRef; + service: ServiceRef; } /** @@ -66,23 +66,23 @@ export interface ServiceFactory< export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', - TSingleton extends boolean = boolean, + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { /** * @deprecated Callable service factories will be removed in a future release, please re-implement the service factory using the available APIs instead. If no options are being passed, you can simply remove the trailing `()`. */ ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } /** @internal */ export interface InternalServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', - TSingleton extends boolean = boolean, -> extends ServiceFactory { + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', +> extends ServiceFactory { version: 'v1'; initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; @@ -105,11 +105,11 @@ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); export interface ServiceRefOptions< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', > { id: string; scope?: TScope; - singleton?: TSingleton; + multiton?: TInstances extends 'multiton' ? true : false; defaultFactory?( service: ServiceRef, ): Promise; @@ -127,8 +127,8 @@ export interface ServiceRefOptions< * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; /** * Creates a new service definition. This overload is used to create root scoped services. @@ -136,8 +136,8 @@ export function createServiceRef( * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; /** * Creates a new service definition. This overload is used to create plugin scoped services. @@ -145,8 +145,8 @@ export function createServiceRef( * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; /** * Creates a new service definition. This overload is used to create root scoped services. @@ -154,16 +154,19 @@ export function createServiceRef( * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; -export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef { - const { id, scope = 'plugin', singleton = true, defaultFactory } = options; + options: ServiceRefOptions, +): ServiceRef; +export function createServiceRef< + TService, + TInstances extends 'singleton' | 'multiton', +>( + options: ServiceRefOptions, +): ServiceRef { + const { id, scope = 'plugin', multiton = false, defaultFactory } = options; return { id, scope, - singleton, + multiton, get T(): TService { throw new Error(`tried to read ServiceRef.T of ${this}`); }, @@ -172,7 +175,7 @@ export function createServiceRef( }, $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, - } as ServiceRef & { + } as ServiceRef & { __defaultFactory?: ( service: ServiceRef, ) => Promise | (() => ServiceFactory)>; @@ -186,15 +189,15 @@ type ServiceRefsToInstances< > = { [key in keyof T as T[key]['scope'] extends TScope ? key - : never]: T[key]['singleton'] extends true - ? T[key]['T'] - : Array; + : never]: T[key]['multiton'] extends true + ? Array + : T[key]['T']; }; /** @public */ export interface RootServiceFactoryOptions< TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { @@ -209,7 +212,7 @@ export interface RootServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; factory(deps: ServiceRefsToInstances): TImpl | Promise; } @@ -217,7 +220,7 @@ export interface RootServiceFactoryOptions< /** @public */ export interface PluginServiceFactoryOptions< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, @@ -233,7 +236,7 @@ export interface PluginServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; createRootContext?( deps: ServiceRefsToInstances, @@ -252,13 +255,13 @@ export interface PluginServiceFactoryOptions< */ export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a root scoped service factory with optional options. * @@ -271,15 +274,15 @@ export function createServiceFactory< */ export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory without options. * @@ -288,7 +291,7 @@ export function createServiceFactory< */ export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, @@ -296,12 +299,12 @@ export function createServiceFactory< >( options: PluginServiceFactoryOptions< TService, - TSingleton, + TInstances, TContext, TImpl, TDeps >, -): ServiceFactoryCompat; +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory with optional options. * @@ -314,7 +317,7 @@ export function createServiceFactory< */ export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, @@ -324,44 +327,49 @@ export function createServiceFactory< options?: TOpts, ) => PluginServiceFactoryOptions< TService, - TSingleton, + TInstances, TContext, TImpl, TDeps >, -): ServiceFactoryCompat; +): ServiceFactoryCompat; export function createServiceFactory< TService, - TSingleton extends boolean, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext, TOpts extends object | undefined = undefined, >( options: - | RootServiceFactoryOptions - | PluginServiceFactoryOptions + | RootServiceFactoryOptions + | PluginServiceFactoryOptions | (( options: TOpts, - ) => RootServiceFactoryOptions) + ) => RootServiceFactoryOptions) | (( options: TOpts, ) => PluginServiceFactoryOptions< TService, - TSingleton, + TInstances, TContext, TImpl, TDeps >) - | (() => RootServiceFactoryOptions) + | (() => RootServiceFactoryOptions) | (() => PluginServiceFactoryOptions< TService, - TSingleton, + TInstances, TContext, TImpl, TDeps >), -): ServiceFactoryCompat { +): ServiceFactoryCompat< + TService, + 'root' | 'plugin', + 'singleton' | 'multiton', + TOpts +> { const configCallback = typeof options === 'function' ? options : () => options; const factory = ( @@ -371,7 +379,7 @@ export function createServiceFactory< if (anyConf.service.scope === 'root') { const c = anyConf as RootServiceFactoryOptions< TService, - TSingleton, + TInstances, TImpl, TDeps >; @@ -387,7 +395,7 @@ export function createServiceFactory< } const c = anyConf as PluginServiceFactoryOptions< TService, - TSingleton, + TInstances, TContext, TImpl, TDeps diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 5a9e016dcd..584fedb1f5 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -156,7 +156,12 @@ export namespace mockServices { // (undocumented) export namespace auth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + AuthService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -165,7 +170,12 @@ export namespace mockServices { // (undocumented) export namespace cache { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + CacheService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -174,7 +184,12 @@ export namespace mockServices { // (undocumented) export namespace database { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + DatabaseService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -188,7 +203,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< DiscoveryService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -199,7 +214,12 @@ export namespace mockServices { // (undocumented) export namespace events { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + EventsService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -213,8 +233,8 @@ export namespace mockServices { export namespace httpAuth { const factory: ((options?: { defaultCredentials?: BackstageCredentials; - }) => ServiceFactory) & - ServiceFactory; + }) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -226,7 +246,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< HttpRouterService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -239,7 +259,12 @@ export namespace mockServices { // (undocumented) export namespace identity { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + IdentityService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -251,7 +276,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< LifecycleService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -262,7 +287,12 @@ export namespace mockServices { // (undocumented) export namespace logger { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + LoggerService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -274,7 +304,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< PermissionsService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -291,15 +321,28 @@ export namespace mockServices { data?: JsonObject; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory< + RootConfigService, + 'root', + 'singleton' | 'multiton' + > & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory< + RootConfigService, + 'root', + 'singleton' | 'multiton' + >); } // (undocumented) export namespace rootHealth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + RootHealthService, + 'root', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -310,8 +353,8 @@ export namespace mockServices { const // (undocumented) factory: (( options?: RootHttpRouterFactoryOptions | undefined, - ) => ServiceFactory) & - ServiceFactory; + ) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -323,7 +366,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< RootLifecycleService, 'root', - true, + 'singleton', undefined >; const // (undocumented) @@ -340,10 +383,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory); const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -355,7 +398,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< SchedulerService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -371,7 +414,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< TokenManagerService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -385,7 +428,7 @@ export namespace mockServices { factory: ServiceFactoryCompat< UrlReaderService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -401,7 +444,7 @@ export namespace mockServices { const factory: ServiceFactoryCompat< UserInfoService, 'plugin', - true, + 'singleton', undefined >; const // (undocumented) @@ -422,31 +465,31 @@ export function registerMswTestHooks(worker: { export class ServiceFactoryTester< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean = true, + TInstances extends 'singleton' | 'multiton' = 'singleton', > { static from< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean = true, + TInstances extends 'singleton' | 'multiton' = 'singleton', >( - subject: ServiceFactory, + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ): ServiceFactoryTester; + ): ServiceFactoryTester; // @deprecated get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; getService< TGetService, TGetScope extends 'root' | 'plugin', - TGetSingleton extends boolean, + TGetInstances extends 'singleton' | 'multiton' = 'singleton', >( - service: ServiceRef, + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; } // @public diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index 94092aff68..e8530f89bf 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -46,9 +46,9 @@ export interface ServiceFactoryTesterOptions { export class ServiceFactoryTester< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean = true, + TInstances extends 'singleton' | 'multiton' = 'singleton', > { - readonly #subject: ServiceRef; + readonly #subject: ServiceRef; readonly #registry: ServiceRegistry; /** @@ -61,11 +61,11 @@ export class ServiceFactoryTester< static from< TService, TScope extends 'root' | 'plugin', - TSingleton extends boolean = true, + TInstances extends 'singleton' | 'multiton' = 'singleton', >( - subject: ServiceFactory, + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ): ServiceFactoryTester { + ): ServiceFactoryTester { const registry = ServiceRegistry.create([ ...defaultServiceFactories, ...(options?.dependencies ?? []), @@ -75,7 +75,7 @@ export class ServiceFactoryTester< } private constructor( - subject: ServiceRef, + subject: ServiceRef, registry: ServiceRegistry, ) { this.#subject = subject; @@ -89,7 +89,7 @@ export class ServiceFactoryTester< */ async get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { return this.getSubject(...args); } @@ -105,7 +105,7 @@ export class ServiceFactoryTester< */ async getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; const instance = this.#registry.get(this.#subject, pluginId ?? 'test')!; return instance; @@ -121,11 +121,11 @@ export class ServiceFactoryTester< async getService< TGetService, TGetScope extends 'root' | 'plugin', - TGetSingleton extends boolean, + TGetInstances extends 'singleton' | 'multiton' = 'singleton', >( - service: ServiceRef, + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; const instance = await this.#registry.get(service, pluginId ?? 'test'); if (instance === undefined) { diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index a2579e29f4..daa388d5e3 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -88,7 +88,7 @@ export interface CatalogProcessingExtensionPoint { export const catalogProcessingExtensionPoint: ExtensionPoint; // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index e159446103..729f526dee 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -66,12 +66,12 @@ export type EventsServiceEventHandler = (params: EventParams) => Promise; export const eventsServiceFactory: ServiceFactoryCompat< EventsService, 'plugin', - true, + 'singleton', undefined >; // @public -export const eventsServiceRef: ServiceRef; +export const eventsServiceRef: ServiceRef; // @public (undocumented) export type EventsServiceSubscribeOptions = { diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index fd356131ee..995b98bc21 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -68,7 +68,7 @@ export interface NotificationService { export const notificationService: ServiceRef< NotificationService, 'plugin', - true + 'singleton' >; // @public (undocumented) diff --git a/plugins/search-backend-node/api-report-alpha.md b/plugins/search-backend-node/api-report-alpha.md index dc41fa6400..e1d5bd024d 100644 --- a/plugins/search-backend-node/api-report-alpha.md +++ b/plugins/search-backend-node/api-report-alpha.md @@ -49,7 +49,7 @@ export type SearchIndexServiceInitOptions = { export const searchIndexServiceRef: ServiceRef< SearchIndexService, 'plugin', - true + 'singleton' >; // (No @packageDocumentation comment for this package) diff --git a/plugins/signals-node/api-report.md b/plugins/signals-node/api-report.md index b2d5a2b210..a368dd2ece 100644 --- a/plugins/signals-node/api-report.md +++ b/plugins/signals-node/api-report.md @@ -37,7 +37,7 @@ export type SignalPayload = { export interface SignalService extends SignalsService {} // @public @deprecated (undocumented) -export const signalService: ServiceRef; +export const signalService: ServiceRef; // @public (undocumented) export interface SignalsService { @@ -52,7 +52,11 @@ export type SignalsServiceOptions = { }; // @public (undocumented) -export const signalsServiceRef: ServiceRef; +export const signalsServiceRef: ServiceRef< + SignalsService, + 'plugin', + 'singleton' +>; // (No @packageDocumentation comment for this package) ``` From d160ce8925c3d67fa711fa7d2de707237eb3dbd9 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 16 Jul 2024 14:49:20 -0400 Subject: [PATCH 039/204] Add trigger to backend-tasks schema Signed-off-by: Tim Hansen --- packages/backend-tasks/src/tasks/types.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index aaa6ca8bd2..a0bdbbaa64 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -378,6 +378,10 @@ function isValidCronFormat(c: string | undefined): boolean { } } +function isValidTrigger(t: string): boolean { + return t === 'manual'; +} + export const taskSettingsV1Schema = z.object({ version: z.literal(1), initialDelayDuration: z @@ -406,6 +410,11 @@ export const taskSettingsV2Schema = z.object({ cadence: z .string() .refine(isValidCronFormat, { message: 'Invalid cron' }) + .or( + z.string().refine(isValidTrigger, { + message: "Invalid trigger, expecting 'manual'", + }), + ) .or( z.string().refine(isValidOptionalDurationString, { message: 'Invalid duration, expecting ISO Period', From df17af74d61c4422a546ac63afe568cd70b5ffe4 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 16 Jul 2024 17:41:09 -0400 Subject: [PATCH 040/204] Update migration, move test file Signed-off-by: Tim Hansen --- .../migrations/scheduler/20240712211735_nullable_next_run.js | 4 ++++ packages/backend-defaults/{ => src}/migrations.test.ts | 5 ----- .../migrations/20240712211735_nullable_next_run.js | 4 ++++ packages/backend-tasks/src/migrations.test.ts | 5 ----- 4 files changed, 8 insertions(+), 10 deletions(-) rename packages/backend-defaults/{ => src}/migrations.test.ts (96%) diff --git a/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js index b636df35d7..efbef1c76f 100644 --- a/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js +++ b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js @@ -31,6 +31,10 @@ exports.up = async function up(knex) { * @returns { Promise } */ exports.down = async function down(knex) { + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { table.dropNullable('next_run_start_at'); }); diff --git a/packages/backend-defaults/migrations.test.ts b/packages/backend-defaults/src/migrations.test.ts similarity index 96% rename from packages/backend-defaults/migrations.test.ts rename to packages/backend-defaults/src/migrations.test.ts index 98f4e41706..cae127442b 100644 --- a/packages/backend-defaults/migrations.test.ts +++ b/packages/backend-defaults/src/migrations.test.ts @@ -107,11 +107,6 @@ describe('migrations', () => { }, ]); - await knex - .delete() - .from('backstage_backend_tasks__tasks') - .where({ next_run_start_at: null }); - await migrateDownOnce(knex); await expect( diff --git a/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js index b636df35d7..efbef1c76f 100644 --- a/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js +++ b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js @@ -31,6 +31,10 @@ exports.up = async function up(knex) { * @returns { Promise } */ exports.down = async function down(knex) { + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { table.dropNullable('next_run_start_at'); }); diff --git a/packages/backend-tasks/src/migrations.test.ts b/packages/backend-tasks/src/migrations.test.ts index 49576d8a58..7583948f71 100644 --- a/packages/backend-tasks/src/migrations.test.ts +++ b/packages/backend-tasks/src/migrations.test.ts @@ -107,11 +107,6 @@ describe('migrations', () => { }, ]); - await knex - .delete() - .from('backstage_backend_tasks__tasks') - .where({ next_run_start_at: null }); - await migrateDownOnce(knex); await expect( From fff1f50a7fbceb1245fd9c519a893a34994ab6b5 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 16 Jul 2024 19:43:03 -0400 Subject: [PATCH 041/204] Fix migration path Signed-off-by: Tim Hansen --- packages/backend-defaults/src/migrations.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-defaults/src/migrations.test.ts b/packages/backend-defaults/src/migrations.test.ts index cae127442b..579c32e626 100644 --- a/packages/backend-defaults/src/migrations.test.ts +++ b/packages/backend-defaults/src/migrations.test.ts @@ -18,7 +18,7 @@ import { Knex } from 'knex'; import { TestDatabases } from '@backstage/backend-test-utils'; import fs from 'fs'; -const migrationsDir = `${__dirname}/../migrations`; +const migrationsDir = `${__dirname}/../migrations/scheduler`; const migrationsFiles = fs.readdirSync(migrationsDir).sort(); async function migrateUpOnce(knex: Knex): Promise { From da971316ae3412c2b0b98f9c0c94fcd8ade65bdb Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 21 Jul 2024 14:01:44 +0530 Subject: [PATCH 042/204] Added test cases for gitlab:issues:create examples Signed-off-by: parmar-abhinav --- .changeset/funny-wolves-learn.md | 5 + .../gitlabIssueCreate.examples.test.ts | 711 ++++++++++++++++++ .../src/actions/gitlabIssueCreate.examples.ts | 168 ++++- 3 files changed, 879 insertions(+), 5 deletions(-) create mode 100644 .changeset/funny-wolves-learn.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts diff --git a/.changeset/funny-wolves-learn.md b/.changeset/funny-wolves-learn.md new file mode 100644 index 0000000000..2010ccd68b --- /dev/null +++ b/.changeset/funny-wolves-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:issues:create examples diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts new file mode 100644 index 0000000000..917ba61d91 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts @@ -0,0 +1,711 @@ +/* + * 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 { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createGitlabIssueAction } from './gitlabIssueCreate'; +import { examples } from './gitlabIssueCreate.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + Issues: { + create: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:issues:create', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(2005, 6, 4, 11, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'sample-token', + apiBaseUrl: 'https://gitlab.com/api/v1', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabIssueAction({ integrations }); + + it(`should ${examples[0].description}`, async () => { + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[1].description}`, async () => { + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [18], + confidential: false, + epicId: undefined, + labels: '', + createdAt: input.createdAt, + dueDate: input.dueDate, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[2].description}`, async () => { + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: input.confidential, + epicId: undefined, + labels: input.labels, + createdAt: input.createdAt, + dueDate: input.dueDate, + discussionToResolve: input.discussionToResolve, + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[3].description}`, async () => { + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[4].description}`, async () => { + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[5].description}`, async () => { + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[6].description}`, async () => { + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[7].description}`, async () => { + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [18, 22], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[8].description}`, async () => { + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[9].description}`, async () => { + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[10].description}`, async () => { + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: 'abc123', + mergeRequestToResolveDiscussionsOf: + input.mergeRequestToResolveDiscussionsOf, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts index 760caf0f9f..04fdeba46d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts @@ -50,8 +50,8 @@ export const examples: TemplateExample[] = [ title: 'Test Issue', assignees: [18], description: 'This is the description of the issue', - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', + createdAt: '2022-09-27T18:00:00.000Z', + dueDate: '2022-09-28T12:00:00.000Z', }, }, ], @@ -72,9 +72,9 @@ export const examples: TemplateExample[] = [ assignees: [18, 15], description: 'This is the description of the issue', confidential: false, - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', - discussionToResolve: 1, + createdAt: '2022-09-27T18:00:00.000Z', + dueDate: '2022-09-28T12:00:00.000Z', + discussionToResolve: '1', epicId: 1, labels: 'phase1:label1,phase2:label2', }, @@ -82,4 +82,162 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitLab issue with token', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue', + description: 'This is the description of the issue', + token: 'sample token', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue with a specific milestone and weight', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue with Milestone', + description: 'This is the description of the issue', + milestoneId: 5, + weight: 3, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type INCIDENT', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'incident', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type TEST', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'test_case', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type TASK with assignees', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'task', + assignees: [18, 22], + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type ISSUE and close it', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'issue', + stateEvent: 'close', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type INCIDENT and reopen it', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'incident', + stateEvent: 'reopen', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab issue to resolve a discussion in a merge request', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue for MR Discussion', + description: 'This is the description of the issue', + mergeRequestToResolveDiscussionsOf: 42, + discussionToResolve: 'abc123', + }, + }, + ], + }), + }, ]; From 382e868473ec01a6a493fe33b633b799f4a057dd Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Mon, 22 Jul 2024 10:17:35 +0530 Subject: [PATCH 043/204] Added test cases for sentry:project:create examples Signed-off-by: parmar-abhinav --- .changeset/warm-monkeys-marry.md | 5 + .../actions/createProject.examples.test.ts | 232 ++++++++++++++++++ .../src/actions/createProject.examples.ts | 37 +++ 3 files changed, 274 insertions(+) create mode 100644 .changeset/warm-monkeys-marry.md create mode 100644 plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts diff --git a/.changeset/warm-monkeys-marry.md b/.changeset/warm-monkeys-marry.md new file mode 100644 index 0000000000..5ea8deae01 --- /dev/null +++ b/.changeset/warm-monkeys-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-sentry': patch +--- + +Added test cases for sentry:project:create examples diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts new file mode 100644 index 0000000000..3e31aef3bc --- /dev/null +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts @@ -0,0 +1,232 @@ +/* + * 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 { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; +import { randomBytes } from 'crypto'; +import { setupServer } from 'msw/node'; +import { HttpResponse, http } from 'msw'; +import { createSentryCreateProjectAction } from './createProject'; +import yaml from 'yaml'; +import { examples } from './createProject.examples'; + +describe('sentry:project:create action', () => { + const worker = setupServer(); + registerMswTestHooks(worker); + + const createScaffolderConfig = (configData: JsonObject = {}) => ({ + config: new ConfigReader({ + scaffolder: { + ...configData, + }, + }), + }); + + const getActionContext = (): ActionContext<{ + organizationSlug: string; + teamSlug: string; + name: string; + slug?: string; + authToken?: string; + }> => + createMockActionContext({ + workspacePath: './dev/proj', + logger: jest.createMockFromModule('winston'), + input: { + organizationSlug: 'org', + teamSlug: 'team', + name: 'test project', + authToken: randomBytes(5).toString('hex'), + }, + }); + + it(`should ${examples[0].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[0].example).steps[1].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer b15711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d4gf93`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project B', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[0].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + actionContext.input = { ...actionContext.input, slug: 'project-slug' }; + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer a14711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d5df96`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project A', + slug: 'scaff-proj-a', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[1].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const sentryScaffolderConfigToken = randomBytes(5).toString('hex'); + const action = createSentryCreateProjectAction( + createScaffolderConfig({ + sentry: { + token: sentryScaffolderConfigToken, + }, + }), + ); + const actionContext = getActionContext(); + actionContext.input.authToken = undefined; + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer ${sentryScaffolderConfigToken}`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project A', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[2].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer c16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf94`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'A very long name for the scaffolded project C that will generate a slug', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts index 9d4551c1b5..3af88a4663 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts @@ -50,4 +50,41 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Creates a Sentry project when no auth token is passed', + example: yaml.stringify({ + steps: [ + { + id: 'create-sentry-project', + action: 'sentry:project:create', + name: 'Create a Sentry project with provided project slug.', + input: { + organizationSlug: 'my-org', + teamSlug: 'team-a', + name: 'Scaffolded project A', + }, + }, + ], + }), + }, + { + description: + 'Creates a Sentry project with a long project name and automatically generated slug.', + example: yaml.stringify({ + steps: [ + { + id: 'create-sentry-project', + action: 'sentry:project:create', + name: 'Create a Sentry project with a long name and no slug provided.', + input: { + organizationSlug: 'my-org', + teamSlug: 'team-c', + name: 'A very long name for the scaffolded project C that will generate a slug', + authToken: + 'c16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf94', + }, + }, + ], + }), + }, ]; From 187f583ba2961dfbce660f224073d049a4bb726e Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Mon, 22 Jul 2024 16:58:50 +0530 Subject: [PATCH 044/204] Added examples for publish:azure action and updated its test cases Signed-off-by: parmar-abhinav --- .changeset/flat-papayas-push.md | 5 + .../src/actions/azure.examples.test.ts | 177 +++++++++++++++++- .../src/actions/azure.examples.ts | 90 +++++++++ 3 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 .changeset/flat-papayas-push.md diff --git a/.changeset/flat-papayas-push.md b/.changeset/flat-papayas-push.md new file mode 100644 index 0000000000..191dad8550 --- /dev/null +++ b/.changeset/flat-papayas-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-azure': minor +--- + +Added examples for publish:azure action and updated its test cases diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts index 35868274fd..dda65a76ea 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts @@ -19,7 +19,10 @@ import { ConfigReader } from '@backstage/config'; import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; import { WebApi } from 'azure-devops-node-api'; -import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { + getRepoSourceDirectory, + initRepoAndPush, +} from '@backstage/plugin-scaffolder-node'; import { examples } from './azure.examples'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -48,6 +51,10 @@ describe('publish:azure examples', () => { host: 'dev.azure.com', credentials: [{ personalAccessToken: 'tokenlols' }], }, + { + host: 'test.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, ], }, }); @@ -114,4 +121,172 @@ describe('publish:azure examples', () => { gitAuthorInfo: {}, }); }); + + it(`should ${examples[3].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: input.gitCommitMessage, + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[4].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { + name: input.gitAuthorName, + email: input.gitAuthorEmail, + }, + }); + }); + + it(`should ${examples[5].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath), + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[6].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: input.token }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[7].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://test.azure.com/organization/project/_git/repo', + webUrl: 'https://test.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://test.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts index 22c2299b30..ae5bfece2b 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts @@ -70,4 +70,94 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: + 'Initializes an Azure DevOps repository with a custom commit message', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + gitCommitMessage: 'Initial setup and configuration', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository with a custom author name and email', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + gitAuthorName: 'John Doe', + gitAuthorEmail: 'john.doe@example.com', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using a specific source path', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + sourcePath: 'path/to/source', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using an authentication token', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + token: 'personal-access-token', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using an custom repo url', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'test.azure.com?organization=organization&project=project&repo=repo', + }, + }, + ], + }), + }, ]; From 612d552e1c5543657621e913466708e68c86d0c3 Mon Sep 17 00:00:00 2001 From: Tavi Nolan Date: Tue, 23 Jul 2024 11:45:56 +0100 Subject: [PATCH 045/204] Updating changes based on review comments Signed-off-by: Tavi Nolan --- .../software-templates}/dry-run-testing.md | 12 +++++------- microsite/sidebars.json | 3 ++- mkdocs.yml | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) rename docs/{plugins => features/software-templates}/dry-run-testing.md (87%) diff --git a/docs/plugins/dry-run-testing.md b/docs/features/software-templates/dry-run-testing.md similarity index 87% rename from docs/plugins/dry-run-testing.md rename to docs/features/software-templates/dry-run-testing.md index 18b7343920..fb2de0f672 100644 --- a/docs/plugins/dry-run-testing.md +++ b/docs/features/software-templates/dry-run-testing.md @@ -1,14 +1,14 @@ --- id: dry run testing title: Dry Run Testing -description: Documentation on how to enable and implement dry run testing in actions +description: How to enable and implement dry run testing in actions --- Scaffolder templates can be tested using the dry run feature of scaffolder actions. This allows you to simulate the effects of running a scaffolder action without making any actual changes to your environment, for example creating a webhook in Github. Once dry run is enabled in the scaffolder action, you can add handling to actions you use in your scaffolder templates to define how an action should operate in a dry run scenario. ## Enabling dry run testing -To enable dry run for your scaffolder action you need to add 'supportsDryRun: true' to the configuration object of 'createTemplateAction' in the function where the behavior of your action is defined: +To enable dry run for your scaffolder action you need to add `supportsDryRun: true` to the configuration object of `createTemplateAction` in the function where the behavior of your action is defined: ```typescript export function exampleAction() { @@ -38,7 +38,7 @@ export function exampleAction() { ## Adding handling for dry run -To add handling for dry run functionality you need to add a check for 'ctx.isDryRun' inside the handler of the configuration object which is being passed into 'createTemplateAction' in the function where the behavior of your action is defined. Once the check is successful, you can perform the desired actions expected in a dry run, e.g. outputting non-sensitive inputs. +To add handling for dry run functionality you need to add a check for `ctx.isDryRun` inside the handler of the configuration object which is being passed into `createTemplateAction` in the function where the behavior of your action is defined. Once the check is successful, you can perform the desired actions expected in a dry run, e.g. outputting non-sensitive inputs. ```typescript async handler(ctx) { @@ -91,9 +91,7 @@ If you're using backend permissions, pass a front-end auth token from a current You can also query the dry run endpoint directly in code, for example: -`dry-run.js` - -```javascript +```javascript title="dry-run.js" const template = yaml.load( await fs.readFile('path/to/templates/template.yaml', 'utf-8'), ); @@ -115,7 +113,7 @@ const response = await fetch( method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: 'Bearer ', // replace 'YOUR_API_KEY' with your actual API key + Authorization: 'Bearer ', // replace 'FRONT_END_TOKEN' with your actual front-end auth token }, body: JSON.stringify(body), }, diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 075025afbb..2e1d74f739 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -120,7 +120,8 @@ "features/software-templates/writing-custom-step-layouts", "features/software-templates/authorizing-scaffolder-template-details", "features/software-templates/migrating-to-rjsf-v5", - "features/software-templates/migrating-from-v1beta2-to-v1beta3" + "features/software-templates/migrating-from-v1beta2-to-v1beta3", + "features/software-templates/dry-run-testing" ] }, { diff --git a/mkdocs.yml b/mkdocs.yml index 045238e97e..3e8c7864c3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' - Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md' + - Dry Run Testing: 'plugins/dry-run-testing.md' - Backstage Search: - Overview: 'features/search/README.md' - Getting Started: 'features/search/getting-started.md' @@ -139,7 +140,6 @@ nav: - URL Reader: 'plugins/url-reader.md' - Testing: - Testing with Jest: 'plugins/testing.md' - - Dry Run Testing: 'plugins/dry-run-testing.md' - Publishing: - Publish private: 'plugins/publish-private.md' - Add to Directory: 'plugins/add-to-directory.md' From e5e7d0501b538933b432384ab0725226ec50ce0d Mon Sep 17 00:00:00 2001 From: "Nolan, Tavi" Date: Tue, 23 Jul 2024 12:43:43 +0100 Subject: [PATCH 046/204] Update mkdocs.yml Signed-off-by: Nolan, Tavi --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3e8c7864c3..707929d132 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,7 +64,7 @@ nav: - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' - Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md' - - Dry Run Testing: 'plugins/dry-run-testing.md' + - Dry Run Testing: 'feature/software-templates/dry-run-testing.md' - Backstage Search: - Overview: 'features/search/README.md' - Getting Started: 'features/search/getting-started.md' From 7a05f509084f487972c276e3018589b51b1fea03 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 25 Jul 2024 12:48:06 +0300 Subject: [PATCH 047/204] fix: allow using notifications without users in catalog additionally refactored and added tests for the notification receiver logic fixes #25768 Signed-off-by: Heikki Hellgren --- .changeset/modern-poems-mate.md | 5 + .../src/service/getUsersForEntityRef.test.ts | 175 ++++++++++++++++++ .../src/service/getUsersForEntityRef.ts | 161 ++++++++++++++++ .../src/service/router.ts | 96 +--------- 4 files changed, 343 insertions(+), 94 deletions(-) create mode 100644 .changeset/modern-poems-mate.md create mode 100644 plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts create mode 100644 plugins/notifications-backend/src/service/getUsersForEntityRef.ts diff --git a/.changeset/modern-poems-mate.md b/.changeset/modern-poems-mate.md new file mode 100644 index 0000000000..06926daac9 --- /dev/null +++ b/.changeset/modern-poems-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +Allow using notifications without users in the catalog diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts new file mode 100644 index 0000000000..b06d6f203b --- /dev/null +++ b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts @@ -0,0 +1,175 @@ +/* + * 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 { mockServices } from '@backstage/backend-test-utils'; +import { getUsersForEntityRef } from './getUsersForEntityRef'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, +} from '@backstage/catalog-model'; + +describe('getUsersForEntityRef', () => { + const catalogApiMock = { + getEntitiesByRefs: jest.fn(), + getEntityByRef: jest.fn(), + }; + const authMock = mockServices.auth(); + + it('should return empty array if entityRef is null', async () => { + await expect( + getUsersForEntityRef(null, [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual([]); + }); + + it('should resolve users without calling catalog', async () => { + await expect( + getUsersForEntityRef(['user:foo', 'user:ignored'], ['user:ignored'], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:foo']); + expect(catalogApiMock.getEntitiesByRefs).not.toHaveBeenCalled(); + }); + + it('should resolve group entities to users', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'parent_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + { + type: RELATION_PARENT_OF, + targetRef: 'group:default/child_group', + }, + ], + }, + ], + }); + + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'child_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/bar', + }, + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/ignored', + }, + ], + }, + ], + }); + + await expect( + getUsersForEntityRef( + 'group:default/parent_group', + ['user:default/ignored'], + { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }, + ), + ).resolves.toEqual(['user:default/foo', 'user:default/bar']); + }); + + it('should resolve user owner of entity from entity ref', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'user:default/foo', + }, + ], + }, + ], + }); + + await expect( + getUsersForEntityRef('component:default/test_component', [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:default/foo']); + }); + + it('should resolve group owner of entity from entity ref', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'group:default/owner_group', + }, + ], + }, + ], + }); + + catalogApiMock.getEntityByRef.mockResolvedValueOnce({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'owner_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + ], + }); + + await expect( + getUsersForEntityRef('component:default/test_component', [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:default/foo']); + }); +}); diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts new file mode 100644 index 0000000000..fd564d6528 --- /dev/null +++ b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts @@ -0,0 +1,161 @@ +/* + * 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 { + Entity, + isGroupEntity, + isUserEntity, + parseEntityRef, + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; + +const isUserEntityRef = (ref: string) => + parseEntityRef(ref).kind.toLocaleLowerCase() === 'user'; + +// Partitions array of entity references to two arrays; user entity refs and other entity refs +const partitionEntityRefs = (refs: string[]): string[][] => + refs.reduce( + ([userEntityRefs, otherEntityRefs]: string[][], ref: string) => { + return isUserEntityRef(ref) + ? [[...userEntityRefs, ref], otherEntityRefs] + : [userEntityRefs, [...otherEntityRefs, ref]]; + }, + [[], []], + ); + +export const getUsersForEntityRef = async ( + entityRef: string | string[] | null, + excludeEntityRefs: string | string[], + options: { + auth: AuthService; + catalogClient: CatalogApi; + }, +): Promise => { + const { auth, catalogClient } = options; + + if (entityRef === null) { + return []; + } + + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + + const excluded = Array.isArray(excludeEntityRefs) + ? excludeEntityRefs + : [excludeEntityRefs]; + + const refsArr = Array.isArray(entityRef) ? entityRef : [entityRef]; + const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(refsArr); + const users: string[] = userEntityRefs.filter(ref => !excluded.includes(ref)); + const entityRefs = otherEntityRefs.filter(ref => !excluded.includes(ref)); + + const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; + let entities: Array = []; + if (entityRefs.length > 0) { + const fetchedEntities = await catalogClient.getEntitiesByRefs( + { + entityRefs, + fields, + }, + { token }, + ); + entities = fetchedEntities.items; + } + + const mapEntity = async (entity: Entity | undefined): Promise => { + if (!entity) { + return []; + } + + const currentEntityRef = stringifyEntityRef(entity); + if (excluded.includes(currentEntityRef)) { + return []; + } + + if (isUserEntity(entity)) { + return [currentEntityRef]; + } + + if (isGroupEntity(entity)) { + if (!entity.relations?.length) { + return []; + } + + const groupUsers = entity.relations + .filter( + relation => + relation.type === RELATION_HAS_MEMBER && + isUserEntityRef(relation.targetRef), + ) + .map(r => r.targetRef); + + const childGroupRefs = entity.relations + .filter(relation => relation.type === RELATION_PARENT_OF) + .map(r => r.targetRef); + + let childGroupUsers: string[][] = []; + if (childGroupRefs.length > 0) { + const childGroups = await catalogClient.getEntitiesByRefs( + { + entityRefs: childGroupRefs, + fields, + }, + { token }, + ); + childGroupUsers = await Promise.all(childGroups.items.map(mapEntity)); + } + + return [...groupUsers, ...childGroupUsers.flat(2)].filter( + ref => !excluded.includes(ref), + ); + } + + if (entity.relations?.length) { + const ownerRef = entity.relations.find( + relation => relation.type === RELATION_OWNED_BY, + )?.targetRef; + + if (!ownerRef) { + return []; + } + + if (isUserEntityRef(ownerRef)) { + if (excluded.includes(ownerRef)) { + return []; + } + return [ownerRef]; + } + + const owner = await catalogClient.getEntityByRef(ownerRef, { token }); + return mapEntity(owner); + } + + return []; + }; + + for (const entity of entities) { + const u = await mapEntity(entity); + users.push(...u); + } + + return [...new Set(users)]; +}; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 3ca3ce46bc..abd7e2dc13 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -23,15 +23,6 @@ import { } from '../database'; import { v4 as uuid } from 'uuid'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { - Entity, - isGroupEntity, - isUserEntity, - RELATION_HAS_MEMBER, - RELATION_OWNED_BY, - RELATION_PARENT_OF, - stringifyEntityRef, -} from '@backstage/catalog-model'; import { NotificationProcessor, NotificationSendOptions, @@ -54,6 +45,7 @@ import { NotificationStatus, } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; +import { getUsersForEntityRef } from './getUsersForEntityRef'; /** @internal */ export interface RouterOptions { @@ -94,91 +86,6 @@ export async function createRouter( return info.userEntityRef; }; - const getUsersForEntityRef = async ( - entityRef: string | string[] | null, - excludeEntityRefs: string | string[], - ): Promise => { - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: await auth.getOwnServiceCredentials(), - targetPluginId: 'catalog', - }); - - if (entityRef === null) { - return []; - } - - const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; - - const refs = Array.isArray(entityRef) ? entityRef : [entityRef]; - const entities = await catalogClient.getEntitiesByRefs( - { - entityRefs: refs, - fields, - }, - { token }, - ); - - const excluded = Array.isArray(excludeEntityRefs) - ? excludeEntityRefs - : [excludeEntityRefs]; - - const mapEntity = async (entity: Entity | undefined): Promise => { - if (!entity) { - return []; - } - - const currentEntityRef = stringifyEntityRef(entity); - if (excluded.includes(currentEntityRef)) { - return []; - } - - if (isUserEntity(entity)) { - return [currentEntityRef]; - } else if (isGroupEntity(entity) && entity.relations) { - const users = entity.relations - .filter(relation => relation.type === RELATION_HAS_MEMBER) - .map(r => r.targetRef); - - const childGroupRefs = entity.relations - .filter(relation => relation.type === RELATION_PARENT_OF) - .map(r => r.targetRef); - - const childGroups = await catalogClient.getEntitiesByRefs( - { - entityRefs: childGroupRefs, - fields, - }, - { token }, - ); - const childGroupUsers = await Promise.all( - childGroups.items.map(mapEntity), - ); - return [...users, ...childGroupUsers.flat(2)]; - } else if (entity.relations) { - const ownerRef = entity.relations.find( - relation => relation.type === RELATION_OWNED_BY, - )?.targetRef; - - if (ownerRef) { - const owner = await catalogClient.getEntityByRef(ownerRef, { token }); - if (owner) { - return mapEntity(owner); - } - } - } - - return []; - }; - - const users: string[] = []; - for (const entity of entities.items) { - const u = await mapEntity(entity); - users.push(...u); - } - - return users; - }; - const filterProcessors = (payload: NotificationPayload) => { const result: NotificationProcessor[] = []; @@ -543,6 +450,7 @@ export async function createRouter( users = await getUsersForEntityRef( entityRef, recipients.excludeEntityRef ?? [], + { auth, catalogClient }, ); } catch (e) { logger.error(`Failed to resolve notification receivers: ${e}`); From 6988f07886abd44898cf9c3e216e9bb30a85c107 Mon Sep 17 00:00:00 2001 From: Valber Junior <84424883+ValberJunior@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:44:49 -0300 Subject: [PATCH 048/204] Create infracost.yaml Signed-off-by: Valber Junior <84424883+ValberJunior@users.noreply.github.com> --- microsite/data/plugins/infracost.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 microsite/data/plugins/infracost.yaml diff --git a/microsite/data/plugins/infracost.yaml b/microsite/data/plugins/infracost.yaml new file mode 100644 index 0000000000..977309d924 --- /dev/null +++ b/microsite/data/plugins/infracost.yaml @@ -0,0 +1,14 @@ +--- +title: VeeCode Infracost +author: VeeCode Platform +authorUrl: https://platform.vee.codes/ +category: FinOps +description: The Infracost plug-in works in conjunction with terraform and provides a graphical representation and cost estimate data for your entity at your respective provider. +iconUrl: https://veecode-platform.github.io/support/imgs/logo_5.svg +npmPackageName: '@veecode-platform/backstage-plugin-infracost' +tags: + - finops + - infracost + - estimate + - terraform +addedDate: '2024-07-25' From 7f191a480d4665cfae3a4080be90eaae0e193937 Mon Sep 17 00:00:00 2001 From: Valber Junior <84424883+ValberJunior@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:05:57 -0300 Subject: [PATCH 049/204] Update infracost.yaml Signed-off-by: Valber Junior <84424883+ValberJunior@users.noreply.github.com> --- microsite/data/plugins/infracost.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/microsite/data/plugins/infracost.yaml b/microsite/data/plugins/infracost.yaml index 977309d924..b6ead655d5 100644 --- a/microsite/data/plugins/infracost.yaml +++ b/microsite/data/plugins/infracost.yaml @@ -4,6 +4,7 @@ author: VeeCode Platform authorUrl: https://platform.vee.codes/ category: FinOps description: The Infracost plug-in works in conjunction with terraform and provides a graphical representation and cost estimate data for your entity at your respective provider. +documentation: https://platform.vee.codes/plugin/Infracost iconUrl: https://veecode-platform.github.io/support/imgs/logo_5.svg npmPackageName: '@veecode-platform/backstage-plugin-infracost' tags: From 0bb72c38f0ac8a19c26c0dee9af0ddc56fd610e6 Mon Sep 17 00:00:00 2001 From: "Nolan, Tavi" Date: Fri, 26 Jul 2024 12:00:06 +0100 Subject: [PATCH 050/204] Update dry-run-testing.md based on comments Signed-off-by: Nolan, Tavi --- .../software-templates/dry-run-testing.md | 56 +------------------ 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/docs/features/software-templates/dry-run-testing.md b/docs/features/software-templates/dry-run-testing.md index fb2de0f672..dfabad265e 100644 --- a/docs/features/software-templates/dry-run-testing.md +++ b/docs/features/software-templates/dry-run-testing.md @@ -1,5 +1,5 @@ --- -id: dry run testing +id: dry-run-testing title: Dry Run Testing description: How to enable and implement dry run testing in actions --- @@ -77,57 +77,3 @@ You will also need to add tests for the dry run handling, for example: expect(...); }); ``` - -## Dry run via API call - -Dry run can be performed using the dry-run API, which allows the dry run to be completed in code. -This [command line script](https://github.com/backstage/backstage/blob/master/contrib/scaffolder/template-testing-dry-run.md) offers a way for you to do work with dry-run API. You run it against a running instance, either locally or remote, and use it like: - -```bash -scaffolder-dry http://localhost:7007/ template-directory values.yml output-directory -``` - -If you're using backend permissions, pass a front-end auth token from a current browser session via --token $FRONTEND_TOKEN. - -You can also query the dry run endpoint directly in code, for example: - -```javascript title="dry-run.js" -const template = yaml.load( - await fs.readFile('path/to/templates/template.yaml', 'utf-8'), -); -const values = JSON.parse( - await fs.readFile('path/to/templates/template_values.json', 'utf-8'), -); - -// Prepare the request body -const body = { - template, - values, - directoryContents, -}; - -// Send the request to the dry-run endpoint -const response = await fetch( - 'http://localhost:7000/api/scaffolder/v2/dry-run', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer ', // replace 'FRONT_END_TOKEN' with your actual front-end auth token - }, - body: JSON.stringify(body), - }, -); -``` - -`template_values.json` - -```json -{ - "example": "test", - "name": "helloworld" -} -``` - -In this example `template.yaml` is your template yaml file, `template_values.json` would be a json file containing key value pairs for your template inputs, and `directoryContents` is an array that is populated with objects representing the contents of a specified directory (e.g. the same directory where you have your template yaml). Each object in the array corresponds to a file within the directory and contains two properties; the name of the file and the content of the file, encoded in Base64. -This is needed if any other files were required by the actions your template is using. From cd203f12502c08030c44f3b7d02cbbfdce8e0339 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sat, 27 Jul 2024 15:27:20 +0530 Subject: [PATCH 051/204] Added examples for action github:pages and improved test cases Signed-off-by: parmar-abhinav --- .changeset/thin-carrots-eat.md | 5 + .../githubPagesEnable.examples.test.ts | 364 +++++++++++++++++- .../src/actions/githubPagesEnable.examples.ts | 205 ++++++++++ 3 files changed, 570 insertions(+), 4 deletions(-) create mode 100644 .changeset/thin-carrots-eat.md diff --git a/.changeset/thin-carrots-eat.md b/.changeset/thin-carrots-eat.md new file mode 100644 index 0000000000..570f21f83d --- /dev/null +++ b/.changeset/thin-carrots-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added examples for action github:pages and improved its test cases diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts index ee7b5caa65..f0aa0ea5a2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts @@ -52,9 +52,14 @@ describe('github:pages', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const input = yaml.parse(examples[0].example).steps[0].input; const mockContext = createMockActionContext({ - input, + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/', + token: 'gph_YourGitHubToken', + }, }); beforeEach(() => { @@ -68,8 +73,20 @@ describe('github:pages', () => { afterEach(jest.resetAllMocks); - it('should work with example input', async () => { - await action.handler(mockContext); + it(`Should ${examples[0].description}`, async () => { + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); expect(mockOctokit.request).toHaveBeenCalledWith( 'POST /repos/{owner}/{repo}/pages', @@ -87,4 +104,343 @@ describe('github:pages', () => { }, ); }); + it(`Should ${examples[1].description}`, async () => { + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'customPathRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[2].description}`, async () => { + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'legacyOwner', + repo: 'legacyRepo', + build_type: 'legacy', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[3].description}`, async () => { + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'branchOwner', + repo: 'customBranchRepo', + build_type: 'workflow', + source: { + branch: 'develop', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + it(`Should ${examples[4].description}`, async () => { + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'fullCustomRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[5].description}`, async () => { + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'minimalOwner', + repo: 'minimalRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + it(`Should ${examples[6].description}`, async () => { + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'customBuildPathRepo', + build_type: 'legacy', + source: { + branch: 'main', + path: '/custom-path', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[7].description}`, async () => { + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'branchPathOwner', + repo: 'customBranchPathRepo', + build_type: 'workflow', + source: { + branch: 'feature-branch', + path: '/project-docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[8].description}`, async () => { + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwnerName', + repo: 'customRepoName', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[9].description}`, async () => { + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'tokenOwner', + repo: 'customTokenRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/site', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[10].description}`, async () => { + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'tokenOwner', + repo: 'specificTokenRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[11].description}`, async () => { + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'docsOwner', + repo: 'docSiteRepo', + build_type: 'workflow', + source: { + branch: 'docs-branch', + path: '/docs-site', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts index be4a33e5df..57102c4b92 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts @@ -37,4 +37,209 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: + 'Enables GitHub Pages for a repository with a custom source path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-path', + name: 'Enable GitHub Pages with Custom Source Path', + input: { + repoUrl: 'github.com?repo=customPathRepo&owner=customOwner', + sourcePath: '/docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository using legacy build type.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-legacy', + name: 'Enable GitHub Pages with Legacy Build Type', + input: { + repoUrl: 'github.com?repo=legacyRepo&owner=legacyOwner', + buildType: 'legacy', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a custom source branch.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-branch', + name: 'Enable GitHub Pages with Custom Source Branch', + input: { + repoUrl: 'github.com?repo=customBranchRepo&owner=branchOwner', + sourceBranch: 'develop', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + + { + description: + 'Enables GitHub Pages for a repository with full customization.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-full-custom', + name: 'Enable GitHub Pages with Full Customization', + input: { + repoUrl: 'github.com?repo=fullCustomRepo&owner=customOwner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with minimal configuration.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-minimal', + name: 'Enable GitHub Pages with Minimal Configuration', + input: { + repoUrl: 'github.com?repo=minimalRepo&owner=minimalOwner', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with custom build type and source path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-build-path', + name: 'Enable GitHub Pages with Custom Build Type and Source Path', + input: { + repoUrl: 'github.com?repo=customBuildPathRepo&owner=customOwner', + buildType: 'legacy', + sourcePath: '/custom-path', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with custom source branch and path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-branch-path', + name: 'Enable GitHub Pages with Custom Source Branch and Path', + input: { + repoUrl: + 'github.com?repo=customBranchPathRepo&owner=branchPathOwner', + sourceBranch: 'feature-branch', + sourcePath: '/project-docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a custom owner and repository name.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-owner-repo', + name: 'Enable GitHub Pages with Custom Owner and Repository Name', + input: { + repoUrl: 'github.com?repo=customRepoName&owner=customOwnerName', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with full customization and a different token.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-full-custom-diff-token', + name: 'Enable GitHub Pages with Full Customization and Different Token', + input: { + repoUrl: 'github.com?repo=customTokenRepo&owner=tokenOwner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/site', + token: 'gph_DifferentGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a specific token for authorization.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-specific-token', + name: 'Enable GitHub Pages with Specific Token', + input: { + repoUrl: 'github.com?repo=specificTokenRepo&owner=tokenOwner', + token: 'gph_SpecificGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a documentation site with custom configuration.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-doc-site', + name: 'Enable GitHub Pages for Documentation Site', + input: { + repoUrl: 'github.com?repo=docSiteRepo&owner=docsOwner', + buildType: 'workflow', + sourceBranch: 'docs-branch', + sourcePath: '/docs-site', + token: 'gph_DocsGitHubToken', + }, + }, + ], + }), + }, ]; From 9e2918640a05130650f1899f380a546691ebd926 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Sat, 27 Jul 2024 14:28:55 +0100 Subject: [PATCH 052/204] yarn-plugin: implement BackstageResolver#getSatisfying It turns out this method is called for `backstage:` descriptors during `yarn dedupe`, so we need to implement it. Signed-off-by: MT Lewis --- .../src/resolver/BackstageResolver.test.ts | 83 +++++++++++++++++++ .../src/resolver/BackstageResolver.ts | 31 +++++-- 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts index 4cc0c1c9f2..e404970b23 100644 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts @@ -178,4 +178,87 @@ describe('BackstageResolver', () => { ).rejects.toThrow(/invalid backstage version/i); }); }); + + describe('getSatisfying', () => { + it('filters out locators for other packages', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + {}, + [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'foo'), + 'npm:1.2.3', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'bar'), + 'npm:1.2.3', + ), + ], + ), + ).resolves.toEqual({ + locators: [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + ], + sorted: true, + }); + }); + + it('filters out locators for other package versions', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + {}, + [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:5.6.7', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'bar'), + 'npm:7.8.9', + ), + ], + ), + ).resolves.toEqual({ + locators: [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + ], + sorted: true, + }); + }); + + it('throws for non `backstage:` descriptors', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'npm:1.2.3', + ), + {}, + [], + ), + ).rejects.toThrow(/unexpected npm: range/i); + }); + }); }); diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.ts index 33bba5df53..82e6c013a3 100644 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.ts +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.ts @@ -92,6 +92,29 @@ export class BackstageResolver implements Resolver { ]; } + /** + * Given a descriptor and a list of possible locators, return a filtered list + * containing only locators that satisfy the descriptor. Since each Backstage + * release version corresponds to a single version for each package, we just + * need to filter that array for locators with that exact version. + */ + async getSatisfying( + descriptor: Descriptor, + _dependencies: Record, + locators: Array, + ): Promise<{ locators: Locator[]; sorted: boolean }> { + const packageVersion = await getPackageVersion(descriptor); + + return { + locators: locators.filter( + locator => + structUtils.areIdentsEqual(descriptor, locator) && + locator.reference === `npm:${packageVersion}`, + ), + sorted: true, + }; + } + /** * This plugin does not need to support any locators itself, since the * `getCandidates` method will always convert `backstage:` versions into @@ -106,14 +129,6 @@ export class BackstageResolver implements Resolver { */ getResolutionDependencies = () => ({}); - /** - * Candidate versions produced by this resolver always use the `npm:` - * protocol, so this function will never be called. - */ - async getSatisfying(): Promise<{ locators: Locator[]; sorted: boolean }> { - throw new Error('Unreachable'); - } - /** * Once transformed into locators (through getCandidates), the versions are * resolved by the NpmSemverResolver From bfeba46d96275da65c2fc5c5427f61558c96d4ee Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 28 Jul 2024 13:22:40 -0500 Subject: [PATCH 053/204] Included permission config Signed-off-by: Andre Wanlin --- .changeset/silly-scissors-turn.md | 5 +++++ .../create-app/templates/default-app/app-config.yaml.hbs | 4 ++++ .../templates/default-app/packages/backend/src/index.ts | 1 + 3 files changed, 10 insertions(+) create mode 100644 .changeset/silly-scissors-turn.md diff --git a/.changeset/silly-scissors-turn.md b/.changeset/silly-scissors-turn.md new file mode 100644 index 0000000000..985a6fb4fd --- /dev/null +++ b/.changeset/silly-scissors-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Included permission config and enabled it out of the box diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index f784d5ba88..b16bdb758d 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -106,3 +106,7 @@ catalog: kubernetes: # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options + + +permission: + enabled: true \ No newline at end of file diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index cf2caae72b..f57d4c5f87 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -32,6 +32,7 @@ backend.add(import('@backstage/plugin-catalog-backend-module-logs')); // permission plugin backend.add(import('@backstage/plugin-permission-backend/alpha')); +// See https://backstage.io/docs/permissions/getting-started for how to create your own permission policy backend.add( import('@backstage/plugin-permission-backend-module-allow-all-policy'), ); From 7a6b05f4c93550ae80ffb68f8c2ed7c7275bc02d Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 28 Jul 2024 13:24:01 -0500 Subject: [PATCH 054/204] Added new line Signed-off-by: Andre Wanlin --- packages/create-app/templates/default-app/app-config.yaml.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index b16bdb758d..e4fa1e6f03 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -109,4 +109,4 @@ kubernetes: permission: - enabled: true \ No newline at end of file + enabled: true From a837ab0d1e14482baa23ab32bdb94f767df1caee Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 28 Jul 2024 13:45:11 -0500 Subject: [PATCH 055/204] Removed extra line Signed-off-by: Andre Wanlin --- packages/create-app/templates/default-app/app-config.yaml.hbs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index e4fa1e6f03..c5aa5fafdb 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -107,6 +107,5 @@ catalog: kubernetes: # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options - permission: enabled: true From b76e877bcf2de9ffcea62d5b49183587ce6b0aed Mon Sep 17 00:00:00 2001 From: Evan Kelly Date: Tue, 18 Jun 2024 14:40:41 +0100 Subject: [PATCH 056/204] added script for datadog script to docs Signed-off-by: Evan Kelly --- docs/integrations/datadog-rum/installation.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 4dc6c24134..b5f2a494c9 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -36,3 +36,48 @@ There are two optional arguments: - `site`: The Datadog site of your organization; defaults to `datadoghq.com` - `env`: The application environment for Datadog events (no default) + +## Script to fix Datadog RUM not Publishing Issue + +Copy and paste this section to your `packages/app/public/index.html` + +``` +<% if (config.has('app.datadogRum')) { %> + + <% } %> +``` From 09d7e3f1d9d813655b286c280bb80044946d870a Mon Sep 17 00:00:00 2001 From: Evan Kelly Date: Wed, 26 Jun 2024 14:14:09 +0100 Subject: [PATCH 057/204] run prettier check Signed-off-by: Evan Kelly --- docs/integrations/datadog-rum/installation.md | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index b5f2a494c9..1301f111f7 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -42,42 +42,42 @@ There are two optional arguments: Copy and paste this section to your `packages/app/public/index.html` ``` -<% if (config.has('app.datadogRum')) { %> - - <% } %> +<% if (config.has('app.datadogRum')) { %> + + <% } %> ``` From 51bf5fa47d87b8ca76c5fa9469b1362c065bb8d0 Mon Sep 17 00:00:00 2001 From: Evan Kelly Date: Tue, 2 Jul 2024 10:53:39 +0100 Subject: [PATCH 058/204] make change suggested in pr Signed-off-by: Evan Kelly --- docs/integrations/datadog-rum/installation.md | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 1301f111f7..f636b100a1 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -26,20 +26,7 @@ app: # sessionReplaySampleRate: 0 ``` -If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. - -The `clientToken` and `applicationId` are generated from the Datadog RUM page -following -[these instructions](https://docs.datadoghq.com/real_user_monitoring/browser/). - -There are two optional arguments: - -- `site`: The Datadog site of your organization; defaults to `datadoghq.com` -- `env`: The application environment for Datadog events (no default) - -## Script to fix Datadog RUM not Publishing Issue - -Copy and paste this section to your `packages/app/public/index.html` +If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. If you a Datadog not publishing issue copy and paste this section in to your `packages/app/public/index.html` ``` <% if (config.has('app.datadogRum')) { %> @@ -81,3 +68,12 @@ Copy and paste this section to your `packages/app/public/index.html` <% } %> ``` + +The `clientToken` and `applicationId` are generated from the Datadog RUM page +following +[these instructions](https://docs.datadoghq.com/real_user_monitoring/browser/). + +There are two optional arguments: + +- `site`: The Datadog site of your organization; defaults to `datadoghq.com` +- `env`: The application environment for Datadog events (no default) From 100ecdcb947e18d015ce26312da55c48b9196f53 Mon Sep 17 00:00:00 2001 From: Evan Kelly Date: Wed, 10 Jul 2024 08:07:38 +0100 Subject: [PATCH 059/204] changes for comments on pr Signed-off-by: Evan Kelly --- docs/integrations/datadog-rum/installation.md | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index f636b100a1..5afd05d6cb 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -26,47 +26,49 @@ app: # sessionReplaySampleRate: 0 ``` -If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. If you a Datadog not publishing issue copy and paste this section in to your `packages/app/public/index.html` +If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. -``` +:::note +In case after a proper configuration, the events still arent being capture copy and paste this section in to your `packages/app/public/index.html` under the `` tag. + +```html <% if (config.has('app.datadogRum')) { %> - - <% } %> + +<% } %> ``` The `clientToken` and `applicationId` are generated from the Datadog RUM page From 3f88be264f8fe1027a438d96772fdbb987b84b54 Mon Sep 17 00:00:00 2001 From: "Kelly, Evan" Date: Fri, 12 Jul 2024 09:31:01 +0100 Subject: [PATCH 060/204] Update installation.md Signed-off-by: Kelly, Evan Signed-off-by: Evan Kelly --- docs/integrations/datadog-rum/installation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 5afd05d6cb..414317d593 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -28,6 +28,8 @@ app: If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. +Please note, there's a bug where env value MUST be specified at build time + :::note In case after a proper configuration, the events still arent being capture copy and paste this section in to your `packages/app/public/index.html` under the `` tag. From 612817b0ce5cd945cc7a9ca16a73f478d8aa296c Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 29 Jul 2024 07:23:59 -0500 Subject: [PATCH 061/204] Added notes based on feedback Signed-off-by: Andre Wanlin --- packages/create-app/templates/default-app/app-config.yaml.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index c5aa5fafdb..ca52ec530c 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -107,5 +107,7 @@ catalog: kubernetes: # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options +# see https://backstage.io/docs/permissions/getting-started for more on the permission framework permission: + # setting this to `false` will disable permissions enabled: true From 3fcb1313ff60b682ca5c44909a3e102f96681581 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 24 Jul 2024 17:09:29 +0200 Subject: [PATCH 062/204] chore: implementing some other apis for testing extensions without a react tree Signed-off-by: blam --- packages/frontend-test-utils/package.json | 1 + .../src/app/createExtensionTester.test.tsx | 135 ++++++++++++++ .../src/app/createExtensionTester.tsx | 167 +++++++++++++++--- yarn.lock | 1 + 4 files changed, 277 insertions(+), 27 deletions(-) diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 1d0b00132b..89a3eb10db 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -31,6 +31,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/config": "workspace:^", "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index f59d44f6d7..889c815fc7 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -24,6 +24,8 @@ import { createApiExtension, createApiFactory, createExtension, + createExtensionDataRef, + createExtensionInput, createSchemaFromZod, useAnalytics, useApi, @@ -31,6 +33,10 @@ import { import { MockAnalyticsApi } from '../apis'; import { createExtensionTester } from './createExtensionTester'; +const stringDataRef = createExtensionDataRef().with({ + id: 'test.string', +}); + describe('createExtensionTester', () => { const defaultDefinition = { namespace: 'test', @@ -229,4 +235,133 @@ describe('createExtensionTester', () => { ), ); }); + + it('should return the correct dataRef when called', () => { + const extension = createExtension({ + namespace: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(stringDataRef)).toBe('test-text'); + }); + + it('should throw an error if trying to access an instance not provided to the tester', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension); + + expect(() => tester.query(extension2)).toThrow( + "Extension with ID 'test/e2' not found, please make sure it's added to the tester", + ); + }); + + it('should throw an error if trying to access an instance which is not part of the tree', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension).add(extension2); + + expect(() => tester.query(extension2)).toThrow( + "Extension with ID 'test/e2' has not been instantiated, because it is not part of the test subject's extension tree", + ); + }); + + // TODO: this should be implemented + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should allow querying an extension and getting outputs', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + inputs: { + input: createExtensionInput( + { + output: stringDataRef, + }, + { singleton: true }, + ), + }, + factory: ({ inputs }) => ({ + text: `nest-${inputs.input.output.output}`, + }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'test/e1', input: 'blob' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension).add(extension2); + + expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + expect(tester.query(extension2).data(stringDataRef)).toBe('test-text'); + // @ts-expect-error + expect(tester.query(extension).input('input').data(stringDataRef)).toBe( + 'nest-test-text', + ); + }); + + // TODO: this should be implemented + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should allow defining inputs to extensions without a corresponding extension definition', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + inputs: { + input: createExtensionInput( + { + output: stringDataRef, + }, + { singleton: true }, + ), + }, + factory: ({ inputs }) => ({ + text: `nest-${inputs.input.output.output}`, + }), + }); + + const tester = createExtensionTester(extension, { + // @ts-expect-error + inputs: { input: 'test-text' }, + }); + + expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 81b74c9fce..1d41958a09 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -20,6 +20,10 @@ import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { ExtensionDataValue, + AppNode, + AppTree, + Extension, + ExtensionDataRef, ExtensionDefinition, IconComponent, RouteRef, @@ -31,12 +35,20 @@ import { createRouterExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; -import { MockConfigApi } from '@backstage/test-utils'; +import { Config, ConfigReader } from '@backstage/config'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/createExtension'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { resolveAppTree } from '../../../frontend-app-api/src/tree/resolveAppTree'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveAppNodeSpecs'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; const NavItem = (props: { routeRef: RouteRef; @@ -86,6 +98,35 @@ const TestAppNavExtension = createExtension({ }, }); +/** @public */ +export class ExtensionQuery { + #node: AppNode; + + constructor(node: AppNode) { + this.#node = node; + } + + get node() { + return this.#node; + } + + get instance() { + const instance = this.#node.instance; + if (!instance) { + throw new Error( + `Unable to access the instance of extension with ID '${ + this.#node.spec.id + }'`, + ); + } + return instance; + } + + data(ref: ExtensionDataRef): T | undefined { + return this.instance.getData(ref); + } +} + /** @public */ export class ExtensionTester { /** @internal */ @@ -139,8 +180,11 @@ export class ExtensionTester { return tester; } + #tree?: AppTree; + readonly #extensions = new Array<{ id: string; + extension: Extension; definition: ExtensionDefinition; config?: JsonValue; }>(); @@ -149,6 +193,12 @@ export class ExtensionTester { extension: ExtensionDefinition, options?: { config?: TConfigInput }, ): ExtensionTester { + if (this.#tree) { + throw new Error( + 'Cannot add more extensions accessing the extension tree', + ); + } + const { name, namespace } = extension; const definition = { @@ -157,10 +207,11 @@ export class ExtensionTester { name: !namespace && !name ? 'test' : name, }; - const { id } = resolveExtensionDefinition(definition); + const resolvedExtension = resolveExtensionDefinition(definition); this.#extensions.push({ - id, + id: resolvedExtension.id, + extension: resolvedExtension, definition, config: options?.config as JsonValue, }); @@ -168,38 +219,42 @@ export class ExtensionTester { return this; } + data(ref: ExtensionDataRef): T | undefined { + const tree = this.#resolveTree(); + + return new ExtensionQuery(tree.root).data(ref); + } + + query(id: string | ExtensionDefinition): ExtensionQuery { + const tree = this.#resolveTree(); + + const actualId = + typeof id === 'string' ? id : resolveExtensionDefinition(id).id; + + const node = tree.nodes.get(actualId); + + if (!node) { + throw new Error( + `Extension with ID '${actualId}' not found, please make sure it's added to the tester.`, + ); + } else if (!node.instance) { + throw new Error( + `Extension with ID '${actualId}' has not been instantiated, because it is not part of the test subject's extension tree.`, + ); + } + return new ExtensionQuery(node); + } + render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; - const [subject, ...rest] = this.#extensions; + const [subject] = this.#extensions; if (!subject) { throw new Error( 'No subject found. At least one extension should be added to the tester.', ); } - const extensionsConfig: JsonArray = [ - ...rest.map(extension => ({ - [extension.id]: { - config: extension.config, - }, - })), - { - [subject.id]: { - config: subject.config, - disabled: false, - }, - }, - ]; - - const finalConfig = { - ...config, - app: { - ...(typeof config.app === 'object' ? config.app : undefined), - extensions: extensionsConfig, - }, - }; - const app = createSpecializedApp({ features: [ createExtensionOverrides({ @@ -215,11 +270,69 @@ export class ExtensionTester { ], }), ], - config: new MockConfigApi(finalConfig), + config: this.#getConfig(config), }); return render(app.createRoot()); } + + #resolveTree() { + if (this.#tree) { + return this.#tree; + } + + const [subject] = this.#extensions; + if (!subject) { + throw new Error( + 'No subject found. At least one extension should be added to the tester.', + ); + } + + const tree = resolveAppTree( + subject.id, + resolveAppNodeSpecs({ + features: [], + builtinExtensions: this.#extensions.map(_ => _.extension), + parameters: readAppExtensionsConfig(this.#getConfig()), + }), + ); + + instantiateAppNodeTree(tree.root); + + this.#tree = tree; + + return tree; + } + + #getConfig(additionalConfig?: JsonObject): Config { + const [subject, ...rest] = this.#extensions; + + const extensionsConfig: JsonArray = [ + ...rest.map(extension => ({ + [extension.id]: { + config: extension.config, + }, + })), + { + [subject.id]: { + config: subject.config, + disabled: false, + }, + }, + ]; + + return ConfigReader.fromConfigs([ + { context: 'render-config', data: additionalConfig ?? {} }, + { + context: 'test', + data: { + app: { + extensions: extensionsConfig, + }, + }, + }, + ]); + } } /** @public */ diff --git a/yarn.lock b/yarn.lock index 155c0eeb9e..135cd07035 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4546,6 +4546,7 @@ __metadata: resolution: "@backstage/frontend-test-utils@workspace:packages/frontend-test-utils" dependencies: "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" From 820944913c16ea1fc3c0b0c4d027c8d56126ab2e Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 24 Jul 2024 17:10:24 +0200 Subject: [PATCH 063/204] chore: added changeset Signed-off-by: blam --- .changeset/clever-pans-brake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clever-pans-brake.md diff --git a/.changeset/clever-pans-brake.md b/.changeset/clever-pans-brake.md new file mode 100644 index 0000000000..1f49c7a842 --- /dev/null +++ b/.changeset/clever-pans-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Added new APIs for testing extensions From 85ef461b0c53e5eb28c01c6ce7fdaeffc5edb7b0 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 29 Jul 2024 11:46:28 +0200 Subject: [PATCH 064/204] chore: fixing api-reports Signed-off-by: blam --- packages/frontend-test-utils/api-report.md | 18 ++++++++++++++++++ packages/frontend-test-utils/src/app/index.ts | 1 + 2 files changed, 19 insertions(+) diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 3b368394ca..3aa9069219 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -7,7 +7,10 @@ import { AnalyticsApi } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/frontend-plugin-api'; +import { AppNode } from '@backstage/frontend-plugin-api'; +import { AppNodeInstance } from '@backstage/frontend-plugin-api'; import { ErrorWithContext } from '@backstage/test-utils'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; import { MockConfigApi } from '@backstage/test-utils'; @@ -36,6 +39,17 @@ export function createExtensionTester( export { ErrorWithContext }; +// @public (undocumented) +export class ExtensionQuery { + constructor(node: AppNode); + // (undocumented) + data(ref: ExtensionDataRef): T | undefined; + // (undocumented) + get instance(): AppNodeInstance; + // (undocumented) + get node(): AppNode; +} + // @public (undocumented) export class ExtensionTester { // (undocumented) @@ -46,6 +60,10 @@ export class ExtensionTester { }, ): ExtensionTester; // (undocumented) + data(ref: ExtensionDataRef): T | undefined; + // (undocumented) + query(id: string | ExtensionDefinition): ExtensionQuery; + // (undocumented) render(options?: { config?: JsonObject }): RenderResult; } diff --git a/packages/frontend-test-utils/src/app/index.ts b/packages/frontend-test-utils/src/app/index.ts index 2351561022..6c6ef26e4c 100644 --- a/packages/frontend-test-utils/src/app/index.ts +++ b/packages/frontend-test-utils/src/app/index.ts @@ -17,6 +17,7 @@ export { createExtensionTester, type ExtensionTester, + type ExtensionQuery, } from './createExtensionTester'; export { renderInTestApp, type TestAppOptions } from './renderInTestApp'; From 6d4cb97f071c17ccd285d83c6a34e938ec77c745 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Tue, 30 Jul 2024 12:41:44 +0530 Subject: [PATCH 065/204] Added examples for github:repo:create action and improved test cases Signed-off-by: parmar-abhinav --- .changeset/itchy-experts-tie.md | 5 + .../actions/githubRepoCreate.examples.test.ts | 2215 +++++++++++++++++ .../src/actions/githubRepoCreate.examples.ts | 928 +++++++ 3 files changed, 3148 insertions(+) create mode 100644 .changeset/itchy-experts-tie.md diff --git a/.changeset/itchy-experts-tie.md b/.changeset/itchy-experts-tie.md new file mode 100644 index 0000000000..c5bf99ecaf --- /dev/null +++ b/.changeset/itchy-experts-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added examples for github:repo:create action and improved test cases diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts index 843a32f758..1a81a54c64 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts @@ -58,6 +58,7 @@ const mockOctokit = { getRepoPublicKey: jest.fn(), }, }, + request: jest.fn().mockResolvedValue({}), }; jest.mock('octokit', () => ({ Octokit: class { @@ -188,4 +189,2218 @@ describe('github:repo:create examples', () => { has_wiki: false, // disable wiki }); }); + + it(`Should ${examples[3].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://example.com', + }); + }); + + it(`Should ${examples[4].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[5].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[6].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[7].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: false, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[8].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'pull_request_title', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[9].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'blank', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[10].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: true, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[11].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[12].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[12].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[13].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[13].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[14].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[14].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[15].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[15].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[16].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[16].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[17].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[17].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: true, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[18].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[18].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[19].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[19].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[20].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[20].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: 'A repository for project XYZ', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://project-xyz.com', + }); + }); + + it(`Should ${examples[21].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[21].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[22].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[22].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[23].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[23].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: true, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[24].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[24].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[25].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[25].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: false, + description: undefined, + has_projects: false, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[26].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[26].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[27].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[27].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[28].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[28].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for project ABC', + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[29].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[29].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'public', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[30].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[30].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[31].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[31].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[32].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[32].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[33].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[33].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[34].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[34].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: 'Internal repository for team collaboration', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://internal.example.com', + }); + }); + + it(`Should ${examples[35].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[35].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[36].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[36].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: true, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[37].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[37].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[38].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[38].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[39].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[39].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[40].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[40].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for web application project', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://webapp.example.com', + }); + }); + + it(`Should ${examples[41].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[41].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'pull_request_description', + allow_merge_commit: false, + allow_rebase_merge: false, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[42].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[42].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: false, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[43].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[43].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[44].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[44].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[45].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[45].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'public', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[46].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[46].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: false, + description: 'Repository for backend service', + has_projects: undefined, + has_wiki: false, + homepage: undefined, + }); + }); + + it(`Should ${examples[47].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[47].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[48].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[48].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[49].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[49].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[50].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[50].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: true, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[51].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[51].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://example.com', + }); + }); + + it(`Should ${examples[52].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[52].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for microservice development', + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[53].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[53].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[54].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[54].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: true, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[55].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[55].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[56].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[56].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts index a58da1ee29..1c1f1ca5d2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts @@ -63,4 +63,932 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Set repository homepage.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with homepage', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + homepage: 'https://example.com', + }, + }, + ], + }), + }, + { + description: 'Create a private repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + }, + }, + ], + }), + }, + { + description: 'Enable required code owner reviews.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required code owner reviews', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireCodeOwnerReviews: true, + }, + }, + ], + }), + }, + { + description: 'Set required approving review count to 2.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required approving review count', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: 'Allow squash merge only.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only squash merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowSquashMerge: true, + allowRebaseMerge: false, + }, + }, + ], + }), + }, + { + description: 'Set squash merge commit title to pull request title.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with squash merge commit title set to pull request title', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + squashMergeCommitTitle: 'pull_request_title', + }, + }, + ], + }), + }, + { + description: 'Set squash merge commit message to blank.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with squash merge commit message set to blank', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + squashMergeCommitMessage: 'blank', + }, + }, + ], + }), + }, + { + description: 'Allow auto-merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing auto-merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowAutoMerge: true, + }, + }, + ], + }), + }, + { + description: 'Set collaborators with push access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with collaborators having push access', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + collaborators: [ + { username: 'user1', permission: 'push' }, + { username: 'user2', permission: 'push' }, + ], + }, + }, + ], + }), + }, + { + description: 'Add topics to repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['devops', 'kubernetes', 'ci-cd'], + }, + }, + ], + }), + }, + { + description: 'Add secret variables to repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'SECRET_KEY', value: 'supersecretkey' }, + { name: 'API_TOKEN', value: 'tokenvalue' }, + ], + }, + }, + ], + }), + }, + { + description: 'Enable branch protection requiring status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/circleci: build'], + }, + }, + ], + }), + }, + { + description: 'Require branches to be up-to-date before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring branches to be up-to-date before merging', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: 'Require conversation resolution before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring conversation resolution before merging', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + }, + }, + ], + }), + }, + { + description: 'Delete branch on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch deletion on merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + deleteBranchOnMerge: true, + }, + }, + ], + }), + }, + { + description: 'Customize OIDC token.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC token customization', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://github.com', + }, + }, + }, + ], + }), + }, + { + description: 'Require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Set multiple properties including description, homepage, and visibility.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple properties', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'A repository for project XYZ', + homepage: 'https://project-xyz.com', + repoVisibility: 'internal', + }, + }, + ], + }), + }, + { + description: 'Configure branch protection with multiple settings.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection settings', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: [ + 'ci/circleci: build', + 'ci/circleci: test', + ], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: true, + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: + 'Set repository access to private and add collaborators with admin access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository with collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + collaborators: [ + { username: 'admin1', permission: 'admin' }, + { username: 'admin2', permission: 'admin' }, + ], + }, + }, + ], + }), + }, + { + description: 'Enable GitHub Projects for the repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with GitHub Projects enabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + hasProjects: true, + }, + }, + ], + }), + }, + { + description: + 'Disable merge commits and allow only rebase and squash merges.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only rebase and squash merges', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowRebaseMerge: true, + allowSquashMerge: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository access to internal with no projects and issues.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository without projects and issues', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + hasProjects: false, + hasIssues: false, + }, + }, + ], + }), + }, + { + description: + 'Create repository with OIDC customization for specific audience.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization for specific audience', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://specific-audience.com', + }, + }, + }, + ], + }), + }, + { + description: 'Require all branches to be up-to-date before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring all branches to be up-to-date', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: 'Set description and topics for the repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for project ABC', + topics: ['python', 'machine-learning', 'data-science'], + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to public and enable commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new public GitHub repository with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'public', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with collaborators and default branch protection.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with collaborators and branch protection', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + collaborators: [ + { username: 'contributor1', permission: 'write' }, + { username: 'contributor2', permission: 'write' }, + ], + requiredStatusCheckContexts: ['ci/travis: build'], + }, + }, + ], + }), + }, + { + description: 'Add multiple secret variables.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'SECRET_KEY_1', value: 'value1' }, + { name: 'SECRET_KEY_2', value: 'value2' }, + ], + }, + }, + ], + }), + }, + { + description: 'Require a minimum of 2 approving reviews for merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with 2 required approving reviews', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection with conversation resolution required.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection and conversation resolution required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to internal with description and homepage.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository with description and homepage', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + description: 'Internal repository for team collaboration', + homepage: 'https://internal.example.com', + }, + }, + ], + }), + }, + { + description: 'Disable auto-merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with auto-merge disabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowAutoMerge: false, + }, + }, + ], + }), + }, + { + description: 'Set repository topics and enable GitHub Projects.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with topics and GitHub Projects enabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['opensource', 'nodejs', 'api'], + hasProjects: true, + }, + }, + ], + }), + }, + { + description: + 'Create a private repository with collaborators having admin and write access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository with multiple collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + collaborators: [ + { username: 'admin1', permission: 'admin' }, + { username: 'writer1', permission: 'write' }, + ], + }, + }, + ], + }), + }, + { + description: 'Disable branch deletion on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch deletion on merge disabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + deleteBranchOnMerge: false, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to internal and enable commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create repository with description, homepage, and required status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description, homepage, and status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for web application project', + homepage: 'https://webapp.example.com', + requiredStatusCheckContexts: [ + 'ci/travis: build', + 'ci/travis: lint', + ], + }, + }, + ], + }), + }, + { + description: + 'Enable squash merges only and set commit message to pull request description.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only squash merges with commit message set to pull request description', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowSquashMerge: true, + allowRebaseMerge: false, + squashMergeCommitMessage: 'pull_request_description', + }, + }, + ], + }), + }, + { + description: 'Enable rebase merges only and require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only rebase merges with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowRebaseMerge: true, + allowSquashMerge: false, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create repository with OIDC customization for multiple audiences.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization for multiple audiences', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: ['https://audience1.com', 'https://audience2.com'], + }, + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection with required approving reviews and status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring approving reviews and status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + requiredStatusCheckContexts: [ + 'ci/circleci: build', + 'ci/circleci: test', + ], + }, + }, + ], + }), + }, + { + description: 'Create a public repository with topics and secret variables.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new public GitHub repository with topics and secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'public', + topics: ['javascript', 'react', 'frontend'], + secrets: [ + { name: 'API_KEY', value: 'apikeyvalue' }, + { name: 'DB_PASSWORD', value: 'dbpasswordvalue' }, + ], + }, + }, + ], + }), + }, + { + description: 'Set repository description and disable issues and wiki.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description, and disable issues and wiki', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for backend service', + hasIssues: false, + hasWiki: false, + }, + }, + ], + }), + }, + { + description: 'Enable required conversation resolution and commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required conversation resolution and commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to private and require branches to be up-to-date.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository requiring branches to be up-to-date', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with default settings and add multiple topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with default settings and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['devops', 'ci-cd', 'automation'], + }, + }, + ], + }), + }, + { + description: + 'Disable merge commits, enable auto-merge, and require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository disabling merge commits, enabling auto-merge, and requiring commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowAutoMerge: true, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with homepage, collaborators, and topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with homepage, collaborators, and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + homepage: 'https://example.com', + collaborators: [ + { username: 'user1', permission: 'push' }, + { username: 'user2', permission: 'admin' }, + ], + topics: ['opensource', 'contribution'], + }, + }, + ], + }), + }, + { + description: 'Create a repository with branch protection and description.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection and description', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/travis: build'], + requiredApprovingReviewCount: 1, + description: 'Repository for microservice development', + }, + }, + ], + }), + }, + { + description: 'Create a repository with OIDC customization and topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://api.example.com', + }, + topics: ['api', 'security'], + }, + }, + ], + }), + }, + { + description: + 'Enable required code owner reviews and branch deletion on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required code owner reviews and branch deletion on merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireCodeOwnerReviews: true, + deleteBranchOnMerge: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with multiple secret variables and collaborators.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple secret variables and collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'API_SECRET', value: 'secretvalue' }, + { name: 'DB_USER', value: 'dbuser' }, + ], + collaborators: [ + { username: 'dev1', permission: 'write' }, + { username: 'dev2', permission: 'push' }, + ], + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection requiring status checks and conversation resolution.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring status checks and conversation resolution', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/build'], + requiredConversationResolution: true, + }, + }, + ], + }), + }, ]; From a65ccea1ffdc13387b9ac54ad2a54ad36d11924c Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 30 Jul 2024 09:35:44 +0200 Subject: [PATCH 066/204] Update flat-papayas-push.md Signed-off-by: Ben Lambert --- .changeset/flat-papayas-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/flat-papayas-push.md b/.changeset/flat-papayas-push.md index 191dad8550..f9e20613fd 100644 --- a/.changeset/flat-papayas-push.md +++ b/.changeset/flat-papayas-push.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend-module-azure': minor +'@backstage/plugin-scaffolder-backend-module-azure': patch --- Added examples for publish:azure action and updated its test cases From c544f811b83af6693f4b0078d525c089c7904bce Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Tue, 30 Jul 2024 11:14:29 +0300 Subject: [PATCH 067/204] feat: add support for status filtering in scaffolder endpoint Signed-off-by: Heikki Hellgren --- .changeset/eighty-jokes-deny.md | 6 +++++ plugins/scaffolder-backend/api-report.md | 4 +-- .../tasks/DatabaseTaskStore.test.ts | 27 +++++++++++++++++++ .../src/scaffolder/tasks/DatabaseTaskStore.ts | 15 +++++++---- .../src/scaffolder/tasks/StorageTaskBroker.ts | 9 +++++-- .../src/scaffolder/tasks/types.ts | 23 +++++++++------- .../src/service/router.test.ts | 3 ++- .../scaffolder-backend/src/service/router.ts | 18 ++++++++++--- plugins/scaffolder-node/api-report.md | 2 +- plugins/scaffolder-node/src/tasks/types.ts | 5 +++- 10 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 .changeset/eighty-jokes-deny.md diff --git a/.changeset/eighty-jokes-deny.md b/.changeset/eighty-jokes-deny.md new file mode 100644 index 0000000000..58944252f4 --- /dev/null +++ b/.changeset/eighty-jokes-deny.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Add support for status filtering in scaffolder tasks endpoint diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index a3beee2b53..2b0661ab59 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -427,7 +427,7 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: { createdBy?: string }): Promise<{ + list(options: { createdBy?: string; status?: TaskStatus_2 }): Promise<{ tasks: SerializedTask_2[]; }>; // (undocumented) @@ -646,7 +646,7 @@ export interface TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list?(options: { createdBy?: string }): Promise<{ + list?(options: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts index f4fae0715b..78a362b5c7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts @@ -95,6 +95,33 @@ describe('DatabaseTaskStore', () => { expect(tasks[0].id).toBeDefined(); }); + it('should list filtered created tasks by status', async () => { + const { store } = await createStore(); + + const { taskId } = await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'me', + }); + + await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'him', + }); + + const message = `This task was marked as stale as it exceeded its timeout`; + await store.completeTask({ + taskId, + status: 'cancelled', + eventBody: { message }, + }); + + const { tasks } = await store.list({ status: 'open' }); + expect(tasks.length).toBe(1); + expect(tasks[0].createdBy).toBe('him'); + expect(tasks[0].status).toBe('open'); + expect(tasks[0].id).toBeDefined(); + }); + it('should sent an event to start cancelling the task', async () => { const { store } = await createStore(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index fe62572af9..ba675b2cc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -22,19 +22,19 @@ import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { TaskStore, - TaskStoreEmitOptions, - TaskStoreListEventsOptions, TaskStoreCreateTaskOptions, TaskStoreCreateTaskResult, - TaskStoreShutDownTaskOptions, + TaskStoreEmitOptions, + TaskStoreListEventsOptions, TaskStoreRecoverTaskOptions, + TaskStoreShutDownTaskOptions, } from './types'; import { - SerializedTaskEvent, SerializedTask, - TaskStatus, + SerializedTaskEvent, TaskEventType, TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { DateTime, Duration } from 'luxon'; import { TaskRecovery, TaskSpec } from '@backstage/plugin-scaffolder-common'; @@ -182,6 +182,7 @@ export class DatabaseTaskStore implements TaskStore { async list(options: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { const queryBuilder = this.db('tasks'); @@ -191,6 +192,10 @@ export class DatabaseTaskStore implements TaskStore { }); } + if (options.status) { + queryBuilder.where({ status: options.status }); + } + const results = await queryBuilder.orderBy('created_at', 'desc').select(); const tasks = results.map(result => ({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index f4946e6652..d17fb33b3b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -20,13 +20,14 @@ import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; import { - TaskSecrets, SerializedTask, SerializedTaskEvent, TaskBroker, TaskBrokerDispatchOptions, TaskCompletionState, TaskContext, + TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { InternalTaskSecrets, TaskStore } from './types'; import { readDuration } from './helper'; @@ -279,13 +280,17 @@ export class StorageTaskBroker implements TaskBroker { async list(options?: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { if (!this.storage.list) { throw new Error( 'TaskStore does not implement the list method. Please implement the list method to be able to list tasks', ); } - return await this.storage.list({ createdBy: options?.createdBy }); + return await this.storage.list({ + createdBy: options?.createdBy, + status: options?.status, + }); } private deferredDispatch = defer(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 0e0ebd706b..89bcce3cc9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { JsonValue, JsonObject, HumanDuration } from '@backstage/types'; +import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { - TemplateAction, - TaskStatus as _TaskStatus, - TaskCompletionState as _TaskCompletionState, SerializedTask as _SerializedTask, - TaskEventType as _TaskEventType, SerializedTaskEvent as _SerializedTaskEvent, - TaskBrokerDispatchResult as _TaskBrokerDispatchResult, - TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, - TaskContext as _TaskContext, TaskBroker as _TaskBroker, + TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, + TaskBrokerDispatchResult as _TaskBrokerDispatchResult, + TaskCompletionState as _TaskCompletionState, + TaskContext as _TaskContext, + TaskEventType as _TaskEventType, + TaskSecrets, + TaskStatus as _TaskStatus, + TemplateAction, } from '@backstage/plugin-scaffolder-node'; /** @@ -190,7 +190,10 @@ export interface TaskStore { tasks: { taskId: string }[]; }>; - list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; emitLogEvent(options: TaskStoreEmitOptions): Promise; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index af06fc4034..01a2fba6e8 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -432,10 +432,11 @@ describe('createRouter', () => { }); const response = await request(app).get( - `/v2/tasks?createdBy=user:default/foo`, + `/v2/tasks?createdBy=user:default/foo&status=completed`, ); expect(taskBroker.list).toHaveBeenCalledWith({ createdBy: 'user:default/foo', + status: 'completed', }); expect(response.status).toEqual(200); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 769a042a78..a25731ce45 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,10 +15,10 @@ */ import { + createLegacyAuthAdapters, HostDiscovery, PluginDatabaseManager, UrlReader, - createLegacyAuthAdapters, } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; @@ -35,22 +35,22 @@ import { ScmIntegrations } from '@backstage/integration'; import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, + TemplateEntityStepV1beta3, TemplateEntityV1beta3, templateEntityV1beta3Validator, TemplateParametersV1beta3, - TemplateEntityStepV1beta3, } from '@backstage/plugin-scaffolder-common'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, scaffolderActionPermissions, + scaffolderTaskPermissions, scaffolderTemplatePermissions, taskCancelPermission, taskCreatePermission, taskReadPermission, templateParameterReadPermission, templateStepReadPermission, - scaffolderTaskPermissions, } from '@backstage/plugin-scaffolder-common/alpha'; import express from 'express'; import Router from 'express-promise-router'; @@ -58,8 +58,9 @@ import { validate } from 'jsonschema'; import { Logger } from 'winston'; import { z } from 'zod'; import { - TemplateAction, TaskBroker, + TaskStatus, + TemplateAction, TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; @@ -587,8 +588,17 @@ export async function createRouter( ); } + const [statusQuery] = [req.query.status].flat(); + if ( + typeof statusQuery !== 'string' && + typeof statusQuery !== 'undefined' + ) { + throw new InputError('status query parameter must be a string'); + } + const tasks = await taskBroker.list({ createdBy: userEntityRef, + status: statusQuery ? (statusQuery as TaskStatus) : undefined, }); res.status(200).json(tasks); diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 66e8ccd678..515a169cb2 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -315,7 +315,7 @@ export interface TaskBroker { // (undocumented) get(taskId: string): Promise; // (undocumented) - list?(options?: { createdBy?: string }): Promise<{ + list?(options?: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index a2adb97b9c..8324e352f2 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -180,5 +180,8 @@ export interface TaskBroker { get(taskId: string): Promise; - list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options?: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; } From 9d6ad1eeb628f93029a1a97510068361490c5a8b Mon Sep 17 00:00:00 2001 From: "Nolan, Tavi" Date: Tue, 30 Jul 2024 14:26:33 +0100 Subject: [PATCH 068/204] Update mkdocs.yml file path Signed-off-by: Nolan, Tavi --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 707929d132..38b03fbbea 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,7 +64,7 @@ nav: - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' - Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md' - - Dry Run Testing: 'feature/software-templates/dry-run-testing.md' + - Dry Run Testing: 'features/software-templates/dry-run-testing.md' - Backstage Search: - Overview: 'features/search/README.md' - Getting Started: 'features/search/getting-started.md' From dd0ce5a890307a7b4092757e10a9665bb8f7f8c7 Mon Sep 17 00:00:00 2001 From: Alex Lorenzi Date: Tue, 30 Jul 2024 14:52:09 -0400 Subject: [PATCH 069/204] The header size calculation weren't taking into account cases where the value was a CSS variable. This could lead to situation where the outputted property was invalid. This update resolves the CSS property and then tries to calculate again using that value Signed-off-by: Alex Lorenzi --- .../transformers/styles/rules/typeset.ts | 30 ++-- .../transformers/styles/transformer.test.ts | 48 ------- .../transformers/styles/transformer.test.tsx | 135 ++++++++++++++++++ 3 files changed, 156 insertions(+), 57 deletions(-) delete mode 100644 plugins/techdocs/src/reader/transformers/styles/transformer.test.ts create mode 100644 plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts index cc7ba6bace..c682008a4d 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts @@ -30,6 +30,8 @@ type TypographyHeadings = Pick< type TypographyHeadingsKeys = keyof TypographyHeadings; const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; +const relativeLengthUnit: RegExp = /(em)|(rem)/gi; +const cssVariable: RegExp = /var\(|\)/gi; export default ({ theme }: RuleOptions) => ` /*================== Typeset ==================*/ @@ -43,17 +45,27 @@ ${headings.reduce((style, heading) => { (theme.typography as BackstageTypography).htmlFontSize ?? 16; const styles = theme.typography[heading]; const { lineHeight, fontFamily, fontWeight, fontSize } = styles; - const calculate = (value: typeof fontSize) => { - let factor: number | string = 1; + const calculate = (value: typeof fontSize): string | undefined => { if (typeof value === 'number') { - // convert px to rem - // 60% of the size defined because it is too big - factor = (value / htmlFontSize) * 0.6; + // Convert px to rem and apply 60% factor + return calculate(`${(value / htmlFontSize) * 0.6}rem`); + } else if (typeof value === 'string') { + if (value.match(cssVariable)) { + // Resolve css variable and calculate recursively + const resolvedValue = window + .getComputedStyle(document.body) + .getPropertyValue(value.replaceAll(cssVariable, '')); + if (resolvedValue !== '') { + return calculate(resolvedValue); + } + } else if (value.match(relativeLengthUnit)) { + // Use relative size as factor + const factor = value.replace(relativeLengthUnit, ''); + return `calc(${factor} * var(--md-typeset-font-size))`; + } } - if (typeof value === 'string') { - factor = value.replace('rem', ''); - } - return `calc(${factor} * var(--md-typeset-font-size))`; + // Value is not a number, relative length unit, or CSS variable, return as is + return value; }; return style.concat(` .md-typeset ${heading} { diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts b/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts deleted file mode 100644 index 90244e96ad..0000000000 --- a/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2022 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 { renderHook } from '@testing-library/react'; -import { useStylesTransformer } from './transformer'; - -describe('Transformers > Styles', () => { - it('should return a function that injects all styles into a given dom element', () => { - const { result } = renderHook(() => useStylesTransformer()); - - const dom = document.createElement('html'); - dom.innerHTML = ''; - result.current(dom); // calling styles transformer - - const style = dom.querySelector('head > style'); - expect(style).toHaveTextContent( - '/*================== Variables ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Reset ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Layout ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Typeset ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Animations ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Extensions ==================*/', - ); - }); -}); diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx new file mode 100644 index 0000000000..464bb834ea --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2022 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 { renderHook } from '@testing-library/react'; +import { useStylesTransformer } from './transformer'; +import { createTheme, ThemeProvider } from '@material-ui/core/styles'; +import React from 'react'; + +describe('Transformers > Styles', () => { + it('should return a function that injects all styles into a given dom element', () => { + const { result } = renderHook(() => useStylesTransformer()); + + const dom = document.createElement('html'); + dom.innerHTML = ''; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + + expect(style).toHaveTextContent( + '/*================== Variables ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Reset ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Layout ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Typeset ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Animations ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Extensions ==================*/', + ); + }); + + it('should use relative header sizes as the factor the md-typeset variable', () => { + const theme = createTheme({ + typography: { + h1: { + fontSize: '20rem', + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + 'font-size: calc(20 * var(--md-typeset-font-size));', + ); + }); + + it('should resolve header sizes that are variables', () => { + document.body.style.setProperty('--font-size-h1', '20rem'); + const theme = createTheme({ + typography: { + h1: { + fontSize: 'var(--font-size-h1)', + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + 'font-size: calc(20 * var(--md-typeset-font-size));', + ); + }); + + it('should convert pixel header sizes to REM and reduce by 60%', () => { + const theme = createTheme({ + typography: { + htmlFontSize: 16, + h1: { + fontSize: 100, + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + // 100px / 16px * 0.6 = 3.75rem + 'font-size: calc(3.75 * var(--md-typeset-font-size));', + ); + }); +}); From bdc547132ac0133cc9711105be8fbd7b2306fa0c Mon Sep 17 00:00:00 2001 From: Alex Lorenzi Date: Tue, 30 Jul 2024 15:03:31 -0400 Subject: [PATCH 070/204] Added changeset Signed-off-by: Alex Lorenzi --- .changeset/wise-spiders-walk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-spiders-walk.md diff --git a/.changeset/wise-spiders-walk.md b/.changeset/wise-spiders-walk.md new file mode 100644 index 0000000000..65793041c5 --- /dev/null +++ b/.changeset/wise-spiders-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. From a16632cf740a6180a62183d1544b57142ae88ec0 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Wed, 31 Jul 2024 00:01:45 +0200 Subject: [PATCH 071/204] fix(techdocs-backend): update config.d.ts Signed-off-by: Thomas Cardonne --- .changeset/shaggy-mugs-return.md | 5 +++++ plugins/techdocs-backend/config.d.ts | 32 +++++++++++++--------------- 2 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 .changeset/shaggy-mugs-return.md diff --git a/.changeset/shaggy-mugs-return.md b/.changeset/shaggy-mugs-return.md new file mode 100644 index 0000000000..6af5d1d18f --- /dev/null +++ b/.changeset/shaggy-mugs-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Update configuration schema to match actual behavior diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 9d1cd9e170..e22fc549d9 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -68,22 +68,14 @@ export interface Config { /** * Techdocs publisher information */ - publisher?: - | { - type: - | 'local' - | 'googleGcs' - | 'awsS3' - | 'azureBlobStorage' - | 'openStackSwift'; - - local?: { - /** - * Directory to store generated static files. - */ - publishDirectory?: string; - }; - } + publisher?: { + local?: { + /** + * Directory to store generated static files. + */ + publishDirectory?: string; + }; + } & ( | { type: 'awsS3'; @@ -132,6 +124,11 @@ export interface Config { * (Required) Cloud Storage Bucket Name */ bucketName: string; + /** + * (Optional) Location in storage bucket to save files + * If not set, the default location will be the root of the storage bucket + */ + bucketRootPath?: string; /** * (Optional) AWS Region. * If not set, AWS_REGION environment variable or aws config file will be used. @@ -261,7 +258,8 @@ export interface Config { */ projectId?: string; }; - }; + } + ); /** * @example http://localhost:7007/api/techdocs From 3d9891d2242f3a09bf113a0250d979c379a48c08 Mon Sep 17 00:00:00 2001 From: Alex Lorenzi <671432+alexlorenzi@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:01:37 -0400 Subject: [PATCH 072/204] Update plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx Signed-off-by: Alex Lorenzi <671432+alexlorenzi@users.noreply.github.com> --- .../src/reader/transformers/styles/transformer.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx index 464bb834ea..b55e372212 100644 --- a/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx @@ -49,7 +49,7 @@ describe('Transformers > Styles', () => { ); }); - it('should use relative header sizes as the factor the md-typeset variable', () => { + it('should use headers relative font-size value as the factor for the md-typeset variable', () => { const theme = createTheme({ typography: { h1: { From 83faf24b3367b5daf84d47c374c7dcae85ab5071 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Wed, 31 Jul 2024 07:46:33 +0300 Subject: [PATCH 073/204] feat: add possibility to allow and deny specific email addresses Notification email processor config to support allowing and denying specific email addresses who can receive notifications in email. This is required to be able to test this in test environments with real production data and not send unnecessary notifications. Signed-off-by: Heikki Hellgren --- .changeset/witty-queens-run.md | 5 ++++ .../config.d.ts | 8 ++++++ .../processor/NotificationsEmailProcessor.ts | 27 +++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .changeset/witty-queens-run.md diff --git a/.changeset/witty-queens-run.md b/.changeset/witty-queens-run.md new file mode 100644 index 0000000000..2d869caa91 --- /dev/null +++ b/.changeset/witty-queens-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +Notification email processor supports allowing or denying specific email addresses from receiving notifications diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index 92f3385d1b..e4ce5ede53 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -132,6 +132,14 @@ export interface Config { */ excludedTopics?: string[]; }; + /** + * White list of addresses to send email to + */ + allowlistEmailAddresses?: string[]; + /** + * Black list of addresses to not send email to + */ + denylistEmailAddresses?: string[]; }; }; }; diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index 3bfccfcc90..f3d9f59b8c 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -56,6 +56,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private readonly throttleInterval: number; private readonly frontendBaseUrl: string; private readonly filter: NotificationProcessorFilters; + private readonly allowlistEmailAddresses?: string[]; + private readonly denylistEmailAddresses?: string[]; constructor( private readonly logger: LoggerService, @@ -85,6 +87,12 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ? durationToMilliseconds(readDurationFromConfig(cacheConfig)) : 3_600_000; this.frontendBaseUrl = config.getString('app.baseUrl'); + this.allowlistEmailAddresses = emailProcessorConfig.getOptionalStringArray( + 'allowlistEmailAddresses', + ); + this.denylistEmailAddresses = emailProcessorConfig.getOptionalStringArray( + 'denylistEmailAddresses', + ); this.filter = getProcessorFiltersFromConfig(emailProcessorConfig); } @@ -197,10 +205,25 @@ export class NotificationsEmailProcessor implements NotificationProcessor { notification: Notification, options: NotificationSendOptions, ) { + let emails: string[]; if (options.recipients.type === 'broadcast' || notification.user === null) { - return await this.getBroadcastEmails(); + emails = await this.getBroadcastEmails(); + } else { + emails = await this.getUserEmail(notification.user); } - return await this.getUserEmail(notification.user); + + if (this.allowlistEmailAddresses) { + emails = emails.filter(email => + this.allowlistEmailAddresses?.includes(email), + ); + } + + if (this.denylistEmailAddresses) { + emails = emails.filter( + email => !this.denylistEmailAddresses?.includes(email), + ); + } + return emails; } private async sendMail(options: Mail.Options) { From 80a0737fac6d0540ee7715b87726393de891e19b Mon Sep 17 00:00:00 2001 From: djamaile Date: Wed, 31 Jul 2024 14:25:16 +0200 Subject: [PATCH 074/204] chore: add packages config to backend-app-api Signed-off-by: djamaile --- .changeset/tricky-ducks-juggle.md | 5 +++++ packages/backend-app-api/config.d.ts | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/tricky-ducks-juggle.md diff --git a/.changeset/tricky-ducks-juggle.md b/.changeset/tricky-ducks-juggle.md new file mode 100644 index 0000000000..7f91c978a2 --- /dev/null +++ b/.changeset/tricky-ducks-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Add `packages` to backend-app-api configuration schema diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index a23bca447b..966ca61ae0 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -287,6 +287,11 @@ export interface Config { } >; }; + /** + * @visibility frontend + * @deepVisibility frontend + */ + packages?: 'all' | { include?: string[]; exclude?: string[] }; }; /** Discovery options. */ From 87c872030031eaebb1465454596165172ff47298 Mon Sep 17 00:00:00 2001 From: Djam Date: Wed, 31 Jul 2024 14:33:37 +0200 Subject: [PATCH 075/204] fix: remove frontend visibility Signed-off-by: Djam --- packages/backend-app-api/config.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index 966ca61ae0..e94da62b77 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -287,10 +287,6 @@ export interface Config { } >; }; - /** - * @visibility frontend - * @deepVisibility frontend - */ packages?: 'all' | { include?: string[]; exclude?: string[] }; }; From 592e742f9b03d9cdf9d0b940f2fd47e9fdc5a496 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 31 Jul 2024 14:34:51 +0200 Subject: [PATCH 076/204] Update v1.30.0-next.1-changelog.md Fixing changelog Signed-off-by: Ben Lambert --- docs/releases/v1.30.0-next.1-changelog.md | 1891 +-------------------- 1 file changed, 1 insertion(+), 1890 deletions(-) diff --git a/docs/releases/v1.30.0-next.1-changelog.md b/docs/releases/v1.30.0-next.1-changelog.md index 2f7153fe5c..efd746686b 100644 --- a/docs/releases/v1.30.0-next.1-changelog.md +++ b/docs/releases/v1.30.0-next.1-changelog.md @@ -4,10 +4,6 @@ Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.1](h ## @backstage/core-app-api@1.14.2-next.0 -# @backstage/core-app-api - -## 1.14.1-next.0 - ### Patch Changes - 9a46a81: The request to delete the session cookie when running the app in protected mode is now done with a plain `fetch` rather than `FetchApi`. This fixes a bug where the app would immediately try to sign-in again when removing the cookie during logout. @@ -16,1892 +12,7 @@ Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.1](h - @backstage/core-plugin-api@1.9.3 - @backstage/types@1.1.1 - @backstage/version-bridge@1.0.8 - -## 1.14.0 - -### Minor Changes - -- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. - - ```yaml - app: - routes: - bindings: - # This has the effect of removing the button for registering new - # catalog entities in the scaffolder template list view - scaffolder.registerComponent: false - ``` - -### Patch Changes - -- db2e2d5: Updated config schema to support app.routes.bindings -- Updated dependencies - - @backstage/config@1.2.0 - - @backstage/core-plugin-api@1.9.3 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.13.1-next.1 - -### Patch Changes - -- db2e2d5: Updated config schema to support app.routes.bindings -- Updated dependencies - - @backstage/core-plugin-api@1.9.3 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.13.0-next.0 - -### Minor Changes - -- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. - - ```yaml - app: - routes: - bindings: - # This has the effect of removing the button for registering new - # catalog entities in the scaffolder template list view - scaffolder.registerComponent: false - ``` - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.9.3 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.12.6 - -### Patch Changes - -- 35fbe09: Added support for configuration of route bindings through static configuration, and default targets for external route refs. - - In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example: - - ```yaml - app: - routes: - bindings: - catalog.createComponent: catalog-import.importPage - ``` - - Each key in the route binding object is of the form `.`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead. - - The equivalent of the above configuration in code is the following: - - ```ts - const app = createApp({ - // ... - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: catalogImportPlugin.routes.importPage, - }); - }, - }); - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.9.3 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.12.6-next.0 - -### Patch Changes - -- 35fbe09: Added support for configuration of route bindings through static configuration, and default targets for external route refs. - - In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example: - - ```yaml - app: - routes: - bindings: - catalog.createComponent: catalog-import.importPage - ``` - - Each key in the route binding object is of the form `.`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead. - - The equivalent of the above configuration in code is the following: - - ```ts - const app = createApp({ - // ... - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: catalogImportPlugin.routes.importPage, - }); - }, - }); - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.9.3-next.0 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.12.5 - -### Patch Changes - -- 1bed9a3: The Backstage identity session expiration check will no longer fall back to using the provider expiration. This was introduced to smooth out the rollout of Backstage release 1.18, and is no longer needed. - -## 1.12.4 - -### Patch Changes - -- c884b9a: The app is now aware of if it is being served from the `app-backend` with a separate public and protected bundles. When in protected mode the app will now continuously refresh the session cookie, as well as clear the cookie if the user signs out. -- abfbcfc: Updated dependency `@testing-library/react` to `^15.0.0`. -- cb1e3b0: Updated dependency `@testing-library/dom` to `^10.0.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.9.2 - - @backstage/version-bridge@1.0.8 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - -## 1.12.4-next.0 - -### Patch Changes - -- c884b9a: The app is now aware of if it is being served from the `app-backend` with a separate public and protected bundles. When in protected mode the app will now continuously refresh the session cookie, as well as clear the cookie if the user signs out. -- Updated dependencies - - @backstage/config@1.2.0 - - @backstage/core-plugin-api@1.9.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.3 - -### Patch Changes - -- e8f026a: Use ESM exports of react-use library -- Updated dependencies - - @backstage/core-plugin-api@1.9.1 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.2 - -### Patch Changes - -- e8f026a: Use ESM exports of react-use library -- Updated dependencies - - @backstage/core-plugin-api@1.9.1 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.2.0 - - @backstage/core-plugin-api@1.9.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.1-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.2.0-next.1 - - @backstage/core-plugin-api@1.9.1-next.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.1.2-next.0 - - @backstage/core-plugin-api@1.9.1-next.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.0 - -### Minor Changes - -- f919be9: Added a utility API for VMware Cloud auth; the API ref is available in the - `@backstage/core-plugin-api` and `@backstage/frontend-plugin-api` packages, the - implementation is in `@backstage/core-app-api` and a factory has been added to - `@backstage/app-defaults`. - -### Patch Changes - -- 9aac2b0: Use `--cwd` as the first `yarn` argument -- 8fe56a8: Widen `@types/react` dependency range to include version 18. -- 7da67ce: Change `defaultScopes` for Bitbucket auth from invalid `team` to `account`. -- Updated dependencies - - @backstage/core-plugin-api@1.9.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.0-next.1 - -### Minor Changes - -- f919be9: Added a utility API for VMware Cloud auth; the API ref is available in the - `@backstage/core-plugin-api` and `@backstage/frontend-plugin-api` packages, the - implementation is in `@backstage/core-app-api` and a factory has been added to - `@backstage/app-defaults`. - -### Patch Changes - -- 9aac2b0: Use `--cwd` as the first `yarn` argument -- 8fe56a8: Widen `@types/react` dependency range to include version 18. -- Updated dependencies - - @backstage/core-plugin-api@1.9.0-next.1 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.4-next.0 - -### Patch Changes - -- 7da67ce: Change `defaultScopes` for Bitbucket auth from invalid `team` to `account`. -- Updated dependencies - - @backstage/core-plugin-api@1.8.3-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.3 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.2 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.3-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.2-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.2 - -### Patch Changes - -- 3e358b0: Added deprecation warning for React Router v6 beta, please make sure you have migrated your apps to use React Router v6 stable as support for the beta version will be removed. See the [migration tutorial](https://backstage.io/docs/tutorials/react-router-stable-migration) for more information. -- 0cbb03b: Fixing regular expression ReDoS with zod packages. Upgrading to latest. ref: -- Updated dependencies - - @backstage/core-plugin-api@1.8.1 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.2-next.1 - -### Patch Changes - -- 3e358b0dff: Added deprecation warning for React Router v6 beta, please make sure you have migrated your apps to use React Router v6 stable as support for the beta version will be removed. See the [migration tutorial](https://backstage.io/docs/tutorials/react-router-stable-migration) for more information. -- Updated dependencies - - @backstage/core-plugin-api@1.8.1-next.1 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.1-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.1 - -### Patch Changes - -- 6c2b872153: Add official support for React 18. -- 89d13e5618: Add current and default scopes when refreshing session -- 9ab0572217: Add component data `core.type` marker for `AppRouter` and `FlatRoutes`. -- Updated dependencies - - @backstage/core-plugin-api@1.8.0 - - @backstage/version-bridge@1.0.7 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - -## 1.11.1-next.0 - -### Patch Changes - -- 6c2b872153: Add official support for React 18. -- 89d13e5618: Add current and default scopes when refreshing session -- 9ab0572217: Add component data `core.type` marker for `AppRouter` and `FlatRoutes`. -- Updated dependencies - - @backstage/core-plugin-api@1.8.0-next.0 - - @backstage/version-bridge@1.0.7-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - -## 1.11.0 - -### Minor Changes - -- c9d9bfeca2: URL encode some well known unsafe characters in `RouteResolver` (and therefore `useRouteRef`) - -### Patch Changes - -- 29e4d8b76b: Fixed bug in `AppRouter` to determine the correct `signOutTargetUrl` if `app.baseUrl` contains a `basePath` -- acca17e91a: Wrap entire app in ``, enabling support for using translations outside plugins. -- 1a0616fa10: Add missing resource and template app icons -- 9a1fce352e: Updated dependency `@testing-library/jest-dom` to `^6.0.0`. -- f95af4e540: Updated dependency `@testing-library/dom` to `^9.0.0`. -- f1b349cfba: Fixed a bug in `TranslationApi` implementation where in some cases it wouldn't notify subscribers of language changes. -- Updated dependencies - - @backstage/core-plugin-api@1.7.0 - - @backstage/version-bridge@1.0.6 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - -## 1.11.0-next.2 - -### Minor Changes - -- c9d9bfeca2: URL encode some well known unsafe characters in `RouteResolver` (and therefore `useRouteRef`) - -### Patch Changes - -- acca17e91a: Wrap entire app in ``, enabling support for using translations outside plugins. -- Updated dependencies - - @backstage/core-plugin-api@1.7.0-next.1 - - @backstage/config@1.1.1-next.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.1-next.1 - -### Patch Changes - -- 1a0616fa10: Add missing resource and template app icons -- Updated dependencies - - @backstage/core-plugin-api@1.7.0-next.0 - - @backstage/config@1.1.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.7.0-next.0 - - @backstage/config@1.1.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.0 - -### Minor Changes - -- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. -- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. -- 6e30769cc627: Introduced experimental support for internationalization. - -### Patch Changes - -- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. -- 9fe827b380e1: Internal refactor -- 8cec7664e146: Removed `@types/node` dependency -- Updated dependencies - - @backstage/config@1.1.0 - - @backstage/core-plugin-api@1.6.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.0-next.3 - -### Patch Changes - -- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. -- 9fe827b380e1: Internal refactor -- Updated dependencies - - @backstage/config@1.1.0-next.2 - - @backstage/core-plugin-api@1.6.0-next.3 - - @backstage/types@1.1.1-next.0 - - @backstage/version-bridge@1.0.5-next.0 - -## 1.10.0-next.2 - -### Minor Changes - -- 6e30769cc627: Introduced experimental support for internationalization. - -### Patch Changes - -- 8cec7664e146: Removed `@types/node` dependency -- Updated dependencies - - @backstage/core-plugin-api@1.6.0-next.2 - - @backstage/config@1.1.0-next.1 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.10.0-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.1.0-next.0 - - @backstage/core-plugin-api@1.6.0-next.1 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.10.0-next.0 - -### Minor Changes - -- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. -- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.6.0-next.0 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.9.1 - -### Patch Changes - -- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension. -- Updated dependencies - - @backstage/core-plugin-api@1.5.3 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.9.1-next.0 - -### Patch Changes - -- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension. -- Updated dependencies - - @backstage/core-plugin-api@1.5.3 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.9.0 - -### Minor Changes - -- a77ddf7ccd71: add login in popup options to config popup width and height - -### Patch Changes - -- 8174cf4c0edf: Fixing MUI / Material UI references -- Updated dependencies - - @backstage/core-plugin-api@1.5.3 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.8.2-next.1 - -### Patch Changes - -- 8174cf4c0edf: Fixing MUI / Material UI references -- Updated dependencies - - @backstage/core-plugin-api@1.5.3-next.1 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.8.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.3-next.0 - - @backstage/config@1.0.8 - -## 1.8.1 - -### Patch Changes - -- 12adfbc8fe2d: Fixed a bug that prevented accurate plugin and route data from being applied to `navigate` analytics events when users visited pages constructed with ``, ``, and similar components that are used to gather one or more routable extensions under a given path. -- ac677bc30ae0: Expose discovery.endpoints configuration to use FrontendHostDiscovery -- 74b216ee4e50: Add `PropsWithChildren` to usages of `ComponentType`, in preparation for React 18 where the children are no longer implicit. -- Updated dependencies - - @backstage/core-plugin-api@1.5.2 - - @backstage/types@1.1.0 - - @backstage/config@1.0.8 - - @backstage/version-bridge@1.0.4 - -## 1.8.1-next.0 - -### Patch Changes - -- 74b216ee4e50: Add `PropsWithChildren` to usages of `ComponentType`, in preparation for React 18 where the children are no longer implicit. -- Updated dependencies - - @backstage/core-plugin-api@1.5.2-next.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4 - -## 1.8.0 - -### Minor Changes - -- c89437db899: The analytics' `navigate` event will now include the route parameters as attributes of the navigate event - -### Patch Changes - -- b645d70034a: Fixed a bug in the Azure auth provider which prevented getting access tokens with multiple scopes for one resource -- 42d817e76ab: Added `FrontendHostDiscovery` for config driven discovery implementation -- Updated dependencies - - @backstage/config@1.0.7 - - @backstage/core-plugin-api@1.5.1 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4 - -## 1.8.0-next.1 - -### Minor Changes - -- c89437db899: The analytics' `navigate` event will now include the route parameters as attributes of the navigate event - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.1 - - @backstage/config@1.0.7 - -## 1.7.1-next.0 - -### Patch Changes - -- 42d817e76ab: Added `FrontendHostDiscovery` for config driven discovery implementation -- Updated dependencies - - @backstage/core-plugin-api@1.5.1 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4 - -## 1.7.0 - -### Minor Changes - -- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window. -- c15e0cedbe1: The `AuthConnector` interface now supports specifying a set of scopes when - refreshing a session. The `DefaultAuthConnector` implementation passes the - `scope` query parameter to the auth-backend plugin appropriately. The - `RefreshingAuthSessionManager` passes any scopes in its `GetSessionRequest` - appropriately. - -### Patch Changes - -- 1e4f5e91b8e: Bump `zod` and `zod-to-json-schema` dependencies. -- e0c6e8b9c3c: Update peer dependencies -- Updated dependencies - - @backstage/core-plugin-api@1.5.1 - - @backstage/version-bridge@1.0.4 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - -## 1.7.0-next.3 - -### Minor Changes - -- c15e0cedbe1: The `AuthConnector` interface now supports specifying a set of scopes when - refreshing a session. The `DefaultAuthConnector` implementation passes the - `scope` query parameter to the auth-backend plugin appropriately. The - `RefreshingAuthSessionManager` passes any scopes in its `GetSessionRequest` - appropriately. - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.7 - - @backstage/core-plugin-api@1.5.1-next.1 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4-next.0 - -## 1.7.0-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.1-next.1 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4-next.0 - -## 1.7.0-next.1 - -### Patch Changes - -- 1e4f5e91b8e: Bump `zod` and `zod-to-json-schema` dependencies. -- e0c6e8b9c3c: Update peer dependencies -- Updated dependencies - - @backstage/core-plugin-api@1.5.1-next.0 - - @backstage/version-bridge@1.0.4-next.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - -## 1.7.0-next.0 - -### Minor Changes - -- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.6.0 - -### Minor Changes - -- 456eaa8cf83: `OAuth2` now gets ID tokens from a session with the `openid` scope explicitly - requested. - - This should not be considered a breaking change, because spec-compliant OIDC - providers will already be returning ID tokens if and only if the `openid` scope - is granted. - - This change makes the dependence explicit, and removes the burden on - OAuth2-based providers which require an ID token (e.g. this is done by various - default [auth handlers](https://backstage.io/docs/auth/identity-resolver/#authhandler)) to add - `openid` to their default scopes. _That_ could carry another indirect benefit: - by removing `openid` from the default scopes for a provider, grants for - resource-specific access tokens can avoid requesting excess ID token-related - scopes. - -### Patch Changes - -- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.5.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.6.0-next.2 - -### Minor Changes - -- 456eaa8cf83: `OAuth2` now gets ID tokens from a session with the `openid` scope explicitly - requested. - - This should not be considered a breaking change, because spec-compliant OIDC - providers will already be returning ID tokens if and only if the `openid` scope - is granted. - - This change makes the dependence explicit, and removes the burden on - OAuth2-based providers which require an ID token (e.g. this is done by various - default [auth handlers](https://backstage.io/docs/auth/identity-resolver/#authhandler)) to add - `openid` to their default scopes. _That_ could carry another indirect benefit: - by removing `openid` from the default scopes for a provider, grants for - resource-specific access tokens can avoid requesting excess ID token-related - scopes. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.0-next.2 - - @backstage/config@1.0.7-next.0 - -## 1.5.1-next.1 - -### Patch Changes - -- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.4.1-next.1 - - @backstage/config@1.0.7-next.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.5.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.4.1-next.0 - - @backstage/config@1.0.6 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.5.0 - -### Minor Changes - -- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults - -### Patch Changes - -- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. -- Updated dependencies - - @backstage/core-plugin-api@1.4.0 - - @backstage/config@1.0.6 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.4.1-next.0 - -### Patch Changes - -- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. -- Updated dependencies - - @backstage/config@1.0.6 - - @backstage/core-plugin-api@1.3.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.4.0 - -### Minor Changes - -- bca8e8b393: Allow defining application level feature flags. See [Feature Flags documentation](https://backstage.io/docs/plugins/feature-flags#in-the-application) for reference. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.3.0 - - @backstage/config@1.0.6 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.4.0-next.1 - -### Minor Changes - -- bca8e8b393: Allow defining application level feature flags. See [Feature Flags documentation](https://backstage.io/docs/plugins/feature-flags#in-the-application) for reference. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.3.0-next.1 - - @backstage/config@1.0.6-next.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.3.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.6-next.0 - - @backstage/core-plugin-api@1.2.1-next.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.3.0 - -### Minor Changes - -- e0d9c9559a: Added a new `AppRouter` component and `app.createRoot()` method that replaces `app.getRouter()` and `app.getProvider()`, which are now deprecated. The new `AppRouter` component is a drop-in replacement for the old router component, while the new `app.createRoot()` method is used instead of the old provider component. - - An old app setup might look like this: - - ```tsx - const app = createApp(/* ... */); - - const AppProvider = app.getProvider(); - const AppRouter = app.getRouter(); - - const routes = ...; - - const App = () => ( - - - - - {routes} - - - ); - - export default App; - ``` - - With these new APIs, the setup now looks like this: - - ```tsx - import { AppRouter } from '@backstage/core-app-api'; - - const app = createApp(/* ... */); - - const routes = ...; - - export default app.createRoot( - <> - - - - {routes} - - , - ); - ``` - - Note that `app.createRoot()` accepts a React element, rather than a component. - -### Patch Changes - -- d3fea4ae0a: Internal fixes to avoid implicit usage of globals -- b05dcd5530: Move the `zod` dependency to a version that does not collide with other libraries -- b4b5b02315: Tweak feature flag registration so that it happens immediately before the first rendering of the app, rather than just after. -- 6870b43dd1: Fix for the automatic rewriting of base URLs. -- 203271b746: Prevent duplicate feature flag components from rendering in the settings when using components -- 3280711113: Updated dependency `msw` to `^0.49.0`. -- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. -- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. -- 8015ff1258: Tweaked wording to use inclusive terminology -- 653d7912ac: Made `WebStorage` notify its subscribers when `localStorage` values change in other tabs/windows -- 63310e3987: Apps will now rewrite the `app.baseUrl` configuration to match the current `location.origin`. The `backend.baseUrl` will also be rewritten in the same way when the `app.baseUrl` and `backend.baseUrl` have matching origins. This will reduce the need for separate frontend builds for different environments. -- Updated dependencies - - @backstage/core-plugin-api@1.2.0 - - @backstage/version-bridge@1.0.3 - - @backstage/types@1.0.2 - - @backstage/config@1.0.5 - -## 1.3.0-next.4 - -### Minor Changes - -- e0d9c9559a: Added a new `AppRouter` component and `app.createRoot()` method that replaces `app.getRouter()` and `app.getProvider()`, which are now deprecated. The new `AppRouter` component is a drop-in replacement for the old router component, while the new `app.createRoot()` method is used instead of the old provider component. - - An old app setup might look like this: - - ```tsx - const app = createApp(/* ... */); - - const AppProvider = app.getProvider(); - const AppRouter = app.getRouter(); - - const routes = ...; - - const App = () => ( - - - - - {routes} - - - ); - - export default App; - ``` - - With these new APIs, the setup now looks like this: - - ```tsx - import { AppRouter } from '@backstage/core-app-api'; - - const app = createApp(/* ... */); - - const routes = ...; - - export default app.createRoot( - <> - - - - {routes} - - , - ); - ``` - - Note that `app.createRoot()` accepts a React element, rather than a component. - -### Patch Changes - -- b05dcd5530: Move the `zod` dependency to a version that does not collide with other libraries -- Updated dependencies - - @backstage/config@1.0.5-next.1 - - @backstage/core-plugin-api@1.2.0-next.2 - - @backstage/types@1.0.2-next.1 - - @backstage/version-bridge@1.0.3-next.0 - -## 1.2.1-next.3 - -### Patch Changes - -- 6870b43dd1: Fix for the automatic rewriting of base URLs. -- 653d7912ac: Made `WebStorage` notify its subscribers when `localStorage` values change in other tabs/windows -- Updated dependencies - - @backstage/config@1.0.5-next.1 - - @backstage/core-plugin-api@1.2.0-next.2 - - @backstage/types@1.0.2-next.1 - - @backstage/version-bridge@1.0.3-next.0 - -## 1.2.1-next.2 - -### Patch Changes - -- b4b5b02315: Tweak feature flag registration so that it happens immediately before the first rendering of the app, rather than just after. -- 203271b746: Prevent duplicate feature flag components from rendering in the settings when using components -- 8015ff1258: Tweaked wording to use inclusive terminology -- 63310e3987: Apps will now rewrite the `app.baseUrl` configuration to match the current `location.origin`. The `backend.baseUrl` will also be rewritten in the same way when the `app.baseUrl` and `backend.baseUrl` have matching origins. This will reduce the need for separate frontend builds for different environments. -- Updated dependencies - - @backstage/core-plugin-api@1.2.0-next.2 - - @backstage/config@1.0.5-next.1 - - @backstage/types@1.0.2-next.1 - - @backstage/version-bridge@1.0.3-next.0 - -## 1.2.1-next.1 - -### Patch Changes - -- d3fea4ae0a: Internal fixes to avoid implicit usage of globals -- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. -- Updated dependencies - - @backstage/version-bridge@1.0.3-next.0 - - @backstage/core-plugin-api@1.1.1-next.1 - - @backstage/types@1.0.2-next.1 - - @backstage/config@1.0.5-next.1 - -## 1.2.1-next.0 - -### Patch Changes - -- 3280711113: Updated dependency `msw` to `^0.49.0`. -- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.1.1-next.0 - - @backstage/types@1.0.2-next.0 - - @backstage/config@1.0.5-next.0 - - @backstage/version-bridge@1.0.2 - -## 1.2.0 - -### Minor Changes - -- 9b737e5f2e: Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override. -- 127fcad26d: Deprecated the `homepage` config as the component that used it - `HomepageTimer` - has been removed and replaced by the `HeaderWorldClock` in the home plugin - -### Patch Changes - -- Updated dependencies - - @backstage/version-bridge@1.0.2 - - @backstage/core-plugin-api@1.1.0 - - @backstage/types@1.0.1 - - @backstage/config@1.0.4 - -## 1.2.0-next.0 - -### Minor Changes - -- 9b737e5f2e: Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override. -- 127fcad26d: Deprecated the `homepage` config as the component that used it - `HomepageTimer` - has been removed and replaced by the `HeaderWorldClock` in the home plugin - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.1.0-next.0 - - @backstage/types@1.0.1-next.0 - - @backstage/config@1.0.4-next.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1 - -### Patch Changes - -- 27e6404aba: Fixed a bug where gathered index routes would fail to bind routable extensions. This would typically show up when placing a routable extension in the entity page overview tab. -- Updated dependencies - - @backstage/core-plugin-api@1.0.7 - - @backstage/config@1.0.3 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.3-next.2 - - @backstage/core-plugin-api@1.0.7-next.2 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1-next.1 - -### Patch Changes - -- 27e6404aba: Fixed a bug where gathered index routes would fail to bind routable extensions. This would typically show up when placing a routable extension in the entity page overview tab. -- Updated dependencies - - @backstage/core-plugin-api@1.0.7-next.1 - - @backstage/config@1.0.3-next.1 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.3-next.0 - - @backstage/core-plugin-api@1.0.7-next.0 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.0 - -### Minor Changes - -- a448fea691: Updated the routing system to be compatible with React Router v6 stable. - -### Patch Changes - -- 817f3196f6: Updated React Router dependencies to be peer dependencies. - -- f9ec4e46e3: When using React Router v6 stable, it is now possible for components within the `Route` element tree to have `path` props, although they will be ignored. - -- 7d47def9c4: Removed dependency on `@types/jest`. - -- 744fea158b: Added `getSystemIcons()` function to the `AppContext` available through `useApp` that will pull a list of all the icons that have been registered in the App. - -- 667d917488: Updated dependency `msw` to `^0.47.0`. - -- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. - -- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. - -- 8448b53dd6: Clarify that the `WebStorage` observable returns `JsonValue` items. - -- 70299c99d5: Updated `FlatRoutes` to be compatible with React Router v6 stable. - -- e9d40ebf54: If you'd like to send analytics events to multiple implementations, you may now - do so using the `MultipleAnalyticsApi` implementation provided by this package. - - ```tsx - import { MultipleAnalyticsApi } from '@backstage/core-app-api'; - import { - analyticsApiRef, - configApiRef, - storageApiRef, - identityApiRef, - } from '@internal/backstage/core-plugin-api'; - import { CustomAnalyticsApi } from '@internal/analytics'; - import { VendorAnalyticsApi } from '@vendor/analytics'; - - createApiFactory({ - api: analyticsApiRef, - deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, - factory: ({ configApi, identityApi, storageApi }) => - MultipleAnalyticsApi.fromApis([ - VendorAnalyticsApi.fromConfig(configApi, { identityApi }), - CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), - ]), - }), - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.0.6 - - @backstage/config@1.0.2 - -## 1.1.0-next.3 - -### Patch Changes - -- 7d47def9c4: Removed dependency on `@types/jest`. -- Updated dependencies - - @backstage/config@1.0.2-next.0 - - @backstage/core-plugin-api@1.0.6-next.3 - -## 1.1.0-next.2 - -### Patch Changes - -- f9ec4e46e3: When using React Router v6 stable, it is now possible for components within the `Route` element tree to have `path` props, although they will be ignored. - -- 667d917488: Updated dependency `msw` to `^0.47.0`. - -- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. - -- e9d40ebf54: If you'd like to send analytics events to multiple implementations, you may now - do so using the `MultipleAnalyticsApi` implementation provided by this package. - - ```tsx - import { MultipleAnalyticsApi } from '@backstage/core-app-api'; - import { - analyticsApiRef, - configApiRef, - storageApiRef, - identityApiRef, - } from '@internal/backstage/core-plugin-api'; - import { CustomAnalyticsApi } from '@internal/analytics'; - import { VendorAnalyticsApi } from '@vendor/analytics'; - - createApiFactory({ - api: analyticsApiRef, - deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, - factory: ({ configApi, identityApi, storageApi }) => - MultipleAnalyticsApi.fromApis([ - VendorAnalyticsApi.fromConfig(configApi, { identityApi }), - CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), - ]), - }), - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.0.6-next.2 - -## 1.1.0-next.1 - -### Minor Changes - -- a448fea691: Updated the routing system to be compatible with React Router v6 stable. - -### Patch Changes - -- 817f3196f6: Updated React Router dependencies to be peer dependencies. -- 70299c99d5: Updated `FlatRoutes` to be compatible with React Router v6 stable. -- Updated dependencies - - @backstage/core-plugin-api@1.0.6-next.1 - -## 1.0.6-next.0 - -### Patch Changes - -- 744fea158b: Added `getSystemIcons()` function to the `AppContext` available through `useApp` that will pull a list of all the icons that have been registered in the App. -- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.6-next.0 - -## 1.0.5 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.0.5 - -## 1.0.5-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.0.5-next.0 - -## 1.0.4 - -### Patch Changes - -- 881fc75a75: Internal tweak removing usage of explicit type parameters for the `BackstagePlugin` type. -- 8fe2357101: The `signOut` method of the `IdentityApi` will now navigate the user back to the base URL of the app as indicated by the `app.baseUrl` config. -- a70869e775: Updated dependency `msw` to `^0.43.0`. -- 8006d0f9bf: Updated dependency `msw` to `^0.44.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.4 - -## 1.0.4-next.1 - -### Patch Changes - -- 881fc75a75: Internal tweak removing usage of explicit type parameters for the `BackstagePlugin` type. -- a70869e775: Updated dependency `msw` to `^0.43.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.4-next.0 - -## 1.0.4-next.0 - -### Patch Changes - -- 8fe2357101: The `signOut` method of the `IdentityApi` will now navigate the user back to the base URL of the app as indicated by the `app.baseUrl` config. - -## 1.0.3 - -### Patch Changes - -- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. -- 19781483a2: Handle URLs as the first argument to `fetchApi`, when using the `plugin:` protocol -- Updated dependencies - - @backstage/core-plugin-api@1.0.3 - -## 1.0.3-next.0 - -### Patch Changes - -- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.3-next.0 - -## 1.0.2 - -### Patch Changes - -- 1fae1f57c9: Fix SAML session schema to no longer require the (deprecated) id, to unbreak session data storage. -- Updated dependencies - - @backstage/core-plugin-api@1.0.2 - - @backstage/config@1.0.1 - -## 1.0.2-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.1-next.0 - - @backstage/core-plugin-api@1.0.2-next.1 - -## 1.0.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.0.2-next.0 - -## 1.0.1 - -### Patch Changes - -- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 -- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 -- 3ff2bfb66e: Refactored the route collection logic to prepare for future changes and avoid duplicate element tree traversal for the analytics context. -- a7bb762dab: fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix -- 230ad0826f: Bump to using `@types/node` v16 -- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. -- Updated dependencies - - @backstage/core-plugin-api@1.0.1 - - @backstage/version-bridge@1.0.1 - -## 1.0.1-next.1 - -### Patch Changes - -- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 -- 3ff2bfb66e: Refactored the route collection logic to prepare for future changes and avoid duplicate element tree traversal for the analytics context. -- 230ad0826f: Bump to using `@types/node` v16 -- Updated dependencies - - @backstage/core-plugin-api@1.0.1-next.0 - -## 1.0.1-next.0 - -### Patch Changes - -- a7bb762dab: fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix -- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. - -## 1.0.0 - -### Major Changes - -- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). - -### Patch Changes - -- a422d7ce5e: chore(deps): bump `@testing-library/react` from 11.2.6 to 12.1.3 -- f24ef7864e: Minor typo fixes -- Updated dependencies - - @backstage/core-plugin-api@1.0.0 - - @backstage/version-bridge@1.0.0 - - @backstage/config@1.0.0 - - @backstage/types@1.0.0 - -## 0.6.0 - -### Minor Changes - -- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use \`observe# @backstage/core-app-api. -- f3cce3dcf7: **BREAKING**: Removed export of `GithubSession` and `SamlSession` which are only used internally. -- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at . -- dbf84eee55: **BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@0.8.0 - -## 0.5.4 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@0.7.0 - -## 0.5.3 - -### Patch Changes - -- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 -- c77c5c7eb6: Added `backstage.role` to `package.json` -- Updated dependencies - - @backstage/core-plugin-api@0.6.1 - - @backstage/config@0.1.14 - - @backstage/types@0.1.2 - - @backstage/version-bridge@0.1.2 - -## 0.5.2 - -### Patch Changes - -- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`. - - > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change. - -## 0.5.2-next.0 - -### Patch Changes - -- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`. - - > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change. - -## 0.5.1 - -### Patch Changes - -- f959c22787: Asynchronous methods on the identity API can now reliably be called at any time, including early in the bootstrap process or prior to successful sign-in. - - Previously in such situations, a `Tried to access IdentityApi before app was loaded` error would be thrown. Now, those methods will wait and resolve eventually (as soon as a concrete identity API is provided). - -## 0.5.0 - -### Minor Changes - -- ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback. - -### Patch Changes - -- fb565073ec: Add an `allowUrl` callback option to `FetchMiddlewares.injectIdentityAuth` -- f050eec2c0: Added validation during the application startup that detects if there are any plugins present that have not had their required external routes bound. Failing the validation will cause a hard crash as it is a programmer error. It lets you detect early on that there are dangling routes, rather than having them cause an error later on. -- Updated dependencies - - @backstage/core-plugin-api@0.6.0 - - @backstage/config@0.1.13 - -## 0.5.0-next.0 - -### Minor Changes - -- ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@0.6.0-next.0 - - @backstage/config@0.1.13-next.0 - -## 0.4.0 - -### Minor Changes - -- e2eb92c109: Removed previously deprecated `ApiRegistry` export. - -### Patch Changes - -- 34442cd5cf: Fixed an issue where valid SAML and GitHub sessions would be considered invalid and not be stored. - - Deprecated the `SamlSession` and `GithubSession` types. - -- 784d8078ab: Removed direct and transitive Material UI dependencies. - -- Updated dependencies - - @backstage/config@0.1.12 - - @backstage/core-plugin-api@0.5.0 - -## 0.3.1 - -### Patch Changes - -- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead. -- Updated dependencies - - @backstage/core-plugin-api@0.4.1 - - @backstage/core-components@0.8.3 - -## 0.3.0 - -### Minor Changes - -- a195284c7b: Updated `WebStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`. -- b3605da81c: - Removed deprecated definition `createApp` from `@backstage/core-app-api` which has been replaced by `@backstage/app-defaults#createApp` - - Removed deprecated type `BackstagePluginWithAnyOutput` - - Removed deprecated constructors for `GithubAuth`, `OAuth2`, and `SamlAuth` as the `create` method should be used instead -- 68f8b10ccd: - Removed deprecation configuration option `theme` from `AppTheme` of the `AppThemeApi` - - Removed reference to `theme` in the `app-defaults` default `AppTheme` - - Removed logic in `AppThemeProvider` that creates `ThemeProvider` from `appTheme.theme` - -### Patch Changes - -- 7927005152: Add `FetchApi` and related `fetchApiRef` which implement fetch, with an added Backstage token header when available. -- 518ddc00bc: Schema-validate local storage cached session info on load -- Updated dependencies - - @backstage/app-defaults@0.1.3 - - @backstage/core-plugin-api@0.4.0 - - @backstage/core-components@0.8.2 - -## 0.2.1 - -### Patch Changes - -- c11ce4f552: Deprecated `Auth0Auth`, pointing to using `OAuth2` directly instead. -- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`. -- Updated dependencies - - @backstage/core-plugin-api@0.3.1 - - @backstage/core-components@0.8.1 - -## 0.2.0 - -### Minor Changes - -- a036b65c2f: **BREAKING CHANGE** - - The app `SignInPage` component has been updated to switch out the `onResult` callback for a new `onSignInSuccess` callback. This is an immediate breaking change without any deprecation period, as it was deemed to be the way of making this change that had the lowest impact. - - The new `onSignInSuccess` callback directly accepts an implementation of an `IdentityApi`, rather than a `SignInResult`. The `SignInPage` from `@backstage/core-component` has been updated to fit this new API, and as long as you pass on `props` directly you should not see any breakage. - - However, if you implement your own custom `SignInPage`, then this will be a breaking change and you need to migrate over to using the new callback. While doing so you can take advantage of the `UserIdentity.fromLegacy` helper from `@backstage/core-components` to make the migration simpler by still using the `SignInResult` type. This helper is also deprecated though and is only provided for immediate migration. Long-term it will be necessary to build the `IdentityApi` using for example `UserIdentity.create` instead. - - The following is an example of how you can migrate existing usage immediately using `UserIdentity.fromLegacy`: - - ```ts - onResult(signInResult); - // becomes - onSignInSuccess(UserIdentity.fromLegacy(signInResult)); - ``` - - The following is an example of how implement the new `onSignInSuccess` callback of the `SignInPage` using `UserIdentity.create`: - - ```ts - const identityResponse = await authApi.getBackstageIdentity(); - // Profile is optional and will be removed, but allows the - // synchronous getProfile method of the IdentityApi to be used. - const profile = await authApi.getProfile(); - onSignInSuccess( - UserIdentity.create({ - identity: identityResponse.identity, - authApi, - profile, - }), - ); - ``` - -### Patch Changes - -- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. -- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly -- Updated dependencies - - @backstage/core-components@0.8.0 - - @backstage/core-plugin-api@0.3.0 - - @backstage/app-defaults@0.1.2 - - @backstage/version-bridge@0.1.1 - -## 0.1.24 - -### Patch Changes - -- 0e7f256034: Fixed a bug where `useRouteRef` would fail in situations where relative navigation was needed and the app was is mounted on a sub-path. This would typically show up as a failure to navigate to a tab on an entity page. -- Updated dependencies - - @backstage/core-components@0.7.6 - - @backstage/theme@0.2.14 - - @backstage/core-plugin-api@0.2.2 - -## 0.1.23 - -### Patch Changes - -- bab752e2b3: Change default port of backend from 7000 to 7007. - - This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. - - You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: - - backend: - listen: 0.0.0.0:7123 - baseUrl: http://localhost:7123 - - More information can be found here: - -- 000190de69: The `ApiRegistry` from `@backstage/core-app-api` class has been deprecated and will be removed in a future release. To replace it, we have introduced two new helpers that are exported from `@backstage/test-utils`, namely `TestApiProvider` and `TestApiRegistry`. - - These two new helpers are more tailored for writing tests and development setups, as they allow for partial implementations of each of the APIs. - - When migrating existing code it is typically best to prefer usage of `TestApiProvider` when possible, so for example the following code: - - ```tsx - render( - - {...} - - ) - ``` - - Would be migrated to this: - - ```tsx - render( - - {...} - - ) - ``` - - In cases where the `ApiProvider` is used in a more standalone way, for example to reuse a set of APIs across multiple tests, the `TestApiRegistry` can be used instead. Note that the `TestApiRegistry` only has a single static factory method, `.from()`, and it is slightly different from the existing `.from()` method on `ApiRegistry` in that it doesn't require the API pairs to be wrapped in an outer array. - - Usage that looks like this: - - ```ts - const apis = ApiRegistry.with( - identityApiRef, - mockIdentityApi as unknown as IdentityApi, - ).with(configApiRef, new ConfigReader({})); - ``` - - OR like this: - - ```ts - const apis = ApiRegistry.from([ - [identityApiRef, mockIdentityApi as unknown as IdentityApi], - [configApiRef, new ConfigReader({})], - ]); - ``` - - Would be migrated to this: - - ```ts - const apis = TestApiRegistry.from( - [identityApiRef, mockIdentityApi], - [configApiRef, new ConfigReader({})], - ); - ``` - - If your app is still using the `ApiRegistry` to construct the `apis` for `createApp`, we recommend that you move over to use the new method of supplying API factories instead, using `createApiFactory`. - -- Updated dependencies - - @backstage/core-plugin-api@0.2.1 - - @backstage/core-components@0.7.5 - -## 0.1.22 - -### Patch Changes - -- Reverted the `createApp` TypeScript type to match the one before version `0.1.21`, as it was an accidental breaking change. - -## 0.1.21 - -### Patch Changes - -- 0b1de52732: Migrated to using new `ErrorApiError` and `ErrorApiErrorContext` names. - -- ecd1fcb80a: Deprecated the `BackstagePluginWithAnyOutput` type. - -- 32bfbafb0f: Start exporting and marking several types as public to address errors in the API report. - -- 014cbf8cb9: The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement. - - The first and most commonly used one is `createApp` from the new `@backstage/app-defaults` package, which behaves just like the existing `createApp`. In the future this method is likely to be expanded to add more APIs and other pieces into the default setup, for example the Utility APIs from `@backstage/integration-react`. - - The other option that we now provide is to use `createSpecializedApp` from `@backstage/core-app-api`. This is a more low-level API where you need to provide a full set of options, including your own `components`, `icons`, `defaultApis`, and `themes`. The `createSpecializedApp` way of creating an app is particularly useful if you are not using `@backstage/core-components` or Material UI, as it allows you to avoid those dependencies completely. - -- 475edb5bc5: move the BehaviorSubject init into the constructor - -- Updated dependencies - - @backstage/core-components@0.7.4 - - @backstage/core-plugin-api@0.2.0 - - @backstage/app-defaults@0.1.1 - -## 0.1.20 - -### Patch Changes - -- 78c512ce8f: I have added default icons for the catalog, scaffolder, techdocs, and search. -- 8b4284cd5c: Improve API documentation for @backstage/core-plugin-api -- Updated dependencies - - @backstage/core-components@0.7.3 - - @backstage/theme@0.2.13 - - @backstage/core-plugin-api@0.1.13 - -## 0.1.19 - -### Patch Changes - -- 10615525f3: Switch to use the json and observable types from `@backstage/types` -- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. -- 925a967f36: Replace usage of test-utils-core with test-utils -- 6b615e92c8: Api cleanup, adding `@public` where necessary and tweaking some comments -- Updated dependencies - - @backstage/config@0.1.11 - - @backstage/theme@0.2.12 - - @backstage/core-components@0.7.2 - - @backstage/core-plugin-api@0.1.12 - -## 0.1.18 - -### Patch Changes - -- 202f322927: Atlassian auth provider - - - AtlassianAuth added to core-app-api - - Atlassian provider added to plugin-auth-backend - - Updated user-settings with Atlassian connection - -- 36e67d2f24: Internal updates to apply more strict checks to throw errors. - -- Updated dependencies - - @backstage/core-components@0.7.1 - - @backstage/core-plugin-api@0.1.11 - -## 0.1.17 - -### Patch Changes - -- 75bc878221: Internal refactor to avoid importing all of `@material-ui/core`. -- Updated dependencies - - @backstage/core-components@0.7.0 - - @backstage/theme@0.2.11 - -## 0.1.16 - -### Patch Changes - -- d9fd798cc8: The Core App API now automatically instruments all route location changes using - the new Analytics API. Each location change triggers a `navigate` event, which - is an analogue of a "pageview" event in traditional web analytics systems. In - addition to the path, these events provide plugin-level metadata via the - analytics context, which can be useful for analyzing plugin usage: - - ```json - { - "action": "navigate", - "subject": "/the-path/navigated/to?with=params#and-hashes", - "context": { - "extension": "App", - "pluginId": "id-of-plugin-that-exported-the-route", - "routeRef": "associated-route-ref-id" - } - } - ``` - - These events can be identified and handled by checking for the action - `navigate` and the extension `App`. - -- 4c3eea7788: Bitbucket Cloud authentication - based on the existing GitHub authentication + changes around BB apis and updated scope. - - - BitbucketAuth added to core-app-api. - - Bitbucket provider added to plugin-auth-backend. - - Cosmetic entry for Bitbucket connection in user-settings Authentication Providers tab. - -- d6ad46eb22: Stop calling connector.removeSession in StaticAuthSessionManager, instead just discarding the - session locally. - -- Updated dependencies - - @backstage/core-components@0.6.1 - - @backstage/core-plugin-api@0.1.10 - -## 0.1.15 - -### Patch Changes - -- 0c4ee1876f: Enables late registration of plugins into the application by updating ApiHolder when additional plugins have been added in. -- Updated dependencies - - @backstage/core-plugin-api@0.1.9 - - @backstage/core-components@0.6.0 - -## 0.1.14 - -### Patch Changes - -- Updated dependencies - - @backstage/core-components@0.5.0 - - @backstage/config@0.1.10 - -## 0.1.13 - -### Patch Changes - -- 671015f132: Switch to using utilities from \`@backstage/version-bridge'. -- bd1981d609: Allow users to specify their own AppThemeProvider -- Updated dependencies - - @backstage/core-components@0.4.2 - - @backstage/core-plugin-api@0.1.8 - -## 0.1.12 - -### Patch Changes - -- 841666a19: Removed deprecated internal functions. -- Updated dependencies - - @backstage/core-components@0.4.1 - - @backstage/config@0.1.9 - - @backstage/core-plugin-api@0.1.7 - -## 0.1.11 - -### Patch Changes - -- Updated dependencies - - @backstage/core-components@0.4.0 - -## 0.1.10 - -### Patch Changes - -- cfcb486aa: Add system icons for the built-in entity types and use them in the entity list of the `catalog-import` plugin. - -- 392b36fa1: Added support for using authenticating via GitHub Apps in addition to GitHub OAuth Apps. It used to be possible to use GitHub Apps, but they did not handle session refresh correctly. - - Note that GitHub Apps handle OAuth scope at the app installation level, meaning that the `scope` parameter for `getAccessToken` has no effect. When calling `getAccessToken` in open source plugins, one should still include the appropriate scope, but also document in the plugin README what scopes are required in the case of GitHub Apps. - - In addition, the `authHandler` and `signInResolver` options have been implemented for the GitHub provider in the auth backend. - -- Updated dependencies - - @backstage/core-components@0.3.3 - - @backstage/config@0.1.8 - -## 0.1.9 - -### Patch Changes - -- 72a31c29a: Add support for additional app origins -- Updated dependencies - - @backstage/config@0.1.7 - - @backstage/core-components@0.3.2 - - @backstage/theme@0.2.10 - -## 0.1.8 - -### Patch Changes - -- 362657623: Add support for serving the app with a base path other than `/`, which is enabled by including the path in `app.baseUrl`. -- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. -- Updated dependencies - - @backstage/core-components@0.3.1 - - @backstage/core-plugin-api@0.1.6 - -## 0.1.7 - -### Patch Changes - -- Updated dependencies - - @backstage/core-components@0.3.0 - - @backstage/config@0.1.6 - - @backstage/core-plugin-api@0.1.5 - -## 0.1.6 - -### Patch Changes - -- 9d40fcb1e: - Bumping `material-ui/core` version to at least `4.12.2` as they made some breaking changes in later versions which broke `Pagination` of the `Table`. - - Switching out `material-table` to `@material-table/core` for support for the later versions of `material-ui/core` - - This causes a minor API change to `@backstage/core-components` as the interface for `Table` re-exports the `prop` from the underlying `Table` components. - - `onChangeRowsPerPage` has been renamed to `onRowsPerPageChange` - - `onChangePage` has been renamed to `onPageChange` - - Migration guide is here: -- Updated dependencies - - @backstage/core-components@0.2.0 - - @backstage/core-plugin-api@0.1.4 - - @backstage/theme@0.2.9 - -## 0.1.5 - -### Patch Changes - -- ea249c6e6: Fix a bug in `FlatRoutes` that prevented outlets from working with the root route, as well as matching root routes too broadly. -- Updated dependencies - - @backstage/core-components@0.1.6 - -## 0.1.4 - -### Patch Changes - -- 62abffee4: Reintroduce export of `defaultConfigLoader`. -- Updated dependencies - - @backstage/core-components@0.1.4 - -## 0.1.3 - -### Patch Changes - -- dc3e7ce68: Introducing new UnhandledErrorForwarder installed by default. For catching unhandled promise rejections, you can override the API to align with general error handling. -- 5f4339b8c: Adding `FeatureFlag` component and treating `FeatureFlags` as first class citizens to composability API -- Updated dependencies - - @backstage/core-plugin-api@0.1.3 - -## 0.1.2 - -### Patch Changes - -- 9bca2a252: Fixes a type bug where supplying all app icons to `createApp` was required, rather than just a partial list. - -- 75b8537ce: This change adds automatic error boundaries around extensions. - - This means that all exposed parts of a plugin are wrapped in a general error boundary component, that is plugin aware. The default design for the error box is borrowed from `@backstage/errors`. To override the default "fallback", one must provide a component named `ErrorBoundaryFallback` to `createApp`, like so: - - ```ts - const app = createApp({ - components: { - ErrorBoundaryFallback: props => { - // a custom fallback component - return ( - <> -

Oops.

-

- The plugin {props.plugin.getId()} failed with{' '} - {props.error.message} -

- - - ); - }, - }, - }); - ``` - - The props here include: - - - `error`. An `Error` object or something that inherits it that represents the error that was thrown from any inner component. - - `resetError`. A callback that will simply attempt to mount the children of the error boundary again. - - `plugin`. A `BackstagePlugin` that can be used to look up info to be presented in the error message. For instance, you may want to keep a map of your internal plugins and team names or slack channels and present these when an error occurs. Typically, you'll do that by getting the plugin ID with `plugin.getId()`. - -- da8cba44f: Deprecate and disable the extension creation methods, which were added to this package by mistake and should only exist within `@backstage/core-plugin-api`. - -- 9bca2a252: Update `createApp` options to allow plugins with unknown output types in order to improve forwards and backwards compatibility. - -- Updated dependencies [e47336ea4] - -- Updated dependencies [75b8537ce] - -- Updated dependencies [da8cba44f] - - @backstage/core-components@0.1.2 - - @backstage/core-plugin-api@0.1.2 - -## 0.1.1 - -### Patch Changes - -- e7c5e4b30: Update installation instructions in README. -- Updated dependencies [031ccd45f] -- Updated dependencies [e7c5e4b30] - - @backstage/core-plugin-api@0.1.1 - - @backstage/core-components@0.1.1 - - @backstage/theme@0.2.8 - + - ## @backstage/config-loader@1.9.0-next.1 ### Minor Changes From 587f1ee44473bf4dd62fc1736407c1fe59e529f9 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 31 Jul 2024 14:38:38 +0200 Subject: [PATCH 077/204] Update tricky-ducks-juggle.md Signed-off-by: Ben Lambert --- .changeset/tricky-ducks-juggle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tricky-ducks-juggle.md b/.changeset/tricky-ducks-juggle.md index 7f91c978a2..48c0d215bd 100644 --- a/.changeset/tricky-ducks-juggle.md +++ b/.changeset/tricky-ducks-juggle.md @@ -2,4 +2,4 @@ '@backstage/backend-app-api': patch --- -Add `packages` to backend-app-api configuration schema +Added configuration for the `packages` options to config schema From df784fe970a6d3b0ec30af6b39b31ed19f9df291 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Wed, 31 Jul 2024 14:04:13 +0100 Subject: [PATCH 078/204] permissions: move MetadataResponse type to permission-common This was missed when moving MetadataResponseSerializedRule in 137fa342b4bf43c91e91d3b0b947b8d37c9e233e - seems useful to keep these two types together. Signed-off-by: MT Lewis --- .changeset/fair-hairs-mix.md | 6 ++++++ .changeset/friendly-cherries-applaud.md | 9 +++++++++ plugins/permission-common/api-report.md | 6 ++++++ plugins/permission-common/src/types/index.ts | 5 ++++- plugins/permission-common/src/types/integration.ts | 12 ++++++++++++ plugins/permission-node/api-report.md | 8 +++----- .../integration/createPermissionIntegrationRouter.ts | 7 +++---- 7 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 .changeset/fair-hairs-mix.md create mode 100644 .changeset/friendly-cherries-applaud.md diff --git a/.changeset/fair-hairs-mix.md b/.changeset/fair-hairs-mix.md new file mode 100644 index 0000000000..6c06c7bf72 --- /dev/null +++ b/.changeset/fair-hairs-mix.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-permission-common': patch +--- + +Add the MetadataResponse type from @backstage/plugin-permission-node, since this +type might be used in frontend code. diff --git a/.changeset/friendly-cherries-applaud.md b/.changeset/friendly-cherries-applaud.md new file mode 100644 index 0000000000..c0160858f2 --- /dev/null +++ b/.changeset/friendly-cherries-applaud.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +The MetadataResponse type has been moved to @backstage/plugin-permission-common +to match the recent move of MetadataResponseSerializedRule, and should be +imported from there going forward. To avoid an immediate breaking change, this +type is still re-exported from this package, but is marked as deprecated and +will be removed in a future release. diff --git a/plugins/permission-common/api-report.md b/plugins/permission-common/api-report.md index 12dd49d090..465c43846f 100644 --- a/plugins/permission-common/api-report.md +++ b/plugins/permission-common/api-report.md @@ -126,6 +126,12 @@ export function isResourcePermission( // @public export function isUpdatePermission(permission: Permission): boolean; +// @public +export type MetadataResponse = { + permissions?: Permission[]; + rules: MetadataResponseSerializedRule[]; +}; + // @public export type MetadataResponseSerializedRule = { name: string; diff --git a/plugins/permission-common/src/types/index.ts b/plugins/permission-common/src/types/index.ts index 1d2243447f..f5380541df 100644 --- a/plugins/permission-common/src/types/index.ts +++ b/plugins/permission-common/src/types/index.ts @@ -40,7 +40,10 @@ export type { NotCriteria, } from './api'; export type { DiscoveryApi } from './discovery'; -export type { MetadataResponseSerializedRule } from './integration'; +export type { + MetadataResponse, + MetadataResponseSerializedRule, +} from './integration'; export type { BasicPermission, PermissionAttributes, diff --git a/plugins/permission-common/src/types/integration.ts b/plugins/permission-common/src/types/integration.ts index 6c208bb775..d531140923 100644 --- a/plugins/permission-common/src/types/integration.ts +++ b/plugins/permission-common/src/types/integration.ts @@ -15,6 +15,7 @@ */ import zodToJsonSchema from 'zod-to-json-schema'; +import { Permission } from './permission'; /** * Serialized permission rules, with the paramsSchema @@ -28,3 +29,14 @@ export type MetadataResponseSerializedRule = { resourceType: string; paramsSchema?: ReturnType; }; + +/** + * Response type for the .metadata endpoint in + * {@link @backstage/plugin-permission-node#createPermissionIntegrationRouter} + * + * @public + */ +export type MetadataResponse = { + permissions?: Permission[]; + rules: MetadataResponseSerializedRule[]; +}; diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 8a90a39728..36483bd342 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -17,6 +17,7 @@ import { DefinitivePolicyDecision } from '@backstage/plugin-permission-common'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentifiedPermissionMessage } from '@backstage/plugin-permission-common'; +import { MetadataResponse as MetadataResponse_2 } from '@backstage/plugin-permission-common'; import { MetadataResponseSerializedRule as MetadataResponseSerializedRule_2 } from '@backstage/plugin-permission-common'; import { NotCriteria } from '@backstage/plugin-permission-common'; import { Permission } from '@backstage/plugin-permission-common'; @@ -188,11 +189,8 @@ export const makeCreatePermissionRule: < rule: PermissionRule, ) => PermissionRule; -// @public -export type MetadataResponse = { - permissions?: Permission[]; - rules: MetadataResponseSerializedRule[]; -}; +// @public @deprecated +export type MetadataResponse = MetadataResponse_2; // @public @deprecated export type MetadataResponseSerializedRule = MetadataResponseSerializedRule_2; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 7793d8ab8e..40d215d515 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -24,6 +24,7 @@ import { AuthorizeResult, DefinitivePolicyDecision, IdentifiedPermissionMessage, + MetadataResponse as CommonMetadataResponse, MetadataResponseSerializedRule as CommonMetadataResponseSerializedRule, Permission, PermissionCondition, @@ -119,11 +120,9 @@ export type MetadataResponseSerializedRule = * Response type for the .metadata endpoint. * * @public + * @deprecated Please import from `@backstage/plugin-permission-common` instead. */ -export type MetadataResponse = { - permissions?: Permission[]; - rules: MetadataResponseSerializedRule[]; -}; +export type MetadataResponse = CommonMetadataResponse; const applyConditions = ( criteria: PermissionCriteria>, From 878dfb2e5e9dcbee78cdd0d6ee0e2aecef86e674 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 15:40:19 +0200 Subject: [PATCH 079/204] :nails: Signed-off-by: blam --- docs/releases/v1.30.0-next.1-changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/releases/v1.30.0-next.1-changelog.md b/docs/releases/v1.30.0-next.1-changelog.md index efd746686b..1d93235740 100644 --- a/docs/releases/v1.30.0-next.1-changelog.md +++ b/docs/releases/v1.30.0-next.1-changelog.md @@ -12,7 +12,8 @@ Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.1](h - @backstage/core-plugin-api@1.9.3 - @backstage/types@1.1.1 - @backstage/version-bridge@1.0.8 - - + - + ## @backstage/config-loader@1.9.0-next.1 ### Minor Changes From 58f764e97e1fe4d4a4779c21479e20eb0fb2664d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:37:38 +0000 Subject: [PATCH 080/204] chore(deps): update chromaui/action digest to fdbe775 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/verify_storybook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 2f93b67616..e9da9f0398 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -51,7 +51,7 @@ jobs: - run: yarn build-storybook - - uses: chromaui/action@5991575112b472a4fa85429f29cc17e2fb83a2a2 # v11 + - uses: chromaui/action@fdbe7756d4dbf493e2fbb822df73be7accd07e1c # v11 with: token: ${{ secrets.GITHUB_TOKEN }} # projectToken intentionally shared to allow collaborators to run Chromatic on forks From 04759f206fb8f9b04ddcc226a746dd72fb15bd73 Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Wed, 31 Jul 2024 19:43:29 -0400 Subject: [PATCH 081/204] add null check for review state object check Signed-off-by: Stephen Glass --- .changeset/slow-ligers-drum.md | 5 +++++ .../src/next/components/ReviewState/util.test.ts | 6 +++++- .../src/next/components/ReviewState/util.ts | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .changeset/slow-ligers-drum.md diff --git a/.changeset/slow-ligers-drum.md b/.changeset/slow-ligers-drum.md new file mode 100644 index 0000000000..867cc07cee --- /dev/null +++ b/.changeset/slow-ligers-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix null check in `isJsonObject` utility function for scaffolder review state component diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts index 9b88ff157e..649d0c3160 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * 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. @@ -33,6 +33,10 @@ describe('isJsonObject', () => { expect(isJsonObject(true)).toBe(false); expect(isJsonObject(undefined)).toBe(false); }); + + it('should return false for null values', () => { + expect(isJsonObject(null)).toBe(false); + }); }); describe('getLastKey', () => { diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts index 49c3b30e7a..d2caf71964 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * 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. @@ -17,7 +17,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; export function isJsonObject(value?: JsonValue): value is JsonObject { - return typeof value === 'object' && !Array.isArray(value); + return typeof value === 'object' && value !== null && !Array.isArray(value); } // Helper function to get the last part of the key From 3123c16a0ac2b32e466d4eabef432dfbdcd3f947 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Thu, 1 Aug 2024 11:41:28 +0100 Subject: [PATCH 082/204] search: fix incorrect metadata due to inconsistent package name According to the usual Backstage package naming conventions, @backstage/plugin-search-backend-node should be called @backstage/plugin-search-node. Because of this mismatch, metadata was generated incorrectly. Adjust the metadata in all search packages to include the node package, and fix the pluginId in the node package itself. Signed-off-by: MT Lewis Co-authored-by: Jack Palmer --- .changeset/healthy-timers-divide.md | 9 +++++++++ plugins/search-backend-node/package.json | 8 ++++++-- plugins/search-backend/package.json | 5 +++-- plugins/search-common/package.json | 5 +++-- plugins/search-react/package.json | 5 +++-- plugins/search/package.json | 5 +++-- 6 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 .changeset/healthy-timers-divide.md diff --git a/.changeset/healthy-timers-divide.md b/.changeset/healthy-timers-divide.md new file mode 100644 index 0000000000..0b9d700723 --- /dev/null +++ b/.changeset/healthy-timers-divide.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search-common': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-search': patch +--- + +Fix package metadata diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 710de9cd67..e2398e8245 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -4,9 +4,13 @@ "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", - "pluginId": "search-backend", + "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search-backend-node" + "@backstage/plugin-search-backend-node", + "@backstage/plugin-search-backend", + "@backstage/plugin-search-common", + "@backstage/plugin-search-react", + "@backstage/plugin-search" ] }, "publishConfig": { diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 8962162af6..2290a753ec 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -6,10 +6,11 @@ "role": "backend-plugin", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-backend", "@backstage/plugin-search-common", - "@backstage/plugin-search-react" + "@backstage/plugin-search-react", + "@backstage/plugin-search" ] }, "publishConfig": { diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index f569388c48..7f0462f69d 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -6,10 +6,11 @@ "role": "common-library", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-backend", "@backstage/plugin-search-common", - "@backstage/plugin-search-react" + "@backstage/plugin-search-react", + "@backstage/plugin-search" ] }, "publishConfig": { diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 4fa50cca10..6d3933b1e4 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -5,10 +5,11 @@ "role": "web-library", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-backend", "@backstage/plugin-search-common", - "@backstage/plugin-search-react" + "@backstage/plugin-search-react", + "@backstage/plugin-search" ] }, "publishConfig": { diff --git a/plugins/search/package.json b/plugins/search/package.json index a4a5ea3593..e772207d4b 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -6,10 +6,11 @@ "role": "frontend-plugin", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-backend", "@backstage/plugin-search-common", - "@backstage/plugin-search-react" + "@backstage/plugin-search-react", + "@backstage/plugin-search" ] }, "publishConfig": { From 4b6d2cb74c7fcfd9913c9cc0d383384130296596 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:12:19 +0000 Subject: [PATCH 083/204] fix(deps): update dependency @graphiql/react to ^0.23.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-147ac48.md | 5 ++ plugins/api-docs/package.json | 2 +- yarn.lock | 111 ++++++++++++++++++++++++++++----- 3 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 .changeset/renovate-147ac48.md diff --git a/.changeset/renovate-147ac48.md b/.changeset/renovate-147ac48.md new file mode 100644 index 0000000000..257255dff1 --- /dev/null +++ b/.changeset/renovate-147ac48.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Updated dependency `@graphiql/react` to `^0.23.0`. diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 4f88e8874a..3633471588 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -63,7 +63,7 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", - "@graphiql/react": "^0.20.0", + "@graphiql/react": "^0.23.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/yarn.lock b/yarn.lock index e65851a7ba..e1cebfb537 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4696,7 +4696,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" - "@graphiql/react": ^0.20.0 + "@graphiql/react": ^0.23.0 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 @@ -9612,7 +9612,7 @@ __metadata: languageName: node linkType: hard -"@graphiql/react@npm:^0.20.0, @graphiql/react@npm:^0.20.3": +"@graphiql/react@npm:^0.20.3": version: 0.20.3 resolution: "@graphiql/react@npm:0.20.3" dependencies: @@ -9639,9 +9639,36 @@ __metadata: languageName: node linkType: hard -"@graphiql/toolkit@npm:^0.9.1": - version: 0.9.1 - resolution: "@graphiql/toolkit@npm:0.9.1" +"@graphiql/react@npm:^0.23.0": + version: 0.23.0 + resolution: "@graphiql/react@npm:0.23.0" + dependencies: + "@graphiql/toolkit": ^0.9.2 + "@headlessui/react": ^1.7.15 + "@radix-ui/react-dialog": ^1.0.4 + "@radix-ui/react-dropdown-menu": ^2.0.5 + "@radix-ui/react-tooltip": ^1.0.6 + "@radix-ui/react-visually-hidden": ^1.0.3 + "@types/codemirror": ^5.60.8 + clsx: ^1.2.1 + codemirror: ^5.65.3 + codemirror-graphql: ^2.0.13 + copy-to-clipboard: ^3.2.0 + framer-motion: ^6.5.1 + graphql-language-service: ^5.2.2 + markdown-it: ^14.1.0 + set-value: ^4.1.0 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + checksum: a62ee7d763d410f5ff8c5670596194069e97f11b906762c4e38c74797e4e34c33cd91e06aae24d5118e0fa05dc6c1ffa6c3d3d8ff5bc2b6af0b689088236f613 + languageName: node + linkType: hard + +"@graphiql/toolkit@npm:^0.9.1, @graphiql/toolkit@npm:^0.9.2": + version: 0.9.2 + resolution: "@graphiql/toolkit@npm:0.9.2" dependencies: "@n1ru4l/push-pull-async-iterable-iterator": ^3.1.0 meros: ^1.1.4 @@ -9651,7 +9678,7 @@ __metadata: peerDependenciesMeta: graphql-ws: optional: true - checksum: 5328426051b7f9a9ffbd569c950d1a103ce0e2ee7b5d7a57f3d899488ad43d1a5101e8aeced7416e106c7687d67bb7981aa7e87dea5b0f17b77569aa738bf3b5 + checksum: 4474a998a948d0df5cef8371245b0e895be40803a07e49231ab91798aa761880e6e4a684478a44b723c013f0e8206dab3d86e5b0fc9b688df035fa9f1fa29734 languageName: node linkType: hard @@ -23202,17 +23229,17 @@ __metadata: languageName: node linkType: hard -"codemirror-graphql@npm:^2.0.10": - version: 2.0.11 - resolution: "codemirror-graphql@npm:2.0.11" +"codemirror-graphql@npm:^2.0.10, codemirror-graphql@npm:^2.0.13": + version: 2.0.13 + resolution: "codemirror-graphql@npm:2.0.13" dependencies: "@types/codemirror": ^0.0.90 - graphql-language-service: 5.2.0 + graphql-language-service: 5.2.2 peerDependencies: "@codemirror/language": 6.0.0 codemirror: ^5.65.3 graphql: ^15.5.0 || ^16.0.0 - checksum: cdbeb713af63a069c5526f864edf4f71dd811437e44dba7967164ea2d380c52dfe51d3ea3fde06c94ffeb596b93af737767923d6fc434c628fa4241621684950 + checksum: 7ecb2ba2716448b3c10ccc6168dbe4c636d2d82695beea40cda6f4938b4821e9abcf6446c99412bf59558340f10b45e5b74dce5f80ae4be4409b8a6b7bfcc5ec languageName: node linkType: hard @@ -24719,6 +24746,13 @@ __metadata: languageName: node linkType: hard +"debounce-promise@npm:^3.1.2": + version: 3.1.2 + resolution: "debounce-promise@npm:3.1.2" + checksum: 29bac4524c423cc852319d7455363909ea3d933a3b9e3eb1149d963cffc34c475fe37219d0bafc61af566500b5d663cba579bbad7ee4023bef06f8394ed900ad + languageName: node + linkType: hard + "debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -28887,17 +28921,18 @@ __metadata: languageName: node linkType: hard -"graphql-language-service@npm:5.2.0, graphql-language-service@npm:^5.2.0": - version: 5.2.0 - resolution: "graphql-language-service@npm:5.2.0" +"graphql-language-service@npm:5.2.2, graphql-language-service@npm:^5.2.0, graphql-language-service@npm:^5.2.2": + version: 5.2.2 + resolution: "graphql-language-service@npm:5.2.2" dependencies: + debounce-promise: ^3.1.2 nullthrows: ^1.0.0 vscode-languageserver-types: ^3.17.1 peerDependencies: graphql: ^15.5.0 || ^16.0.0 bin: graphql: dist/temp-bin.js - checksum: b053c6b7158d0ee7a3e55391bfd8be956fc5380211ca586b3a252007845e119540fb40efcc438975eaebc5ef25f46973f7ff4d9543c66e14ebd992957e0299b7 + checksum: 3f112ba67ae10096ac4a415e946cddad019ce9bd5467e29730fdfe08b4470f5b4126412edd6bfcdf6cfbd816c65e295da21134ae501e577540b319f521bc032d languageName: node linkType: hard @@ -32742,6 +32777,15 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:^5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: ^2.0.0 + checksum: b0b86cadaf816b64c947a83994ceaad1c15f9fe7e079776ab88699fb71afd7b8fc3fd3d0ae5ebec8c92c1d347be9ba257b8aef338c0ebf81b0d27dcf429a765a + languageName: node + linkType: hard + "linkify-react@npm:4.1.3": version: 4.1.3 resolution: "linkify-react@npm:4.1.3" @@ -33524,6 +33568,22 @@ __metadata: languageName: node linkType: hard +"markdown-it@npm:^14.1.0": + version: 14.1.0 + resolution: "markdown-it@npm:14.1.0" + dependencies: + argparse: ^2.0.1 + entities: ^4.4.0 + linkify-it: ^5.0.0 + mdurl: ^2.0.0 + punycode.js: ^2.3.1 + uc.micro: ^2.1.0 + bin: + markdown-it: bin/markdown-it.mjs + checksum: 07296b45ebd0b13a55611a24d1b1ad002c6729ec54f558f597846994b0b7b1de79d13cd99ff3e7b6e9e027f36b63125cdcf69174da294ecabdd4e6b9fff39e5d + languageName: node + linkType: hard + "markdown-table@npm:^3.0.0": version: 3.0.1 resolution: "markdown-table@npm:3.0.1" @@ -33768,6 +33828,13 @@ __metadata: languageName: node linkType: hard +"mdurl@npm:^2.0.0": + version: 2.0.0 + resolution: "mdurl@npm:2.0.0" + checksum: 880bc289ef668df0bb34c5b2b5aaa7b6ea755052108cdaf4a5e5968ad01cf27e74927334acc9ebcc50a8628b65272ae6b1fd51fae1330c130e261c0466e1a3b2 + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -38136,6 +38203,13 @@ __metadata: languageName: node linkType: hard +"punycode.js@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 13466d7ed5e8dacdab8c4cc03837e7dd14218a59a40eb14a837f1f53ca396e18ef2c4ee6d7766b8ed2fc391d6a3ac489eebf2de83b3596f5a54e86df4a251b72 + languageName: node + linkType: hard + "punycode@npm:1.3.2": version: 1.3.2 resolution: "punycode@npm:1.3.2" @@ -43422,6 +43496,13 @@ __metadata: languageName: node linkType: hard +"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 37197358242eb9afe367502d4638ac8c5838b78792ab218eafe48287b0ed28aaca268ec0392cc5729f6c90266744de32c06ae938549aee041fc93b0f9672d6b2 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.17.0 resolution: "uglify-js@npm:3.17.0" From c0f371f08b11fae8954540d83a86c6a24f970ca8 Mon Sep 17 00:00:00 2001 From: zeshanziya Date: Thu, 1 Aug 2024 19:07:03 +0530 Subject: [PATCH 084/204] remove deperecation Signed-off-by: zeshanziya --- docs/backend-system/core-services/root-logger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md index 733b7b8277..b0bd82cda9 100644 --- a/docs/backend-system/core-services/root-logger.md +++ b/docs/backend-system/core-services/root-logger.md @@ -17,7 +17,7 @@ The following example is how you can override the root logger service to add add ```ts import { coreServices } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; +import { WinstonLogger } from '@backstage/backend-defaults/rootLogger'; const backend = createBackend(); From df7e7f57d2f657e545f1a4d7499473d3449e3c45 Mon Sep 17 00:00:00 2001 From: zeshanziya Date: Thu, 1 Aug 2024 19:08:15 +0530 Subject: [PATCH 085/204] add secret redactor example Signed-off-by: zeshanziya --- docs/backend-system/core-services/root-logger.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md index b0bd82cda9..36bb276920 100644 --- a/docs/backend-system/core-services/root-logger.md +++ b/docs/backend-system/core-services/root-logger.md @@ -18,6 +18,7 @@ The following example is how you can override the root logger service to add add ```ts import { coreServices } from '@backstage/backend-plugin-api'; import { WinstonLogger } from '@backstage/backend-defaults/rootLogger'; +import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootConfig'; const backend = createBackend(); @@ -43,6 +44,12 @@ backend.add( transports: [new transports.Console()], }); + const secretEnumerator = await createConfigSecretEnumerator({ + logger, + }); + logger.addRedactions(secretEnumerator(config)); + config.subscribe?.(() => logger.addRedactions(secretEnumerator(config))); + return logger; }, }), From 530ca74ce87509707ac65c8943813eb06d37680e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 1 Aug 2024 16:45:07 +0200 Subject: [PATCH 086/204] plugins/search-*: fix metadata ordering Signed-off-by: Patrik Oldsberg --- plugins/search-backend-node/package.json | 6 +++--- plugins/search-backend/package.json | 6 +++--- plugins/search-common/package.json | 6 +++--- plugins/search-react/package.json | 6 +++--- plugins/search/package.json | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index e2398e8245..01337796b0 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -6,11 +6,11 @@ "role": "node-library", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search-backend-node", + "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", - "@backstage/plugin-search-react", - "@backstage/plugin-search" + "@backstage/plugin-search-react" ] }, "publishConfig": { diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 2290a753ec..0e39ff2638 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -6,11 +6,11 @@ "role": "backend-plugin", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search-backend-node", + "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", - "@backstage/plugin-search-react", - "@backstage/plugin-search" + "@backstage/plugin-search-react" ] }, "publishConfig": { diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index 7f0462f69d..c58aff0c21 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -6,11 +6,11 @@ "role": "common-library", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search-backend-node", + "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", - "@backstage/plugin-search-react", - "@backstage/plugin-search" + "@backstage/plugin-search-react" ] }, "publishConfig": { diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 6d3933b1e4..87ebe36f14 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -5,11 +5,11 @@ "role": "web-library", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search-backend-node", + "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", - "@backstage/plugin-search-react", - "@backstage/plugin-search" + "@backstage/plugin-search-react" ] }, "publishConfig": { diff --git a/plugins/search/package.json b/plugins/search/package.json index e772207d4b..e1cbfcd76a 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -6,11 +6,11 @@ "role": "frontend-plugin", "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search-backend-node", + "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", - "@backstage/plugin-search-react", - "@backstage/plugin-search" + "@backstage/plugin-search-react" ] }, "publishConfig": { From 8c1aa06bcc4890f24b78a0d9966d60d639d21651 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Thu, 1 Aug 2024 22:56:40 +0200 Subject: [PATCH 087/204] fix(kubernetes-backend): add customResources to the config.d.ts (#25866) Signed-off-by: Thomas Cardonne --- .changeset/silly-cycles-tan.md | 6 ++++++ plugins/kubernetes-backend/config.d.ts | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/silly-cycles-tan.md diff --git a/.changeset/silly-cycles-tan.md b/.changeset/silly-cycles-tan.md new file mode 100644 index 0000000000..517ce526ea --- /dev/null +++ b/.changeset/silly-cycles-tan.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. +This was already documented and supported by the plugin. diff --git a/plugins/kubernetes-backend/config.d.ts b/plugins/kubernetes-backend/config.d.ts index 777a3392db..d6c5f1d32b 100644 --- a/plugins/kubernetes-backend/config.d.ts +++ b/plugins/kubernetes-backend/config.d.ts @@ -68,6 +68,11 @@ export interface Config { caData?: string; /** @visibility secret */ caFile?: string; + customResources?: Array<{ + group: string; + apiVersion: string; + plural: string; + }>; }>; } | { From fff0b82cd7dd8437bf19c275e512b9f6ae2e2bc5 Mon Sep 17 00:00:00 2001 From: solimant Date: Thu, 1 Aug 2024 20:51:34 -0400 Subject: [PATCH 088/204] Fix highlight typo in 05-frontend-authorization.md Signed-off-by: solimant --- docs/permissions/plugin-authors/05-frontend-authorization.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index 60458aaf4d..4d673e3b4f 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -199,7 +199,8 @@ const routes = ( - {/* highlight-add-end */}} + {/* highlight-add-end */} + }> {/* ... */} From 24310a7e250307315333cf0fb8171c544a96a453 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Fri, 2 Aug 2024 09:15:46 +0200 Subject: [PATCH 089/204] microsite: Azure Storage Explorer plugin updated Signed-off-by: Deepankumar Loganathan --- microsite/data/plugins/azure-storage.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/microsite/data/plugins/azure-storage.yaml b/microsite/data/plugins/azure-storage.yaml index 655a6421ed..6e723c0eeb 100644 --- a/microsite/data/plugins/azure-storage.yaml +++ b/microsite/data/plugins/azure-storage.yaml @@ -4,9 +4,9 @@ author: Deepankumar authorUrl: https://github.com/deepan10 category: Infrastructure description: Explore Azure Storage Blobs in Backstage. -documentation: https://github.com/deepan10/backstage-plugins/tree/main/plugins/azure-storage +documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/azure-storage-explorer/plugins/azure-storage iconUrl: /img/azure-storage-folder.png -npmPackageName: 'backstage-plugin-azure-storage' +npmPackageName: '@backstage-community/plugin-azure-storage-explorer' tags: - Azure - Azure Storage From 93095eecea5c40f8a2949d179caa5aaed57b5b55 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Fri, 2 Aug 2024 12:11:32 +0300 Subject: [PATCH 090/204] chore(deps): bump node-fetch to ^2.7.0 Relates to #24590 Signed-off-by: Heikki Hellgren --- .changeset/fair-pumas-hang.md | 43 ++++++++++ packages/backend-app-api/package.json | 2 +- packages/backend-common/package.json | 2 +- packages/backend-defaults/package.json | 2 +- packages/cli/package.json | 2 +- packages/config-loader/package.json | 2 +- packages/create-app/package.json | 2 +- plugins/app-backend/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- plugins/auth-backend/package.json | 2 +- plugins/auth-node/package.json | 2 +- .../catalog-backend-module-azure/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/devtools-backend/package.json | 2 +- plugins/kubernetes-backend/package.json | 2 +- plugins/kubernetes-node/package.json | 2 +- plugins/notifications-backend/package.json | 2 +- plugins/notifications-node/package.json | 2 +- plugins/permission-backend/package.json | 2 +- plugins/proxy-backend/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- plugins/signals-backend/package.json | 2 +- plugins/techdocs-backend/package.json | 2 +- yarn.lock | 78 +++++++++---------- 41 files changed, 121 insertions(+), 78 deletions(-) create mode 100644 .changeset/fair-pumas-hang.md diff --git a/.changeset/fair-pumas-hang.md b/.changeset/fair-pumas-hang.md new file mode 100644 index 0000000000..156779b76a --- /dev/null +++ b/.changeset/fair-pumas-hang.md @@ -0,0 +1,43 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-scaffolder-backend-module-gerrit': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-scaffolder-backend-module-gitea': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-search-backend-module-explore': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-notifications-node': patch +'@backstage/plugin-permission-backend': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-kubernetes-node': patch +'@backstage/plugin-signals-backend': patch +'@backstage/config-loader': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/create-app': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/cli': patch +--- + +Make sure node-fetch is version 2.7.0 or greater diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index adfd2476a2..6a4e3a669b 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -79,7 +79,7 @@ "minimatch": "^9.0.0", "minimist": "^1.2.5", "morgan": "^1.10.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "path-to-regexp": "^6.2.1", "selfsigned": "^2.0.0", diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 04353fc04c..17913d87d4 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -102,7 +102,7 @@ "minimist": "^1.2.5", "morgan": "^1.10.0", "mysql2": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "p-limit": "^3.1.0", "path-to-regexp": "^6.2.1", diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 7114e7090a..196c9b188a 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -165,7 +165,7 @@ "minimist": "^1.2.5", "morgan": "^1.10.0", "mysql2": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "p-limit": "^3.1.0", "path-to-regexp": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 0b746fbaf8..e54ea18e8e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -121,7 +121,7 @@ "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-libs-browser": "^2.2.1", "npm-packlist": "^5.0.0", "ora": "^5.3.0", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 08589bfca4..1b49af9cf0 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -49,7 +49,7 @@ "json-schema-traverse": "^1.0.0", "lodash": "^4.17.21", "minimist": "^1.2.5", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "typescript-json-schema": "^0.63.0", "yaml": "^2.0.0" }, diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 848b6640e7..a4a967de1a 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -39,7 +39,7 @@ "fs-extra": "^11.2.0", "handlebars": "^4.7.3", "inquirer": "^8.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "ora": "^5.3.0", "recursive-readdir": "^2.2.2" }, diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 7ca6b7924d..2bd66c7ca0 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -82,7 +82,7 @@ "@backstage/cli": "workspace:^", "@backstage/types": "workspace:^", "@types/supertest": "^2.0.8", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "supertest": "^6.1.3" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index c7a03d7177..cb1649a23e 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -44,7 +44,7 @@ "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", "node-cache": "^5.1.2", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index e33a50c25d..2cd60873d3 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -40,7 +40,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index eab709c3a3..e80adbac1a 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -38,7 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "passport-microsoft": "^1.0.0" }, "devDependencies": { diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 086e5de7c5..8a3d2966a8 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -87,7 +87,7 @@ "minimatch": "^9.0.0", "morgan": "^1.10.0", "node-cache": "^5.1.2", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "openid-client": "^5.2.1", "passport": "^0.7.0", "passport-auth0": "^1.4.3", diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 3ce4490790..bb0057f42a 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -49,7 +49,7 @@ "express": "^4.17.1", "jose": "^5.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "passport": "^0.7.0", "winston": "^3.2.1", "zod": "^3.22.4", diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 02f4094131..39f889cb54 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -58,7 +58,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 7f35ea702c..7d6f1a38b3 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -59,7 +59,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@types/node-fetch": "^2.5.12", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index abec87fab7..6358590dae 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -55,7 +55,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index ca2042f214..c6af27c2ce 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -67,7 +67,7 @@ "git-url-parse": "^14.0.0", "lodash": "^4.17.21", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 8cc2b6ac3a..2b44a0b75e 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -62,7 +62,7 @@ "@backstage/plugin-events-node": "workspace:^", "@gitbeaker/rest": "^40.0.3", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index fc0502201b..a889d6a22a 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -62,7 +62,7 @@ "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.0.2", "qs": "^6.9.4", "uuid": "^9.0.0" diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index ad76a864d3..fd9384b567 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -63,7 +63,7 @@ "@backstage/types": "workspace:^", "lodash": "^4.17.21", "luxon": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index c78d5dc578..cb3cc31cca 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -90,7 +90,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.0.2", "prom-client": "^15.0.0", "uuid": "^9.0.0", diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index f2f39dea9a..630ea213b8 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -56,7 +56,7 @@ "express-promise-router": "^4.1.0", "fs-extra": "^11.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "ping": "^0.4.1", "semver": "^7.5.3", "yn": "^4.0.0" diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 71154e0712..4b69c912b8 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -92,7 +92,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "morgan": "^1.10.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 7597206bd0..f238334824 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -43,7 +43,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/types": "workspace:^", "@kubernetes/client-node": "^0.20.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 5916b18116..8bb3fd2acf 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -52,7 +52,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index e3b61e2734..d71fb7af82 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -44,7 +44,7 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", "knex": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 100bde718f..a5b6a9fb59 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -63,7 +63,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yn": "^4.0.0", "zod": "^3.22.4" }, diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index ad4627b907..655ccfed9d 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -79,7 +79,7 @@ "@types/uuid": "^9.0.0", "@types/yup": "^0.32.0", "msw": "^2.0.0", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "configSchema": "config.d.ts" } diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index f5bbd2df63..0446c39b0c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -49,7 +49,7 @@ "@backstage/plugin-bitbucket-cloud-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index c4c00f96e7..0ce8ac90e8 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -48,7 +48,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index 584f61b502..e29e75ee12 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -50,7 +50,7 @@ "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 5b54e3841d..a0d5631608 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -53,7 +53,7 @@ "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", "git-url-parse": "^14.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-html-markdown": "^1.3.0", "yaml": "^2.0.0" }, diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index 68bdd36f44..f39e67cedc 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -47,7 +47,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 432daf6c8b..14b0305b10 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -47,7 +47,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 069f310dcc..81171ea78d 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -46,7 +46,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.3.3" }, "devDependencies": { diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 7827067c8a..9c6197e8ca 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -55,7 +55,7 @@ "@backstage/config": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index 7f66d093d8..08236889af 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -41,7 +41,7 @@ "@backstage/config": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "qs": "^6.9.4" }, "devDependencies": { diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index ec703cf38e..0dc3a362d3 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -61,7 +61,7 @@ "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-techdocs-node": "workspace:^", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.1.0" }, "devDependencies": { diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 6e4a17d612..05e7189beb 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -48,7 +48,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "http-proxy-middleware": "^2.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0", "winston": "^3.2.1", "ws": "^8.17.0", diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index f12a32a1ec..448f88e800 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -76,7 +76,7 @@ "fs-extra": "^11.2.0", "knex": "^3.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.1.0", "winston": "^3.2.1" }, diff --git a/yarn.lock b/yarn.lock index b321863309..198a6310f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3503,7 +3503,7 @@ __metadata: minimist: ^1.2.5 morgan: ^1.10.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 path-to-regexp: ^6.2.1 selfsigned: ^2.0.0 @@ -3588,7 +3588,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 mysql2: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 p-limit: ^3.1.0 path-to-regexp: ^6.2.1 @@ -3677,7 +3677,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 mysql2: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 p-limit: ^3.1.0 path-to-regexp: ^6.2.1 @@ -4009,7 +4009,7 @@ __metadata: mini-css-extract-plugin: ^2.4.2 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-libs-browser: ^2.2.1 nodemon: ^3.0.1 npm-packlist: ^5.0.0 @@ -4103,7 +4103,7 @@ __metadata: lodash: ^4.17.21 minimist: ^1.2.5 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 typescript-json-schema: ^0.63.0 yaml: ^2.0.0 zen-observable: ^0.10.0 @@ -4395,7 +4395,7 @@ __metadata: handlebars: ^4.7.3 inquirer: ^8.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 nodemon: ^3.0.1 ora: ^5.3.0 recursive-readdir: ^2.2.2 @@ -4745,7 +4745,7 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.3 yn: ^4.0.0 languageName: unknown @@ -4815,7 +4815,7 @@ __metadata: jose: ^5.0.0 msw: ^2.0.8 node-cache: ^5.1.2 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 languageName: unknown linkType: soft @@ -4870,7 +4870,7 @@ __metadata: express: ^4.18.2 jose: ^5.0.0 msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-mocks-http: ^1.0.0 uuid: ^9.0.0 languageName: unknown @@ -4971,7 +4971,7 @@ __metadata: express: ^4.18.2 jose: ^5.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 passport-microsoft: ^1.0.0 supertest: ^6.3.3 languageName: unknown @@ -5170,7 +5170,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 node-cache: ^5.1.2 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 openid-client: ^5.2.1 passport: ^0.7.0 passport-auth0: ^1.4.3 @@ -5207,7 +5207,7 @@ __metadata: jose: ^5.0.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 passport: ^0.7.0 supertest: ^6.1.3 uuid: ^9.0.0 @@ -5300,7 +5300,7 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5366,7 +5366,7 @@ __metadata: "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5405,7 +5405,7 @@ __metadata: fs-extra: ^11.2.0 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5452,7 +5452,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5496,7 +5496,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5582,7 +5582,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.0.2 qs: ^6.9.4 uuid: ^9.0.0 @@ -5629,7 +5629,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5712,7 +5712,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.0.2 prom-client: ^15.0.0 supertest: ^6.1.3 @@ -6063,7 +6063,7 @@ __metadata: express-promise-router: ^4.1.0 fs-extra: ^11.0.0 lodash: ^4.17.21 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 ping: ^0.4.1 semver: ^7.5.3 supertest: ^6.2.4 @@ -6338,7 +6338,7 @@ __metadata: luxon: ^3.0.0 morgan: ^1.10.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 stream-buffers: ^3.0.2 supertest: ^6.1.3 winston: ^3.2.1 @@ -6411,7 +6411,7 @@ __metadata: "@backstage/types": "workspace:^" "@kubernetes/client-node": ^0.20.0 msw: ^1.3.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.3 winston: ^3.2.1 languageName: unknown @@ -6545,7 +6545,7 @@ __metadata: express-promise-router: ^4.1.0 knex: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.2.4 uuid: ^9.0.0 winston: ^3.2.1 @@ -6578,7 +6578,7 @@ __metadata: "@backstage/test-utils": "workspace:^" knex: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -6720,7 +6720,7 @@ __metadata: express-promise-router: ^4.1.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.6 yn: ^4.0.0 zod: ^3.22.4 @@ -6840,7 +6840,7 @@ __metadata: http-proxy-middleware: ^2.0.0 morgan: ^1.10.0 msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 @@ -6880,7 +6880,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6899,7 +6899,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6920,7 +6920,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6941,7 +6941,7 @@ __metadata: fs-extra: ^11.2.0 git-url-parse: ^14.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-html-markdown: ^1.3.0 yaml: ^2.0.0 languageName: unknown @@ -7000,7 +7000,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -7018,7 +7018,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -7123,7 +7123,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.3.3 languageName: unknown linkType: soft @@ -7455,7 +7455,7 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.2.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 languageName: unknown linkType: soft @@ -7490,7 +7490,7 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.2.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 qs: ^6.9.4 languageName: unknown linkType: soft @@ -7515,7 +7515,7 @@ __metadata: "@backstage/plugin-techdocs-node": "workspace:^" lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.1.0 languageName: unknown linkType: soft @@ -7686,7 +7686,7 @@ __metadata: express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.2.4 uuid: ^9.0.0 winston: ^3.2.1 @@ -7809,7 +7809,7 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.1.0 supertest: ^6.1.3 winston: ^3.2.1 From 5020910446d6e8cbc0124ab3149daf8653c12154 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 2 Aug 2024 14:56:17 +0200 Subject: [PATCH 091/204] frontend-plugin-api: add tests for generator factories + error on undeclared output Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.test.ts | 5 +- .../src/wiring/createExtension.test.ts | 99 +++++++++++++++++++ .../src/wiring/createExtension.ts | 11 ++- .../wiring/createExtensionBlueprint.test.tsx | 39 ++++++++ 4 files changed, 150 insertions(+), 4 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index b4090185a3..de8889daf0 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -914,7 +914,7 @@ describe('instantiateAppNodeTree', () => { namespace: 'app', name: 'test', attachTo: { id: 'ignored', input: 'ignored' }, - output: [], + output: [testDataRef], factory() { const error = new Error('NOPE'); error.name = 'NopeError'; @@ -981,11 +981,12 @@ describe('instantiateAppNodeTree', () => { createAppNodeInstance({ node: makeNode( resolveExtensionDefinition( + // @ts-expect-error createExtension({ namespace: 'app', name: 'test', attachTo: { id: 'ignored', input: 'ignored' }, - output: [], + output: [], // Output not declared factory({}) { return [testDataRef('test')] as any; }, diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 327639af37..ab6a6c19a5 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -394,6 +394,18 @@ describe('createExtension', () => { }), ).toMatchObject({ version: 'v2' }); + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + factory() { + return [stringDataRef('hello'), numberDataRef(4)]; + }, + }), + ).toMatchObject({ version: 'v2' }); + expect( createExtension({ namespace: 'test', @@ -428,6 +440,93 @@ describe('createExtension', () => { ).toMatchObject({ version: 'v2' }); }); + it('should support new form of outputs with a generator', () => { + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + // Missing all outputs + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + yield stringDataRef('hello'); // Missing number output + }, + }), + ).toMatchObject({ version: 'v2' }); + + // Duplicate output, we won't attempt to handle this a compile time and instead error out at runtime + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + *factory() { + yield stringDataRef('hello'); + yield stringDataRef('hello'); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); // No declared output + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + *factory() { + yield stringDataRef('hello'); // Missing number output, but it's optional so that's allowed + }, + }), + ).toMatchObject({ version: 'v2' }); + }); + it('should support new form of inputs', () => { expect( createExtension({ diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 4d31982ed3..a8bf4ef17c 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -157,9 +157,16 @@ export type VerifyExtensionFactoryOutput< : never ) extends infer IRequiredOutputIds ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? {} + ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] + ? {} + : { + 'Error: The extension factory has undeclared output(s)': Exclude< + UFactoryOutput['id'], + UDeclaredOutput['id'] + >; + } : { - 'Error: The extension factory is missing the following outputs': Exclude< + 'Error: The extension factory is missing the following output(s)': Exclude< IRequiredOutputIds, UFactoryOutput['id'] >; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index f57cc2b70e..6cf10d7f08 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -64,6 +64,45 @@ describe('createExtensionBlueprint', () => { expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); }); + it('should allow creation of extension blueprints with a generator', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + *factory(params: { text: string }) { + yield coreExtensionData.reactElement(

{params.text}

); + }, + }); + + const extension = TestExtensionBlueprint.make({ + name: 'my-extension', + params: { + text: 'Hello, world!', + }, + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + attachTo: { + id: 'test', + input: 'default', + }, + configSchema: undefined, + disabled: false, + inputs: {}, + kind: 'test-extension', + name: 'my-extension', + namespace: undefined, + output: [coreExtensionData.reactElement], + factory: expect.any(Function), + toString: expect.any(Function), + version: 'v2', + }); + + const { container } = createExtensionTester(extension).render(); + expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); + }); + it('should allow overriding of the default factory', () => { const TestExtensionBlueprint = createExtensionBlueprint({ kind: 'test-extension', From 7e1dfe7bd6a277c8454f4539195ca307553c0f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jano=20Gonz=C3=A1lez?= Date: Fri, 2 Aug 2024 15:58:50 +0200 Subject: [PATCH 092/204] feat: introduce subdomains card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jano González --- plugins/catalog/src/alpha/entityCards.tsx | 9 ++ .../HasSubdomainsCard.test.tsx | 122 ++++++++++++++++++ .../HasSubdomainsCard/HasSubdomainsCard.tsx | 55 ++++++++ .../src/components/HasSubdomainsCard/index.ts | 18 +++ plugins/catalog/src/index.ts | 2 + plugins/catalog/src/plugin.ts | 14 ++ plugins/catalog/src/translation.ts | 4 + 7 files changed, 224 insertions(+) create mode 100644 plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx create mode 100644 plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx create mode 100644 plugins/catalog/src/components/HasSubdomainsCard/index.ts diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index da6a6a669e..332bd49c55 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -84,6 +84,14 @@ export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({ ), }); +export const catalogHasSubdomainsEntityCard = createEntityCardExtension({ + name: 'has-subdomains', + loader: async () => + import('../components/HasSubdomainsCard').then(m => + compatWrapper(), + ), +}); + export const catalogHasSystemsEntityCard = createEntityCardExtension({ name: 'has-systems', loader: async () => @@ -101,5 +109,6 @@ export default [ catalogHasComponentsEntityCard, catalogHasResourcesEntityCard, catalogHasSubcomponentsEntityCard, + catalogHasSubdomainsEntityCard, catalogHasSystemsEntityCard, ]; diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx new file mode 100644 index 0000000000..6c145b3228 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright 2020 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 { Entity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { waitFor, screen } from '@testing-library/react'; +import React from 'react'; +import { HasSubdomainsCard } from './HasSubdomainsCard'; + +describe('', () => { + const getEntitiesByRefs: jest.MockedFunction< + CatalogApi['getEntitiesByRefs'] + > = jest.fn(); + let Wrapper: React.ComponentType>; + + beforeEach(() => { + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [], + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('Has subdomains')).toBeInTheDocument(); + expect( + screen.getByText(/No subdomain is part of this domain/i), + ).toBeInTheDocument(); + }); + + it('shows related subdomains', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [ + { + targetRef: 'domain:my-namespace/target-name', + type: RELATION_HAS_PART, + }, + ], + }; + getEntitiesByRefs.mockResolvedValue({ + items: [ + { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: {}, + }, + ], + }); + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(screen.getByText('Has subdomains')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx new file mode 100644 index 0000000000..811f74d820 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2020 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 { RELATION_HAS_PART } from '@backstage/catalog-model'; +import { InfoCardVariants, TableOptions } from '@backstage/core-components'; +import React from 'react'; +import { + asComponentEntities, + componentEntityColumns, + RelatedEntitiesCard, +} from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @public */ +export interface HasSubdomainsCardProps { + variant?: InfoCardVariants; + tableOptions?: TableOptions; + title?: string; +} + +export function HasSubdomainsCard(props: HasSubdomainsCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); + const { + variant = 'gridItem', + tableOptions = {}, + title = t('hasSubdomainsCard.title'), + } = props; + return ( + + ); +} diff --git a/plugins/catalog/src/components/HasSubdomainsCard/index.ts b/plugins/catalog/src/components/HasSubdomainsCard/index.ts new file mode 100644 index 0000000000..a740abbcc2 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 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 { HasSubdomainsCard } from './HasSubdomainsCard'; +export type { HasSubdomainsCardProps } from './HasSubdomainsCard'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index a691011c96..806ea40c66 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -49,6 +49,7 @@ export { EntityHasComponentsCard, EntityHasResourcesCard, EntityHasSubcomponentsCard, + EntityHasSubdomainsCard, EntityHasSystemsCard, EntityLinksCard, EntityLabelsCard, @@ -71,6 +72,7 @@ export type { EntityContextMenuClassKey } from './components/EntityContextMenu'; export type { HasComponentsCardProps } from './components/HasComponentsCard'; export type { HasResourcesCardProps } from './components/HasResourcesCard'; export type { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +export type { HasSubdomainsCardProps } from './components/HasSubdomainsCard'; export type { HasSystemsCardProps } from './components/HasSystemsCard'; export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; export type { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 33ceaef632..a339ff31a8 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -50,6 +50,7 @@ import { DependsOnResourcesCardProps } from './components/DependsOnResourcesCard import { HasComponentsCardProps } from './components/HasComponentsCard'; import { HasResourcesCardProps } from './components/HasResourcesCard'; import { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +import { HasSubdomainsCardProps } from './components/HasSubdomainsCard'; import { HasSystemsCardProps } from './components/HasSystemsCard'; import { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; import { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; @@ -199,6 +200,19 @@ export const EntityHasSubcomponentsCard: ( }), ); +/** @public */ +export const EntityHasSubdomainsCard: ( + props: HasSubdomainsCardProps, +) => JSX.Element = catalogPlugin.provide( + createComponentExtension({ + name: 'EntityHasSubdomainsCard', + component: { + lazy: () => + import('./components/HasSubdomainsCard').then(m => m.HasSubdomainsCard), + }, + }), +); + /** @public */ export const EntityHasResourcesCard: ( props: HasResourcesCardProps, diff --git a/plugins/catalog/src/translation.ts b/plugins/catalog/src/translation.ts index 2699b58542..f38a34be84 100644 --- a/plugins/catalog/src/translation.ts +++ b/plugins/catalog/src/translation.ts @@ -143,6 +143,10 @@ export const catalogTranslationRef = createTranslationRef({ title: 'Has subcomponents', emptyMessage: 'No subcomponent is part of this component', }, + hasSubdomainsCard: { + title: 'Has subdomains', + emptyMessage: 'No subdomain is part of this domain', + }, hasSystemsCard: { title: 'Has systems', emptyMessage: 'No system is part of this domain', From f889bf7ac8a2df69312831fd13f6f7bc5cf7e6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jano=20Gonz=C3=A1lez?= Date: Fri, 2 Aug 2024 15:59:07 +0200 Subject: [PATCH 093/204] chore: update api report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jano González --- plugins/catalog/api-report-alpha.md | 2 ++ plugins/catalog/api-report.md | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index a62b047c4c..f79d65394a 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -85,6 +85,8 @@ export const catalogTranslationRef: TranslationRef< readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system'; readonly 'hasSubcomponentsCard.title': 'Has subcomponents'; readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component'; + readonly 'hasSubdomainsCard.title': 'Has subdomains'; + readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain'; readonly 'hasSystemsCard.title': 'Has systems'; readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain'; readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.'; diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index c1fb52fb94..95333a6451 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -370,6 +370,11 @@ export const EntityHasSubcomponentsCard: ( props: HasSubcomponentsCardProps, ) => JSX.Element; +// @public (undocumented) +export const EntityHasSubdomainsCard: ( + props: HasSubdomainsCardProps, +) => JSX.Element; + // @public (undocumented) export const EntityHasSystemsCard: (props: HasSystemsCardProps) => JSX.Element; @@ -551,6 +556,16 @@ export interface HasSubcomponentsCardProps { variant?: InfoCardVariants; } +// @public (undocumented) +export interface HasSubdomainsCardProps { + // (undocumented) + tableOptions?: TableOptions; + // (undocumented) + title?: string; + // (undocumented) + variant?: InfoCardVariants; +} + // @public (undocumented) export interface HasSystemsCardProps { // (undocumented) From 6925dcb4517422523281682ee2383a8f34ddf9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jano=20Gonz=C3=A1lez?= Date: Fri, 2 Aug 2024 16:21:11 +0200 Subject: [PATCH 094/204] chore: add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jano González --- .changeset/metal-rice-call.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/metal-rice-call.md diff --git a/.changeset/metal-rice-call.md b/.changeset/metal-rice-call.md new file mode 100644 index 0000000000..46e30dd26f --- /dev/null +++ b/.changeset/metal-rice-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Introduces the HasSubdomainsCard component that displays the subdomains of a given domain From 67e76f2bb93509145ce625e7e85336c7a42c890c Mon Sep 17 00:00:00 2001 From: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:36:13 -0400 Subject: [PATCH 095/204] Transform meta redirect tags (#25766) * Add TechDocs support for the mkdocs-redirects plugin. --------- Signed-off-by: Sydney Achinger --- .changeset/silly-candles-sin.md | 5 ++ .../TechDocsReaderPageContent/dom.tsx | 4 +- .../transformers/handleMetaRedirects.test.ts | 86 +++++++++++++++++++ .../transformers/handleMetaRedirects.ts | 59 +++++++++++++ .../transformers/html/transformer.test.tsx | 40 +++++++++ .../reader/transformers/html/transformer.ts | 24 +++++- 6 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 .changeset/silly-candles-sin.md create mode 100644 plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts create mode 100644 plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts diff --git a/.changeset/silly-candles-sin.md b/.changeset/silly-candles-sin.md new file mode 100644 index 0000000000..b1d55113fd --- /dev/null +++ b/.changeset/silly-candles-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index de2460a543..c686827255 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -46,6 +46,7 @@ import { useStylesTransformer, } from '../../transformers'; import { useNavigateUrl } from './useNavigateUrl'; +import { handleMetaRedirects } from '../../transformers/handleMetaRedirects'; const MOBILE_MEDIA_QUERY = 'screen and (max-width: 76.1875em)'; @@ -186,6 +187,7 @@ export const useTechDocsReaderDom = ( const postRender = useCallback( async (transformedElement: Element) => transformer(transformedElement, [ + handleMetaRedirects(navigate, entityRef.name), scrollIntoNavigation(), copyToClipboard(theme), addLinkClickListener({ @@ -243,7 +245,7 @@ export const useTechDocsReaderDom = ( onLoaded: () => {}, }), ]), - [theme, navigate, analytics], + [theme, navigate, analytics, entityRef.name], ); useEffect(() => { diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts new file mode 100644 index 0000000000..8fbd63428d --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { handleMetaRedirects } from './handleMetaRedirects'; +import { createTestShadowDom } from '../../test-utils'; + +describe('handleMetaRedirects', () => { + const navigate = jest.fn(); + + const setUpNewTestShadowDom = async ( + html: string, + rootHref: string, + rootPath: string, + ) => { + const entityName = 'testEntity'; + // Mock window.location.href for each test + Object.defineProperty(window, 'location', { + value: { + href: rootHref, + pathname: rootPath, + hostname: 'localhost', + }, + writable: true, + }); + + return await createTestShadowDom(html, { + preTransformers: [], + postTransformers: [handleMetaRedirects(navigate, entityName)], + }); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should navigate to relative URL if meta redirect tag is present', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + expect(navigate).toHaveBeenCalledWith( + 'http://localhost/docs/default/component/testEntity/anotherPage', + ); + }); + + it('should navigate to site home if meta redirect tag is present and external', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + expect(navigate).toHaveBeenCalledWith('/docs/default/component/testEntity'); + }); + + it('should navigate to absolute URL if meta redirect tag is present and not external', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + expect(navigate).toHaveBeenCalledWith('http://localhost/test'); + }); + + it('should not navigate if meta redirect tag is not present', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + expect(navigate).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts new file mode 100644 index 0000000000..4c56d4d363 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts @@ -0,0 +1,59 @@ +/* + * 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 { Transformer } from './transformer'; +import { normalizeUrl } from './rewriteDocLinks'; + +export const handleMetaRedirects = ( + navigate: (to: string) => void, + entityName: string, +): Transformer => { + return dom => { + for (const elem of Array.from(dom.querySelectorAll('meta'))) { + if (elem.getAttribute('http-equiv') === 'refresh') { + const metaContentParameters = elem + .getAttribute('content') + ?.split('url='); + if (!metaContentParameters || metaContentParameters.length < 2) { + continue; + } + + const metaUrl = metaContentParameters[1]; + const normalizedCurrentUrl = normalizeUrl(window.location.href); + // If metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href when creating URL object. + const absoluteRedirectObj = new URL(metaUrl, normalizedCurrentUrl); + const isExternalRedirect = + absoluteRedirectObj.hostname !== window.location.hostname; + + if (isExternalRedirect) { + // If the redirect is external, navigate to the documentation site home instead of the external url. + const currentTechDocPath = window.location.pathname; + const indexOfSiteHome = currentTechDocPath.indexOf(entityName); + const siteHomePath = currentTechDocPath.slice( + 0, + indexOfSiteHome + entityName.length, + ); + navigate(siteHomePath); + } else { + // The navigate function from dom.tsx is a wrapper around react-router navigate function that helps absolute url redirects. + navigate(absoluteRedirectObj.href); + } + return dom; + } + } + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx index ed5fbba085..e45d66c676 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx @@ -81,4 +81,44 @@ describe('Transformers > Html', () => { expect(iframes).toHaveLength(1); expect(iframes[0].src).toMatch('docs.google.com'); }); + + it('should return a function that allows refresh meta tags', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + `; + const cleanDom = await result.current(dirtyDom); // calling html transformer + + const metaTags = Array.from( + cleanDom.querySelectorAll('meta'), + ); + + expect(metaTags).toHaveLength(1); + expect(metaTags[0].getAttribute('http-equiv')).toEqual('refresh'); + expect(metaTags[0].getAttribute('content')).toEqual( + '0;url=https://test.com', + ); + }); + + it('should return a function that does not allow non-refresh meta tags', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + `; + const cleanDom = await result.current(dirtyDom); // calling html transformer + + const metaTags = Array.from( + cleanDom.querySelectorAll('meta'), + ); + + expect(metaTags).toHaveLength(0); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 61498a37b0..4938a36648 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -44,17 +44,39 @@ export const useSanitizerTransformer = (): Transformer => { const hosts = config?.getOptionalStringArray('allowedIframeHosts'); DOMPurify.addHook('beforeSanitizeElements', removeUnsafeLinks); - const tags = ['link']; + const tags = ['link', 'meta']; if (hosts) { tags.push('iframe'); DOMPurify.addHook('beforeSanitizeElements', removeUnsafeIframes(hosts)); } + // Only allow meta tags if they are used for refreshing the page. They are required for the redirect feature. + DOMPurify.addHook('uponSanitizeElement', (currNode, data) => { + if (data.tagName === 'meta') { + const isMetaRefreshTag = + currNode.getAttribute('http-equiv') === 'refresh' && + currNode.getAttribute('content')?.includes('url='); + if (!isMetaRefreshTag) { + currNode.parentNode?.removeChild(currNode); + } + } + }); + + // Only allow http-equiv and content attributes on meta tags. They are required for the redirect feature. + DOMPurify.addHook('uponSanitizeAttribute', (currNode, data) => { + if (currNode.tagName !== 'meta') { + if (data.attrName === 'http-equiv' || data.attrName === 'content') { + currNode.removeAttribute(data.attrName); + } + } + }); + // using outerHTML as we want to preserve the html tag attributes (lang) return DOMPurify.sanitize(dom.outerHTML, { ADD_TAGS: tags, FORBID_TAGS: ['style'], + ADD_ATTR: ['http-equiv', 'content'], WHOLE_DOCUMENT: true, RETURN_DOM: true, }); From 08528670dc83cfa13678f0784bbd3a4024cd3fbe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 3 Aug 2024 12:42:46 +0200 Subject: [PATCH 096/204] core-compat-api: add forwards compat for app context Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/package.json | 1 + .../compatWrapper/BackwardsCompatProvider.tsx | 4 +- .../compatWrapper/ForwardsCompatProvider.tsx | 102 +++++++++++++++++- .../src/compatWrapper/compatWrapper.test.tsx | 47 ++++++-- yarn.lock | 1 + 5 files changed, 145 insertions(+), 10 deletions(-) diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index a76f7878dd..4a143a50c7 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -44,6 +44,7 @@ "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-catalog": "workspace:^", + "@backstage/test-utils": "workspace:^", "@oriflame/backstage-plugin-score-card": "^0.8.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^15.0.0" diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 288f3f900b..4f9ba3a836 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -50,7 +50,9 @@ const legacyPluginStore = getOrCreateGlobalSingleton( () => new WeakMap(), ); -function toLegacyPlugin(plugin: NewBackstagePlugin): LegacyBackstagePlugin { +export function toLegacyPlugin( + plugin: NewBackstagePlugin, +): LegacyBackstagePlugin { let legacy = legacyPluginStore.get(plugin); if (legacy) { return legacy; diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index 5544862aee..182b774cca 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -14,10 +14,106 @@ * limitations under the License. */ -import React from 'react'; +import { + ApiHolder, + ApiRef, + AppContext, + useApp, +} from '@backstage/core-plugin-api'; +import { + ComponentRef, + ComponentsApi, + CoreErrorBoundaryFallbackProps, + CoreNotFoundErrorPageProps, + CoreProgressProps, + IconComponent, + IconsApi, + componentsApiRef, + coreComponentRefs, + iconsApiRef, +} from '@backstage/frontend-plugin-api'; +import React, { ComponentType, useMemo } from 'react'; import { ReactNode } from 'react'; +import { toLegacyPlugin } from './BackwardsCompatProvider'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { ApiProvider } from '../../../core-app-api/src/apis/system/ApiProvider'; + +class CompatComponentsApi implements ComponentsApi { + readonly #Progress: ComponentType; + readonly #NotFoundErrorPage: ComponentType; + readonly #ErrorBoundaryFallback: ComponentType; + + constructor(app: AppContext) { + const components = app.getComponents(); + const ErrorBoundaryFallback = (props: CoreErrorBoundaryFallbackProps) => ( + + ); + this.#Progress = components.Progress; + this.#NotFoundErrorPage = components.NotFoundErrorPage; + this.#ErrorBoundaryFallback = ErrorBoundaryFallback; + } + + getComponent(ref: ComponentRef): ComponentType { + switch (ref.id) { + case coreComponentRefs.progress.id: + return this.#Progress as ComponentType; + case coreComponentRefs.notFoundErrorPage.id: + return this.#NotFoundErrorPage as ComponentType; + case coreComponentRefs.errorBoundaryFallback.id: + return this.#ErrorBoundaryFallback as ComponentType; + default: + throw new Error( + `No backwards compatible component is available for ref '${ref.id}'`, + ); + } + } +} + +class CompatIconsApi implements IconsApi { + readonly #app: AppContext; + + constructor(app: AppContext) { + this.#app = app; + } + + getIcon(key: string): IconComponent | undefined { + return this.#app.getSystemIcon(key); + } + + listIconKeys(): string[] { + return Object.keys(this.#app.getSystemIcons()); + } +} + +class AppFallbackApis implements ApiHolder { + readonly #componentsApi: ComponentsApi; + readonly #iconsApi: IconsApi; + + constructor(app: AppContext) { + this.#componentsApi = new CompatComponentsApi(app); + this.#iconsApi = new CompatIconsApi(app); + } + + get(ref: ApiRef): T | undefined { + if (ref.id === componentsApiRef.id) { + return this.#componentsApi as T; + } else if (ref.id === iconsApiRef.id) { + return this.#iconsApi as T; + } + return undefined; + } +} + +function NewAppApisProvider(props: { children: ReactNode }) { + const app = useApp(); + const appFallbackApis = useMemo(() => new AppFallbackApis(app), [app]); + + return {props.children}; +} export function ForwardsCompatProvider(props: { children: ReactNode }) { - // TODO(Rugvip): Implement - return <>{props.children}; + return {props.children}; } diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index 74b9f209eb..b17ca2d508 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -16,21 +16,27 @@ import React from 'react'; import { + componentsApiRef, + coreComponentRefs, coreExtensionData, createExtension, + iconsApiRef, + useRouteRef as useNewRouteRef, + useApi, } from '@backstage/frontend-plugin-api'; import { createExtensionTester, - renderInTestApp, + renderInTestApp as renderInNewTestApp, } from '@backstage/frontend-test-utils'; import { screen } from '@testing-library/react'; import { compatWrapper } from './compatWrapper'; import { - createRouteRef, useApp, - useRouteRef, + useRouteRef as useOldRouteRef, + createRouteRef as createOldRouteRef, } from '@backstage/core-plugin-api'; import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; +import { renderInTestApp as renderInOldTestApp } from '@backstage/test-utils'; describe('BackwardsCompatProvider', () => { it('should convert the app context', () => { @@ -74,17 +80,46 @@ describe('BackwardsCompatProvider', () => { }); it('should convert the routing context', () => { - const routeRef = createRouteRef({ id: 'test' }); + const routeRef = createOldRouteRef({ id: 'test' }); function Component() { - const link = useRouteRef(routeRef); + const link = useOldRouteRef(routeRef); return
link: {link()}
; } - renderInTestApp(compatWrapper(), { + renderInNewTestApp(compatWrapper(), { mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, }); expect(screen.getByText('link: /test')).toBeInTheDocument(); }); }); + +describe('ForwardsCompatProvider', () => { + it('should convert the app context', async () => { + function Component() { + const components = useApi(componentsApiRef); + const icons = useApi(iconsApiRef); + return ( +
+ components:{' '} + {Object.entries(coreComponentRefs) + .map( + ([name, ref]) => + `${name}=${Boolean(components.getComponent(ref))}`, + ) + .join(', ')} + {'\n'} + icons: {icons.listIconKeys().join(', ')} +
+ ); + } + + await renderInOldTestApp(compatWrapper()); + + expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(` + "components: progress=true, notFoundErrorPage=true, errorBoundaryFallback=true + icons: kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, user, warning" + `); + }); +}); diff --git a/yarn.lock b/yarn.lock index c1dc608e87..e26ceb0aa6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4171,6 +4171,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/plugin-catalog": "workspace:^" + "@backstage/test-utils": "workspace:^" "@backstage/version-bridge": "workspace:^" "@oriflame/backstage-plugin-score-card": ^0.8.0 "@testing-library/jest-dom": ^6.0.0 From 9ade0f99e5277113a8628eaef21cd90d138d8d62 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 3 Aug 2024 16:01:32 +0200 Subject: [PATCH 097/204] core-compat-api: add test for convertLegacyRouteRef Signed-off-by: Patrik Oldsberg --- .../src/convertLegacyRouteRef.test.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 packages/core-compat-api/src/convertLegacyRouteRef.test.ts diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts new file mode 100644 index 0000000000..3681d0d2d9 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright 2023 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 { + createRouteRef as createOldRouteRef, + createSubRouteRef as createOldSubRouteRef, + createExternalRouteRef as createOldExternalRouteRef, +} from '@backstage/core-plugin-api'; +import { + RouteRef as NewRouteRef, + SubRouteRef as NewSubRouteRef, + ExternalRouteRef as NewExternalRouteRef, +} from '@backstage/frontend-plugin-api'; +import { convertLegacyRouteRef } from './convertLegacyRouteRef'; + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalRouteRef as toInternalNewRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalSubRouteRef as toInternalNewSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalExternalRouteRef as toInternalNewExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef'; + +describe('convertLegacyRouteRef', () => { + it('converts old to new', () => { + const ref1 = createOldRouteRef({ id: 'ref1' }); + const ref2 = createOldRouteRef({ id: 'ref2', params: ['p1', 'p2'] }); + const ref1sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createOldSubRouteRef({ + id: 'sub2', + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createOldExternalRouteRef({ + id: 'ref3', + }); + const ref4 = createOldExternalRouteRef({ + id: 'ref4', + optional: true, + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: NewRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: NewRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: NewExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: NewExternalRouteRef = convertLegacyRouteRef(ref4); + + const ref1Internal = toInternalNewRouteRef(ref1Converted); + const ref2Internal = toInternalNewRouteRef(ref2Converted); + const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted); + const ref1sub2Internal = toInternalNewSubRouteRef(ref1sub2Converted); + const ref2sub1Internal = toInternalNewSubRouteRef(ref2sub1Converted); + const ref3Internal = toInternalNewExternalRouteRef(ref3Converted); + const ref4Internal = toInternalNewExternalRouteRef(ref4Converted); + + expect(ref1Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref1}', + ); + expect(ref1Internal.getParams()).toEqual([]); + expect(ref2Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref2}', + ); + expect(ref2Internal.getParams()).toEqual(['p1', 'p2']); + + expect(ref1sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref1sub1Internal.getParams()).toEqual([]); + expect(ref1sub1Internal.getParent()).toBe(ref1); + expect(ref1sub2Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub2}', + ); + expect(ref1sub2Internal.getParams()).toEqual(['p3']); + expect(ref1sub2Internal.getParent()).toBe(ref1); + expect(ref2sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref2sub1Internal.getParams()).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Internal.getParent()).toBe(ref2); + + expect(ref3Internal.getDefaultTarget()).toBe(undefined); + expect(ref3Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref3}', + ); + expect(ref3Internal.getParams()).toEqual([]); + expect(ref3Internal.optional).toBe(false); + expect(ref4Internal.getDefaultTarget()).toBe('ref2'); + expect(ref4Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref4}', + ); + expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); + expect(ref4Internal.optional).toBe(true); + }); +}); From ef1b2f4033058c0a0842bbbfa865121e584fba9d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 11:28:38 +0200 Subject: [PATCH 098/204] core-compat-api: add support for converting new route refs to legacy Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/api-report.md | 18 +++ .../src/convertLegacyRouteRef.test.ts | 87 ++++++++++++++ .../src/convertLegacyRouteRef.ts | 107 +++++++++++++++++- 3 files changed, 207 insertions(+), 5 deletions(-) diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index be80ca50e9..3cd78e6f30 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -44,6 +44,24 @@ export function convertLegacyRouteRef< ref: ExternalRouteRef, ): ExternalRouteRef_2; +// @public +export function convertLegacyRouteRef( + ref: RouteRef_2, +): RouteRef; + +// @public +export function convertLegacyRouteRef( + ref: SubRouteRef_2, +): SubRouteRef; + +// @public +export function convertLegacyRouteRef< + TParams extends AnyRouteRefParams, + TOptional extends boolean, +>( + ref: ExternalRouteRef_2, +): ExternalRouteRef; + // @public export function convertLegacyRouteRefs< TRefs extends { diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts index 3681d0d2d9..bf82d1e034 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -15,6 +15,9 @@ */ import { + RouteRef as OldRouteRef, + SubRouteRef as OldSubRouteRef, + ExternalRouteRef as OldExternalRouteRef, createRouteRef as createOldRouteRef, createSubRouteRef as createOldSubRouteRef, createExternalRouteRef as createOldExternalRouteRef, @@ -23,6 +26,9 @@ import { RouteRef as NewRouteRef, SubRouteRef as NewSubRouteRef, ExternalRouteRef as NewExternalRouteRef, + createRouteRef as createNewRouteRef, + createSubRouteRef as createNewSubRouteRef, + createExternalRouteRef as createNewExternalRouteRef, } from '@backstage/frontend-plugin-api'; import { convertLegacyRouteRef } from './convertLegacyRouteRef'; @@ -70,6 +76,15 @@ describe('convertLegacyRouteRef', () => { const ref3Converted: NewExternalRouteRef = convertLegacyRouteRef(ref3); const ref4Converted: NewExternalRouteRef = convertLegacyRouteRef(ref4); + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + const ref1Internal = toInternalNewRouteRef(ref1Converted); const ref2Internal = toInternalNewRouteRef(ref2Converted); const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted); @@ -116,4 +131,76 @@ describe('convertLegacyRouteRef', () => { expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); expect(ref4Internal.optional).toBe(true); }); + + it('converts new to old', () => { + const ref1 = createNewRouteRef(); + const ref2 = createNewRouteRef({ params: ['p1', 'p2'] }); + const ref1sub1 = createNewSubRouteRef({ + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createNewSubRouteRef({ + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createNewSubRouteRef({ + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createNewExternalRouteRef(); + const ref4 = createNewExternalRouteRef({ + optional: true, + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: OldRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: OldRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: OldExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: OldExternalRouteRef = convertLegacyRouteRef(ref4); + + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + + expect(String(ref1Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref1Converted.params).toEqual([]); + expect(String(ref2Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref2Converted.params).toEqual(['p1', 'p2']); + + expect(String(ref1sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1 with parent created at '.*'\}$/, + ); + expect(ref1sub1Converted.params).toEqual([]); + expect(ref1sub1Converted.parent).toBe(ref1); + expect(String(ref1sub2Converted)).toMatch( + /^SubRouteRef\{at \/sub2\/:p3 with parent created at '.*'\}$/, + ); + expect(ref1sub2Converted.params).toEqual(['p3']); + expect(ref1sub2Converted.parent).toBe(ref1); + expect(String(ref2sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1\/:p3 with parent created at '.*'\}$/, + ); + expect(ref2sub1Converted.params).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Converted.parent).toBe(ref2); + + expect(String(ref3Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref3Converted.params).toEqual([]); + expect(ref3Converted.optional).toBe(false); + expect(String(ref4Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref4Converted.params).toEqual(['p1', 'p2']); + expect(ref4Converted.optional).toBe(true); + }); }); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index c967f9e9b5..24c589bb87 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -116,16 +116,113 @@ export function convertLegacyRouteRef< ref: LegacyExternalRouteRef, ): ExternalRouteRef; +/** + * A temporary helper to convert a new route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: RouteRef, +): LegacyRouteRef; + +/** + * A temporary helper to convert a new sub route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createSubRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: SubRouteRef, +): LegacySubRouteRef; + +/** + * A temporary helper to convert a new external route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef< + TParams extends AnyRouteRefParams, + TOptional extends boolean, +>( + ref: ExternalRouteRef, +): LegacyExternalRouteRef; export function convertLegacyRouteRef( - ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, -): RouteRef | SubRouteRef | ExternalRouteRef { + ref: + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef + | RouteRef + | SubRouteRef + | ExternalRouteRef, +): + | RouteRef + | SubRouteRef + | ExternalRouteRef + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef { + const isNew = '$$type' in ref; + const oldType = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + // Ref has already been converted - if ('$$type' in ref) { - return ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef; + if (isNew && oldType) { + return ref as any; } - const type = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + if (isNew) { + return convertNewToOld( + ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef, + ); + } + return convertOldToNew(ref, oldType); +} + +function convertNewToOld( + ref: RouteRef | SubRouteRef | ExternalRouteRef, +): LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef { + if (ref.$$type === '@backstage/RouteRef') { + const newRef = toInternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'absolute', + params: newRef.getParams(), + title: newRef.getDescription(), + } as Omit) as unknown as LegacyRouteRef; + } + if (ref.$$type === '@backstage/SubRouteRef') { + const newRef = toInternalSubRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'sub', + parent: convertLegacyRouteRef(newRef.getParent()), + params: newRef.getParams(), + } as Omit) as unknown as LegacySubRouteRef; + } + if (ref.$$type === '@backstage/ExternalRouteRef') { + const newRef = toInternalExternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'external', + params: newRef.getParams(), + defaultTarget: newRef.getDefaultTarget(), + } as Omit) as unknown as LegacyExternalRouteRef; + } + + throw new Error( + `Failed to convert route ref, unknown type '${(ref as any).$$type}'`, + ); +} + +function convertOldToNew( + ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, + type: unknown, +): RouteRef | SubRouteRef | ExternalRouteRef { if (type === 'absolute') { const legacyRef = ref as LegacyRouteRef; const legacyRefStr = String(legacyRef); From f8e2383016dc5d42719d4d8d67f573b604897388 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 11:40:32 +0200 Subject: [PATCH 099/204] core-compat-api: add forwards compat for routing context Signed-off-by: Patrik Oldsberg --- .../compatWrapper/ForwardsCompatProvider.tsx | 55 ++++++++++++++++++- .../src/compatWrapper/compatWrapper.test.tsx | 16 ++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index 182b774cca..b2200387cc 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -21,22 +21,34 @@ import { useApp, } from '@backstage/core-plugin-api'; import { + AnyRouteRefParams, ComponentRef, ComponentsApi, CoreErrorBoundaryFallbackProps, CoreNotFoundErrorPageProps, CoreProgressProps, + ExternalRouteRef, IconComponent, IconsApi, + RouteFunc, + RouteRef, + RouteResolutionApi, + RouteResolutionApiResolveOptions, + SubRouteRef, componentsApiRef, coreComponentRefs, iconsApiRef, + routeResolutionApiRef, } from '@backstage/frontend-plugin-api'; import React, { ComponentType, useMemo } from 'react'; import { ReactNode } from 'react'; import { toLegacyPlugin } from './BackwardsCompatProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { ApiProvider } from '../../../core-app-api/src/apis/system/ApiProvider'; +import { useVersionedContext } from '@backstage/version-bridge'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { type RouteResolver } from '../../../core-plugin-api/src/routing/useRouteRef'; +import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; class CompatComponentsApi implements ComponentsApi { readonly #Progress: ComponentType; @@ -88,13 +100,34 @@ class CompatIconsApi implements IconsApi { } } -class AppFallbackApis implements ApiHolder { +class CompatRouteResolutionApi implements RouteResolutionApi { + readonly #routeResolver: RouteResolver; + + constructor(routeResolver: RouteResolver) { + this.#routeResolver = routeResolver; + } + + resolve( + anyRouteRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, + options?: RouteResolutionApiResolveOptions | undefined, + ): RouteFunc | undefined { + const legacyRef = convertLegacyRouteRef(anyRouteRef as RouteRef); + return this.#routeResolver.resolve(legacyRef, options?.sourcePath ?? '/'); + } +} + +class ForwardsCompatApis implements ApiHolder { readonly #componentsApi: ComponentsApi; readonly #iconsApi: IconsApi; + readonly #routeResolutionApi: RouteResolutionApi; - constructor(app: AppContext) { + constructor(app: AppContext, routeResolver: RouteResolver) { this.#componentsApi = new CompatComponentsApi(app); this.#iconsApi = new CompatIconsApi(app); + this.#routeResolutionApi = new CompatRouteResolutionApi(routeResolver); } get(ref: ApiRef): T | undefined { @@ -102,6 +135,8 @@ class AppFallbackApis implements ApiHolder { return this.#componentsApi as T; } else if (ref.id === iconsApiRef.id) { return this.#iconsApi as T; + } else if (ref.id === routeResolutionApiRef.id) { + return this.#routeResolutionApi as T; } return undefined; } @@ -109,7 +144,21 @@ class AppFallbackApis implements ApiHolder { function NewAppApisProvider(props: { children: ReactNode }) { const app = useApp(); - const appFallbackApis = useMemo(() => new AppFallbackApis(app), [app]); + const versionedRouteResolverContext = useVersionedContext<{ + 1: RouteResolver; + }>('routing-context'); + if (!versionedRouteResolverContext) { + throw new Error('Routing context is not available'); + } + const routeResolver = versionedRouteResolverContext.atVersion(1); + if (!routeResolver) { + throw new Error('RoutingContext v1 not available'); + } + + const appFallbackApis = useMemo( + () => new ForwardsCompatApis(app, routeResolver), + [app, routeResolver], + ); return {props.children}; } diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index b17ca2d508..6a27bf19e8 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -22,6 +22,7 @@ import { createExtension, iconsApiRef, useRouteRef as useNewRouteRef, + createRouteRef as createNewRouteRef, useApi, } from '@backstage/frontend-plugin-api'; import { @@ -122,4 +123,19 @@ describe('ForwardsCompatProvider', () => { icons: kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, user, warning" `); }); + + it('should convert the routing context', async () => { + const routeRef = createNewRouteRef(); + + function Component() { + const link = useNewRouteRef(routeRef); + return
link: {link()}
; + } + + await renderInOldTestApp(compatWrapper(), { + mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, + }); + + expect(screen.getByText('link: /test')).toBeInTheDocument(); + }); }); From 16cf96c28a5cccc5b0446f7a20efd9004c86e1d8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 11:48:55 +0200 Subject: [PATCH 100/204] changesets: add changeset for new -> old compat Signed-off-by: Patrik Oldsberg --- .changeset/slow-ducks-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slow-ducks-rush.md diff --git a/.changeset/slow-ducks-rush.md b/.changeset/slow-ducks-rush.md new file mode 100644 index 0000000000..c09f883e9c --- /dev/null +++ b/.changeset/slow-ducks-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. From 8b45adcadd32fa70eee7c9c4a967b6764ec61a4f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 17:37:38 +0200 Subject: [PATCH 101/204] backend-plugin-api: internal refactor to split creators Signed-off-by: Patrik Oldsberg --- ...es.test.ts => createBackendModule.test.ts} | 53 +--- .../src/wiring/createBackendModule.ts | 107 ++++++++ .../src/wiring/createBackendPlugin.test.ts | 55 +++++ .../src/wiring/createBackendPlugin.ts | 100 ++++++++ .../src/wiring/createExtensionPoint.test.ts | 27 +++ .../src/wiring/createExtensionPoint.ts | 58 +++++ .../src/wiring/factories.ts | 229 ------------------ .../backend-plugin-api/src/wiring/index.ts | 16 +- 8 files changed, 354 insertions(+), 291 deletions(-) rename packages/backend-plugin-api/src/wiring/{factories.test.ts => createBackendModule.test.ts} (53%) create mode 100644 packages/backend-plugin-api/src/wiring/createBackendModule.ts create mode 100644 packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts create mode 100644 packages/backend-plugin-api/src/wiring/createBackendPlugin.ts create mode 100644 packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts create mode 100644 packages/backend-plugin-api/src/wiring/createExtensionPoint.ts delete mode 100644 packages/backend-plugin-api/src/wiring/factories.ts diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts similarity index 53% rename from packages/backend-plugin-api/src/wiring/factories.test.ts rename to packages/backend-plugin-api/src/wiring/createBackendModule.test.ts index 6d5c1f39cd..31496020bf 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts @@ -14,60 +14,9 @@ * limitations under the License. */ -import { - createBackendModule, - createBackendPlugin, - createExtensionPoint, -} from './factories'; +import { createBackendModule } from './createBackendModule'; import { InternalBackendFeature } from './types'; -describe('createExtensionPoint', () => { - it('should create an ExtensionPoint', () => { - const extensionPoint = createExtensionPoint({ id: 'x' }); - expect(extensionPoint).toBeDefined(); - expect(extensionPoint.id).toBe('x'); - expect(() => extensionPoint.T).not.toThrow(); - expect(String(extensionPoint)).toBe('extensionPoint{x}'); - }); -}); - -describe('createBackendPlugin', () => { - it('should create a BackendPlugin', () => { - const result = createBackendPlugin({ - pluginId: 'x', - register(r) { - r.registerInit({ deps: {}, async init() {} }); - }, - }); - - // legacy form - const legacy = result() as unknown as InternalBackendFeature; - expect(legacy.$$type).toEqual('@backstage/BackendFeature'); - expect(legacy.version).toEqual('v1'); - expect(legacy.getRegistrations).toEqual(expect.any(Function)); - - // new form - const plugin = result as unknown as InternalBackendFeature; - expect(plugin.$$type).toEqual('@backstage/BackendFeature'); - expect(plugin.version).toEqual('v1'); - expect(plugin.getRegistrations).toEqual(expect.any(Function)); - expect(plugin.getRegistrations()).toEqual([ - { - type: 'plugin', - pluginId: 'x', - extensionPoints: [], - init: { - deps: expect.any(Object), - func: expect.any(Function), - }, - }, - ]); - - // @ts-expect-error - expect(plugin({ a: 'a' })).toBeDefined(); - }); -}); - describe('createBackendModule', () => { it('should create a BackendModule', () => { const result = createBackendModule({ diff --git a/packages/backend-plugin-api/src/wiring/createBackendModule.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.ts new file mode 100644 index 0000000000..56f041606e --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.ts @@ -0,0 +1,107 @@ +/* + * Copyright 2022 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 { BackendFeatureCompat } from '../types'; +import { + BackendModuleRegistrationPoints, + InternalBackendModuleRegistration, + InternalBackendPluginRegistration, +} from './types'; + +/** + * The configuration options passed to {@link createBackendModule}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateBackendModuleOptions { + /** + * Should exactly match the `id` of the plugin that the module extends. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + pluginId: string; + + /** + * The ID of this module, used to identify the module and ensure that it is not installed twice. + */ + moduleId: string; + register(reg: BackendModuleRegistrationPoints): void; +} + +/** + * Creates a new backend module for a given plugin. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export function createBackendModule( + options: CreateBackendModuleOptions, +): BackendFeatureCompat { + function getRegistrations() { + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendModuleRegistration['init'] | undefined = undefined; + + options.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error('registerExtensionPoint called after registerInit'); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`, + ); + } + + return [ + { + type: 'module', + pluginId: options.pluginId, + moduleId: options.moduleId, + extensionPoints, + init, + }, + ]; + } + + function backendFeatureCompatWrapper() { + return backendFeatureCompatWrapper; + } + + Object.assign(backendFeatureCompatWrapper, { + $$type: '@backstage/BackendFeature' as const, + version: 'v1', + getRegistrations, + }); + + return backendFeatureCompatWrapper as BackendFeatureCompat; +} diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts new file mode 100644 index 0000000000..cd1ce164bf --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2022 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 { createBackendPlugin } from './createBackendPlugin'; +import { InternalBackendFeature } from './types'; + +describe('createBackendPlugin', () => { + it('should create a BackendPlugin', () => { + const result = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ deps: {}, async init() {} }); + }, + }); + + // legacy form + const legacy = result() as unknown as InternalBackendFeature; + expect(legacy.$$type).toEqual('@backstage/BackendFeature'); + expect(legacy.version).toEqual('v1'); + expect(legacy.getRegistrations).toEqual(expect.any(Function)); + + // new form + const plugin = result as unknown as InternalBackendFeature; + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + expect(plugin.version).toEqual('v1'); + expect(plugin.getRegistrations).toEqual(expect.any(Function)); + expect(plugin.getRegistrations()).toEqual([ + { + type: 'plugin', + pluginId: 'x', + extensionPoints: [], + init: { + deps: expect.any(Object), + func: expect.any(Function), + }, + }, + ]); + + // @ts-expect-error + expect(plugin({ a: 'a' })).toBeDefined(); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts new file mode 100644 index 0000000000..f5984ef24b --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2022 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 { BackendFeatureCompat } from '../types'; +import { + BackendPluginRegistrationPoints, + InternalBackendPluginRegistration, +} from './types'; + +/** + * The configuration options passed to {@link createBackendPlugin}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateBackendPluginOptions { + /** + * The ID of this plugin. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + pluginId: string; + register(reg: BackendPluginRegistrationPoints): void; +} + +/** + * Creates a new backend plugin. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export function createBackendPlugin( + options: CreateBackendPluginOptions, +): BackendFeatureCompat { + function getRegistrations() { + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendPluginRegistration['init'] | undefined = undefined; + + options.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error('registerExtensionPoint called after registerInit'); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${options.pluginId}`, + ); + } + + return [ + { + type: 'plugin', + pluginId: options.pluginId, + extensionPoints, + init, + }, + ]; + } + + function backendFeatureCompatWrapper() { + return backendFeatureCompatWrapper; + } + + Object.assign(backendFeatureCompatWrapper, { + $$type: '@backstage/BackendFeature' as const, + version: 'v1', + getRegistrations, + }); + + return backendFeatureCompatWrapper as BackendFeatureCompat; +} diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts new file mode 100644 index 0000000000..825063e845 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 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 { createExtensionPoint } from './createExtensionPoint'; + +describe('createExtensionPoint', () => { + it('should create an ExtensionPoint', () => { + const extensionPoint = createExtensionPoint({ id: 'x' }); + expect(extensionPoint).toBeDefined(); + expect(extensionPoint.id).toBe('x'); + expect(() => extensionPoint.T).not.toThrow(); + expect(String(extensionPoint)).toBe('extensionPoint{x}'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts new file mode 100644 index 0000000000..9dbae249ea --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2022 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 { ExtensionPoint } from './types'; + +/** + * The configuration options passed to {@link createExtensionPoint}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateExtensionPointOptions { + /** + * The ID of this extension point. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + id: string; +} + +/** + * Creates a new backend extension point. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + */ +export function createExtensionPoint( + options: CreateExtensionPointOptions, +): ExtensionPoint { + return { + id: options.id, + get T(): T { + if (process.env.NODE_ENV === 'test') { + // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken + return null as T; + } + throw new Error(`tried to read ExtensionPoint.T of ${this}`); + }, + toString() { + return `extensionPoint{${options.id}}`; + }, + $$type: '@backstage/ExtensionPoint', + }; +} diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts deleted file mode 100644 index 81e2247152..0000000000 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2022 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 { BackendFeatureCompat } from '../types'; -import { - BackendModuleRegistrationPoints, - BackendPluginRegistrationPoints, - ExtensionPoint, - InternalBackendModuleRegistration, - InternalBackendPluginRegistration, -} from './types'; - -/** - * The configuration options passed to {@link createExtensionPoint}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateExtensionPointOptions { - /** - * The ID of this extension point. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - id: string; -} - -/** - * Creates a new backend extension point. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} - */ -export function createExtensionPoint( - options: CreateExtensionPointOptions, -): ExtensionPoint { - return { - id: options.id, - get T(): T { - if (process.env.NODE_ENV === 'test') { - // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken - return null as T; - } - throw new Error(`tried to read ExtensionPoint.T of ${this}`); - }, - toString() { - return `extensionPoint{${options.id}}`; - }, - $$type: '@backstage/ExtensionPoint', - }; -} - -/** - * The configuration options passed to {@link createBackendPlugin}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateBackendPluginOptions { - /** - * The ID of this plugin. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - pluginId: string; - register(reg: BackendPluginRegistrationPoints): void; -} - -/** - * Creates a new backend plugin. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export function createBackendPlugin( - options: CreateBackendPluginOptions, -): BackendFeatureCompat { - function getRegistrations() { - const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = - []; - let init: InternalBackendPluginRegistration['init'] | undefined = undefined; - - options.register({ - registerExtensionPoint(ext, impl) { - if (init) { - throw new Error('registerExtensionPoint called after registerInit'); - } - extensionPoints.push([ext, impl]); - }, - registerInit(regInit) { - if (init) { - throw new Error('registerInit must only be called once'); - } - init = { - deps: regInit.deps, - func: regInit.init, - }; - }, - }); - - if (!init) { - throw new Error( - `registerInit was not called by register in ${options.pluginId}`, - ); - } - - return [ - { - type: 'plugin', - pluginId: options.pluginId, - extensionPoints, - init, - }, - ]; - } - - function backendFeatureCompatWrapper() { - return backendFeatureCompatWrapper; - } - - Object.assign(backendFeatureCompatWrapper, { - $$type: '@backstage/BackendFeature' as const, - version: 'v1', - getRegistrations, - }); - - return backendFeatureCompatWrapper as BackendFeatureCompat; -} - -/** - * The configuration options passed to {@link createBackendModule}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateBackendModuleOptions { - /** - * Should exactly match the `id` of the plugin that the module extends. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - pluginId: string; - - /** - * The ID of this module, used to identify the module and ensure that it is not installed twice. - */ - moduleId: string; - register(reg: BackendModuleRegistrationPoints): void; -} - -/** - * Creates a new backend module for a given plugin. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export function createBackendModule( - options: CreateBackendModuleOptions, -): BackendFeatureCompat { - function getRegistrations() { - const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = - []; - let init: InternalBackendModuleRegistration['init'] | undefined = undefined; - - options.register({ - registerExtensionPoint(ext, impl) { - if (init) { - throw new Error('registerExtensionPoint called after registerInit'); - } - extensionPoints.push([ext, impl]); - }, - registerInit(regInit) { - if (init) { - throw new Error('registerInit must only be called once'); - } - init = { - deps: regInit.deps, - func: regInit.init, - }; - }, - }); - - if (!init) { - throw new Error( - `registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`, - ); - } - - return [ - { - type: 'module', - pluginId: options.pluginId, - moduleId: options.moduleId, - extensionPoints, - init, - }, - ]; - } - - function backendFeatureCompatWrapper() { - return backendFeatureCompatWrapper; - } - - Object.assign(backendFeatureCompatWrapper, { - $$type: '@backstage/BackendFeature' as const, - version: 'v1', - getRegistrations, - }); - - return backendFeatureCompatWrapper as BackendFeatureCompat; -} diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 5197cec050..5f7960e9cd 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -14,17 +14,13 @@ * limitations under the License. */ -import type { - CreateBackendPluginOptions, - CreateBackendModuleOptions, - CreateExtensionPointOptions, -} from './factories'; +import { type CreateBackendModuleOptions } from './createBackendModule'; +import { type CreateBackendPluginOptions } from './createBackendPlugin'; +import { type CreateExtensionPointOptions } from './createExtensionPoint'; -export { - createBackendModule, - createBackendPlugin, - createExtensionPoint, -} from './factories'; +export { createBackendModule } from './createBackendModule'; +export { createBackendPlugin } from './createBackendPlugin'; +export { createExtensionPoint } from './createExtensionPoint'; export type { BackendModuleRegistrationPoints, From 6061061a818900224275c157d275621c32c53bf0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 01:26:08 +0200 Subject: [PATCH 102/204] backend-plugin-api: add createBackendFeatureLoader Signed-off-by: Patrik Oldsberg --- .changeset/calm-crabs-drop.md | 5 + packages/backend-plugin-api/api-report.md | 41 ++++ .../wiring/createBackendFeatureLoader.test.ts | 213 ++++++++++++++++++ .../src/wiring/createBackendFeatureLoader.ts | 73 ++++++ .../backend-plugin-api/src/wiring/index.ts | 4 + .../backend-plugin-api/src/wiring/types.ts | 14 +- 6 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-crabs-drop.md create mode 100644 packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts create mode 100644 packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts diff --git a/.changeset/calm-crabs-drop.md b/.changeset/calm-crabs-drop.md new file mode 100644 index 0000000000..09bed7c091 --- /dev/null +++ b/.changeset/calm-crabs-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index b3c35d3699..3bd112682a 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -213,6 +213,47 @@ export namespace coreServices { identity: ServiceRef; } +// @public +export function createBackendFeatureLoader< + TDeps extends { + [name in string]: unknown; + }, +>(options: CreateBackendFeatureLoaderOptions): BackendFeature; + +// @public +export interface CreateBackendFeatureLoaderOptions< + TDeps extends { + [name in string]: unknown; + }, +> { + // (undocumented) + deps?: { + [name in keyof TDeps]: ServiceRef; + }; + // (undocumented) + loader(deps: TDeps): + | Iterable< + | BackendFeature + | Promise<{ + default: BackendFeature; + }> + > + | Promise< + Iterable< + | BackendFeature + | Promise<{ + default: BackendFeature; + }> + > + > + | AsyncIterable< + | BackendFeature + | { + default: BackendFeature; + } + >; +} + // @public export function createBackendModule( options: CreateBackendModuleOptions, diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts new file mode 100644 index 0000000000..98471bef4c --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts @@ -0,0 +1,213 @@ +/* + * Copyright 2022 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 { coreServices, createServiceFactory } from '../services'; +import { BackendFeature } from '../types'; +import { createBackendFeatureLoader } from './createBackendFeatureLoader'; +import { createBackendPlugin } from './createBackendPlugin'; +import { + InternalBackendFeature, + InternalBackendFeatureLoaderRegistration, +} from './types'; + +describe('createBackendFeatureLoader', () => { + it('should create an empty feature loader', () => { + const result = createBackendFeatureLoader({ + deps: {}, + loader: () => [], + }); + + const plugin = result as unknown as InternalBackendFeature; + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + expect(plugin.version).toEqual('v1'); + expect(plugin.getRegistrations).toEqual(expect.any(Function)); + expect(plugin.getRegistrations()).toEqual([ + { + type: 'loader', + description: expect.stringMatching(/^created at '.*'$/), + deps: expect.any(Object), + loader: expect.any(Function), + }, + ]); + }); + + it('should create a feature loader that loads a few features', async () => { + const result = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + loader({ config: _unused }) { + return [ + createBackendPlugin({ + pluginId: 'x', + register() {}, + })(), + createServiceFactory({ + service: coreServices.pluginMetadata, + deps: {}, + factory: () => ({ getId: () => 'fake-id' }), + })(), + // Dynamic import format + Promise.resolve({ + default: createBackendPlugin({ + pluginId: 'y', + register() {}, + })(), + }), + ]; + }, + }) as InternalBackendFeature; + + expect(result.$$type).toEqual('@backstage/BackendFeature'); + expect(result.version).toEqual('v1'); + expect(result.getRegistrations).toEqual(expect.any(Function)); + + const registrations = result.getRegistrations(); + expect(registrations).toEqual([ + { + type: 'loader', + description: expect.stringMatching(/^created at '.*'$/), + deps: expect.any(Object), + loader: expect.any(Function), + }, + ]); + + const results = await (registrations[0] as any).loader({ config: {} }); + expect(results.length).toBe(3); + const [pluginX, serviceFactory, pluginY] = results; + expect(pluginX.$$type).toBe('@backstage/BackendFeature'); + expect(serviceFactory.$$type).toBe('@backstage/BackendFeature'); + expect(pluginY.$$type).toBe('@backstage/BackendFeature'); + expect(serviceFactory.service.id).toBe(coreServices.pluginMetadata.id); + }); + + it('should support multiple output formats', async () => { + const feature = createBackendPlugin({ pluginId: 'x', register() {} })(); + const dynamicFeature = Promise.resolve({ default: feature }); + + async function extractResult(f: BackendFeature) { + const internal = f as InternalBackendFeature; + const reg = + internal.getRegistrations()[0] as InternalBackendFeatureLoaderRegistration; + return reg.loader({}); + } + + await expect( + extractResult( + createBackendFeatureLoader({ + loader() { + return [feature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async loader() { + return [feature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + *loader() { + yield feature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async *loader() { + yield feature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + loader() { + return [dynamicFeature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async loader() { + return [dynamicFeature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + *loader() { + yield dynamicFeature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async *loader() { + yield dynamicFeature; + }, + }), + ), + ).resolves.toEqual([feature]); + }); + + it('should only allow dependencies on root scoped services', () => { + createBackendFeatureLoader({ + deps: { + rootLogger: coreServices.rootLogger, + }, + loader: () => [], + }); + createBackendFeatureLoader({ + deps: { + // @ts-expect-error + logger: coreServices.logger, + }, + loader: () => [], + }); + createBackendFeatureLoader({ + deps: { + rootLogger: coreServices.rootLogger, + // @ts-expect-error + logger: coreServices.logger, + }, + loader: () => [], + }); + expect('test').toBe('test'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts new file mode 100644 index 0000000000..9722cc534f --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts @@ -0,0 +1,73 @@ +/* + * 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 { ServiceRef } from '../services'; +import { BackendFeature } from '../types'; +import { InternalBackendFeature } from './types'; + +/** + * @public + * Options for creating a new backend feature loader. + */ +export interface CreateBackendFeatureLoaderOptions< + TDeps extends { [name in string]: unknown }, +> { + deps?: { + [name in keyof TDeps]: ServiceRef; + }; + loader( + deps: TDeps, + ): + | Iterable> + | Promise>> + | AsyncIterable; +} + +/** + * @public + * Creates a new backend feature loader. + */ +export function createBackendFeatureLoader< + TDeps extends { [name in string]: unknown }, +>(options: CreateBackendFeatureLoaderOptions): BackendFeature { + const registrations = [ + { + type: 'loader', + description: `created at '${new Date().toISOString()}'`, + deps: options.deps, + async loader(deps: TDeps) { + const it = await options.loader(deps); + const result = new Array(); + for await (const item of it) { + if ('$$type' in item && item.$$type === '@backstage/BackendFeature') { + result.push(item); + } else if ('default' in item) { + result.push(item.default); + } else { + throw new Error(`Invalid item "${item}"`); + } + } + return result; + }, + }, + ]; + + return { + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: () => registrations, + } as InternalBackendFeature; +} diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 5f7960e9cd..95eea6c3d4 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -21,6 +21,10 @@ import { type CreateExtensionPointOptions } from './createExtensionPoint'; export { createBackendModule } from './createBackendModule'; export { createBackendPlugin } from './createBackendPlugin'; export { createExtensionPoint } from './createExtensionPoint'; +export { + createBackendFeatureLoader, + type CreateBackendFeatureLoaderOptions, +} from './createBackendFeatureLoader'; export type { BackendModuleRegistrationPoints, diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 55d43ccb7c..7569cc2b41 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -76,7 +76,9 @@ export interface BackendModuleRegistrationPoints { export interface InternalBackendFeature extends BackendFeature { version: 'v1'; getRegistrations(): Array< - InternalBackendPluginRegistration | InternalBackendModuleRegistration + | InternalBackendPluginRegistration + | InternalBackendModuleRegistration + | InternalBackendFeatureLoaderRegistration >; } @@ -102,3 +104,13 @@ export interface InternalBackendModuleRegistration { func(deps: Record): Promise; }; } + +/** + * @public + */ +export interface InternalBackendFeatureLoaderRegistration { + type: 'loader'; + description: string; + deps: Record>; + loader(deps: Record): Promise; +} From 8f70547ea54ccf19b071312cd2894cc10c423fd7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 01:43:06 +0200 Subject: [PATCH 103/204] backend-plugin-api: refactor to flatten internal backend feature loader Signed-off-by: Patrik Oldsberg --- .../src/services/system/types.test.ts | 10 +++- .../src/services/system/types.ts | 3 ++ .../wiring/createBackendFeatureLoader.test.ts | 52 ++++++------------- .../src/wiring/createBackendFeatureLoader.ts | 44 +++++++--------- .../backend-plugin-api/src/wiring/types.ts | 10 ++-- 5 files changed, 53 insertions(+), 66 deletions(-) diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts index adf097761b..32c1774455 100644 --- a/packages/backend-plugin-api/src/services/system/types.test.ts +++ b/packages/backend-plugin-api/src/services/system/types.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createServiceFactory, createServiceRef } from './types'; +import { + InternalServiceFactory, + createServiceFactory, + createServiceRef, +} from './types'; const ref = createServiceRef({ id: 'x' }); const rootDep = createServiceRef({ id: 'y', scope: 'root' }); @@ -36,6 +40,10 @@ describe('createServiceFactory', () => { }, }); expect(metaFactory).toEqual(expect.any(Function)); + expect(metaFactory().$$type).toBe('@backstage/BackendFeature'); + expect((metaFactory() as InternalServiceFactory).featureType).toBe( + 'service', + ); expect(metaFactory().service).toBe(ref); // @ts-expect-error diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index d31350806d..ccec36e39a 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -78,6 +78,7 @@ export interface InternalServiceFactory< TScope extends 'plugin' | 'root' = 'plugin' | 'root', > extends ServiceFactory { version: 'v1'; + featureType: 'service'; initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; createRootContext?(deps: { [key in string]: unknown }): Promise; @@ -307,6 +308,7 @@ export function createServiceFactory< return { $$type: '@backstage/BackendFeature', version: 'v1', + featureType: 'service', service: c.service, initialization: c.initialization, deps: c.deps, @@ -322,6 +324,7 @@ export function createServiceFactory< return { $$type: '@backstage/BackendFeature', version: 'v1', + featureType: 'service', service: c.service, initialization: c.initialization, ...('createRootContext' in c diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts index 98471bef4c..37fc079551 100644 --- a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts @@ -15,33 +15,25 @@ */ import { coreServices, createServiceFactory } from '../services'; +import { InternalServiceFactory } from '../services/system/types'; import { BackendFeature } from '../types'; import { createBackendFeatureLoader } from './createBackendFeatureLoader'; import { createBackendPlugin } from './createBackendPlugin'; -import { - InternalBackendFeature, - InternalBackendFeatureLoaderRegistration, -} from './types'; +import { InternalBackendFeatureLoader } from './types'; describe('createBackendFeatureLoader', () => { it('should create an empty feature loader', () => { const result = createBackendFeatureLoader({ deps: {}, loader: () => [], - }); + }) as InternalBackendFeatureLoader; - const plugin = result as unknown as InternalBackendFeature; - expect(plugin.$$type).toEqual('@backstage/BackendFeature'); - expect(plugin.version).toEqual('v1'); - expect(plugin.getRegistrations).toEqual(expect.any(Function)); - expect(plugin.getRegistrations()).toEqual([ - { - type: 'loader', - description: expect.stringMatching(/^created at '.*'$/), - deps: expect.any(Object), - loader: expect.any(Function), - }, - ]); + expect(result.$$type).toEqual('@backstage/BackendFeature'); + expect(result.version).toEqual('v1'); + expect(result.featureType).toEqual('loader'); + expect(result.deps).toEqual({}); + expect(result.loader).toEqual(expect.any(Function)); + expect(result.description).toMatch(/^created at '.*'$/); }); it('should create a feature loader that loads a few features', async () => { @@ -69,29 +61,21 @@ describe('createBackendFeatureLoader', () => { }), ]; }, - }) as InternalBackendFeature; + }) as InternalBackendFeatureLoader; expect(result.$$type).toEqual('@backstage/BackendFeature'); expect(result.version).toEqual('v1'); - expect(result.getRegistrations).toEqual(expect.any(Function)); + expect(result.featureType).toEqual('loader'); - const registrations = result.getRegistrations(); - expect(registrations).toEqual([ - { - type: 'loader', - description: expect.stringMatching(/^created at '.*'$/), - deps: expect.any(Object), - loader: expect.any(Function), - }, - ]); - - const results = await (registrations[0] as any).loader({ config: {} }); + const results = await result.loader({ config: {} }); expect(results.length).toBe(3); const [pluginX, serviceFactory, pluginY] = results; expect(pluginX.$$type).toBe('@backstage/BackendFeature'); expect(serviceFactory.$$type).toBe('@backstage/BackendFeature'); expect(pluginY.$$type).toBe('@backstage/BackendFeature'); - expect(serviceFactory.service.id).toBe(coreServices.pluginMetadata.id); + expect((serviceFactory as InternalServiceFactory).service.id).toBe( + coreServices.pluginMetadata.id, + ); }); it('should support multiple output formats', async () => { @@ -99,10 +83,8 @@ describe('createBackendFeatureLoader', () => { const dynamicFeature = Promise.resolve({ default: feature }); async function extractResult(f: BackendFeature) { - const internal = f as InternalBackendFeature; - const reg = - internal.getRegistrations()[0] as InternalBackendFeatureLoaderRegistration; - return reg.loader({}); + const internal = f as InternalBackendFeatureLoader; + return internal.loader({}); } await expect( diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts index 9722cc534f..de26b5154d 100644 --- a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts @@ -16,7 +16,7 @@ import { ServiceRef } from '../services'; import { BackendFeature } from '../types'; -import { InternalBackendFeature } from './types'; +import { InternalBackendFeatureLoader } from './types'; /** * @public @@ -43,31 +43,25 @@ export interface CreateBackendFeatureLoaderOptions< export function createBackendFeatureLoader< TDeps extends { [name in string]: unknown }, >(options: CreateBackendFeatureLoaderOptions): BackendFeature { - const registrations = [ - { - type: 'loader', - description: `created at '${new Date().toISOString()}'`, - deps: options.deps, - async loader(deps: TDeps) { - const it = await options.loader(deps); - const result = new Array(); - for await (const item of it) { - if ('$$type' in item && item.$$type === '@backstage/BackendFeature') { - result.push(item); - } else if ('default' in item) { - result.push(item.default); - } else { - throw new Error(`Invalid item "${item}"`); - } - } - return result; - }, - }, - ]; - return { $$type: '@backstage/BackendFeature', version: 'v1', - getRegistrations: () => registrations, - } as InternalBackendFeature; + featureType: 'loader', + description: `created at '${new Date().toISOString()}'`, + deps: options.deps, + async loader(deps: TDeps) { + const it = await options.loader(deps); + const result = new Array(); + for await (const item of it) { + if ('$$type' in item && item.$$type === '@backstage/BackendFeature') { + result.push(item); + } else if ('default' in item) { + result.push(item.default); + } else { + throw new Error(`Invalid item "${item}"`); + } + } + return result; + }, + } as InternalBackendFeatureLoader; } diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 7569cc2b41..df0ca93089 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -75,10 +75,9 @@ export interface BackendModuleRegistrationPoints { /** @internal */ export interface InternalBackendFeature extends BackendFeature { version: 'v1'; + featureType: 'registrations'; getRegistrations(): Array< - | InternalBackendPluginRegistration - | InternalBackendModuleRegistration - | InternalBackendFeatureLoaderRegistration + InternalBackendPluginRegistration | InternalBackendModuleRegistration >; } @@ -108,8 +107,9 @@ export interface InternalBackendModuleRegistration { /** * @public */ -export interface InternalBackendFeatureLoaderRegistration { - type: 'loader'; +export interface InternalBackendFeatureLoader extends BackendFeature { + version: 'v1'; + featureType: 'loader'; description: string; deps: Record>; loader(deps: Record): Promise; From 765d91aa0b75cbf8fac5c4b4331de2af4cda5b04 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 09:20:02 +0200 Subject: [PATCH 104/204] chore: added failing test for callback form Signed-off-by: blam --- .../wiring/createExtensionBlueprint.test.tsx | 56 +++++++++++++++++++ .../src/wiring/createExtensionBlueprint.ts | 7 ++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 6cf10d7f08..38e92fc26a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -326,4 +326,60 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + + it('should allow providing callback for properties to set with params', () => { + const Blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + namespace: props => props.test, + name: props => `${props.test}-name`, + config: { + schema: props => ({ + test: z => z.string().default(props.test), + }), + }, + factory(params: { test: string }) { + return [coreExtensionData.reactElement(
{params.test}
)]; + }, + }); + + expect(Blueprint.make({ params: { test: 'hello' } })) + .toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "test", + "input": "default", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "test": { + "default": "test", + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "test-extension", + "name": "hello-name", + "namespace": "hello", + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + + expect(true).toBe(true); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index b579cc207a..4a8f4a7c46 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -47,14 +47,14 @@ export type CreateExtensionBlueprintOptions< UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > = { - kind: string; - namespace?: string; + kind: string | ((params: TParams) => string); + namespace?: string | ((params: TParams) => string); attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; config?: { - schema: TConfigSchema; + schema: TConfigSchema | ((params: TParams) => TConfigSchema); }; factory( params: TParams, @@ -254,6 +254,7 @@ class ExtensionBlueprintImpl< ...this.options.config?.schema, ...args.config?.schema, } as TConfigSchema & TExtensionConfigSchema; + return createExtension({ kind: this.options.kind, namespace: args.namespace ?? this.options.namespace, From dc0d284419732fde05aa4c1dbecf71a1bb3ec3ca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 09:53:54 +0200 Subject: [PATCH 105/204] backend-plugin-api: fix feature loader description Signed-off-by: Patrik Oldsberg --- .../src/wiring/createBackendFeatureLoader.ts | 3 ++- .../src/wiring/describeParentCallSite.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 packages/backend-plugin-api/src/wiring/describeParentCallSite.ts diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts index de26b5154d..cfa3e96c65 100644 --- a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts @@ -16,6 +16,7 @@ import { ServiceRef } from '../services'; import { BackendFeature } from '../types'; +import { describeParentCallSite } from './describeParentCallSite'; import { InternalBackendFeatureLoader } from './types'; /** @@ -47,7 +48,7 @@ export function createBackendFeatureLoader< $$type: '@backstage/BackendFeature', version: 'v1', featureType: 'loader', - description: `created at '${new Date().toISOString()}'`, + description: `created at '${describeParentCallSite()}'`, deps: options.deps, async loader(deps: TDeps) { const it = await options.loader(deps); diff --git a/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts new file mode 100644 index 0000000000..35603e33b0 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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. + */ + +// Single re-export to avoid doing this import in multiple places, but still +// avoid duplicate declarations because this one is a bit tricky. +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite'; From d4d048b3fffb61d76f47efb065919d7ff80da635 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 12:16:19 +0200 Subject: [PATCH 106/204] backend-*-api: refactor for more explicit internal feature types Signed-off-by: Patrik Oldsberg --- .../src/wiring/BackendInitializer.ts | 73 ++++++++++++++----- .../src/wiring/createBackendModule.test.ts | 6 +- .../src/wiring/createBackendPlugin.test.ts | 6 +- .../backend-plugin-api/src/wiring/types.ts | 10 ++- .../src/next/wiring/TestBackend.ts | 48 +++++++----- 5 files changed, 96 insertions(+), 47 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ddad94de14..902258b9b0 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -26,7 +26,13 @@ import { import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports -import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +import type { + InternalBackendFeature, + InternalBackendFeatureLoader, + InternalBackendRegistrations, +} from '@backstage/backend-plugin-api/src/wiring/types'; +// eslint-disable-next-line @backstage/no-forbidden-package-imports +import type { InternalServiceFactory } from '@backstage/backend-plugin-api/src/services/system/types'; import { ForwardedError, ConflictError } from '@backstage/errors'; import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; @@ -44,7 +50,7 @@ export interface BackendRegisterInit { export class BackendInitializer { #startPromise?: Promise; - #features = new Array(); + #registrations = new Array(); #extensionPoints = new Map(); #serviceRegistry: ServiceRegistry; #registeredFeatures = new Array>(); @@ -101,21 +107,15 @@ export class BackendInitializer { } #addFeature(feature: BackendFeature) { - if (feature.$$type !== '@backstage/BackendFeature') { - throw new Error( - `Failed to add feature, invalid type '${feature.$$type}'`, - ); - } - if (isServiceFactory(feature)) { this.#serviceRegistry.add(feature); - } else if (isInternalBackendFeature(feature)) { + } else if (isBackendRegistrations(feature)) { if (feature.version !== 'v1') { throw new Error( `Failed to add feature, invalid version '${feature.version}'`, ); } - this.#features.push(feature); + this.#registrations.push(feature); } else { throw new Error( `Failed to add feature, invalid feature ${JSON.stringify(feature)}`, @@ -176,8 +176,8 @@ export class BackendInitializer { const pluginInits = new Map(); const moduleInits = new Map>(); - // Enumerate all features - for (const feature of this.#features) { + // Enumerate all registrations + for (const feature of this.#registrations) { for (const r of feature.getRegistrations()) { const provides = new Set>(); @@ -205,7 +205,7 @@ export class BackendInitializer { consumes: new Set(Object.values(r.init.deps)), init: r.init, }); - } else { + } else if (r.type === 'module') { let modules = moduleInits.get(r.pluginId); if (!modules) { modules = new Map(); @@ -221,6 +221,8 @@ export class BackendInitializer { consumes: new Set(Object.values(r.init.deps)), init: r.init, }); + } else { + throw new Error(`Invalid registration type '${(r as any).type}'`); } } } @@ -385,14 +387,45 @@ export class BackendInitializer { } } -function isServiceFactory(feature: BackendFeature): feature is ServiceFactory { - return !!(feature as ServiceFactory).service; +function toInternalBackendFeature( + feature: BackendFeature, +): InternalBackendFeature { + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`); + } + const internal = feature as InternalBackendFeature; + if (internal.version !== 'v1') { + throw new Error( + `Invalid BackendFeature, bad version '${internal.version}'`, + ); + } + return internal; } -function isInternalBackendFeature( +function isServiceFactory( feature: BackendFeature, -): feature is InternalBackendFeature { - return ( - typeof (feature as InternalBackendFeature).getRegistrations === 'function' - ); +): feature is InternalServiceFactory { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'service') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + if ('service' in internal) { + return true; + } + return false; +} + +function isBackendRegistrations( + feature: BackendFeature, +): feature is InternalBackendRegistrations { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'registrations') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + if ('getRegistrations' in internal) { + return true; + } + return false; } diff --git a/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts index 31496020bf..e3c9f9007a 100644 --- a/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from './createBackendModule'; -import { InternalBackendFeature } from './types'; +import { InternalBackendRegistrations } from './types'; describe('createBackendModule', () => { it('should create a BackendModule', () => { @@ -28,13 +28,13 @@ describe('createBackendModule', () => { }); // legacy form - const legacy = result() as unknown as InternalBackendFeature; + const legacy = result() as unknown as InternalBackendRegistrations; expect(legacy.$$type).toEqual('@backstage/BackendFeature'); expect(legacy.version).toEqual('v1'); expect(legacy.getRegistrations).toEqual(expect.any(Function)); // new form - const module = result as unknown as InternalBackendFeature; + const module = result as unknown as InternalBackendRegistrations; expect(module.$$type).toEqual('@backstage/BackendFeature'); expect(module.version).toEqual('v1'); expect(module.getRegistrations).toEqual(expect.any(Function)); diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts index cd1ce164bf..db4d60ee19 100644 --- a/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts @@ -15,7 +15,7 @@ */ import { createBackendPlugin } from './createBackendPlugin'; -import { InternalBackendFeature } from './types'; +import { InternalBackendRegistrations } from './types'; describe('createBackendPlugin', () => { it('should create a BackendPlugin', () => { @@ -27,13 +27,13 @@ describe('createBackendPlugin', () => { }); // legacy form - const legacy = result() as unknown as InternalBackendFeature; + const legacy = result() as unknown as InternalBackendRegistrations; expect(legacy.$$type).toEqual('@backstage/BackendFeature'); expect(legacy.version).toEqual('v1'); expect(legacy.getRegistrations).toEqual(expect.any(Function)); // new form - const plugin = result as unknown as InternalBackendFeature; + const plugin = result as unknown as InternalBackendRegistrations; expect(plugin.$$type).toEqual('@backstage/BackendFeature'); expect(plugin.version).toEqual('v1'); expect(plugin.getRegistrations).toEqual(expect.any(Function)); diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index df0ca93089..01ab8c2e3e 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ServiceRef } from '../services/system/types'; +import { InternalServiceFactory, ServiceRef } from '../services/system/types'; import { BackendFeature } from '../types'; /** @@ -73,7 +73,7 @@ export interface BackendModuleRegistrationPoints { } /** @internal */ -export interface InternalBackendFeature extends BackendFeature { +export interface InternalBackendRegistrations extends BackendFeature { version: 'v1'; featureType: 'registrations'; getRegistrations(): Array< @@ -114,3 +114,9 @@ export interface InternalBackendFeatureLoader extends BackendFeature { deps: Record>; loader(deps: Record): Promise; } + +/** @internal */ +export type InternalBackendFeature = + | InternalBackendRegistrations + | InternalBackendFeatureLoader + | InternalServiceFactory; diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 3d662a8c12..cf98c25e4a 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -36,7 +36,10 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports -import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +import { + InternalBackendFeature, + InternalBackendRegistrations, +} from '@backstage/backend-plugin-api/src/wiring/types'; import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter'; /** @public */ @@ -94,7 +97,7 @@ function createPluginsForOrphanModules(features: Array) { const modulePluginIds = new Set(); for (const feature of features) { - if (isInternalBackendFeature(feature)) { + if (isInternalBackendRegistrations(feature)) { const registrations = feature.getRegistrations(); for (const registration of registrations) { if (registration.type === 'plugin') { @@ -137,18 +140,7 @@ function createExtensionPointTestModules( } const registrations = features.flatMap(feature => { - if (feature.$$type !== '@backstage/BackendFeature') { - throw new Error( - `Failed to add feature, invalid type '${feature.$$type}'`, - ); - } - - if (isInternalBackendFeature(feature)) { - if (feature.version !== 'v1') { - throw new Error( - `Failed to add feature, invalid version '${feature.version}'`, - ); - } + if (isInternalBackendRegistrations(feature)) { return feature.getRegistrations(); } return []; @@ -361,10 +353,28 @@ function registerTestHooks() { registerTestHooks(); -function isInternalBackendFeature( +function toInternalBackendFeature( feature: BackendFeature, -): feature is InternalBackendFeature { - return ( - typeof (feature as InternalBackendFeature).getRegistrations === 'function' - ); +): InternalBackendFeature { + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`); + } + const internal = feature as InternalBackendFeature; + if (internal.version !== 'v1') { + throw new Error( + `Invalid BackendFeature, bad version '${internal.version}'`, + ); + } + return internal; +} + +function isInternalBackendRegistrations( + feature: BackendFeature, +): feature is InternalBackendRegistrations { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'registrations') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'getRegistrations' in internal; } From d3b49a60f2f96e33b04362f1d5f6dd89501cb8bd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 13:03:16 +0200 Subject: [PATCH 107/204] backend-app-api: add support for feature loaders Signed-off-by: Patrik Oldsberg --- .../src/wiring/BackendInitializer.test.ts | 123 ++++++++++++++++++ .../src/wiring/BackendInitializer.ts | 89 +++++++++++-- 2 files changed, 199 insertions(+), 13 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 3bbef5a4c0..6f60d65861 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -24,6 +24,7 @@ import { createBackendPlugin, createBackendModule, createExtensionPoint, + createBackendFeatureLoader, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; @@ -105,6 +106,128 @@ describe('BackendInitializer', () => { expect(factory3).not.toHaveBeenCalled(); }); + it('should discover features from feature loader', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + const ref2 = createServiceRef<{ x: number }>({ + id: '2', + scope: 'plugin', + }); + const factory1 = jest.fn(); + const factory2 = jest.fn(); + + const pluginInit = jest.fn(async () => {}); + const moduleInit = jest.fn(async () => {}); + + const init = new BackendInitializer(baseFactories); + init.add( + createBackendFeatureLoader({ + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: factory1, + }); + yield createServiceFactory({ + service: ref2, + initialization: 'always', + deps: {}, + factory: factory2, + }); + yield createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + init: pluginInit, + }); + }, + }); + yield createBackendModule({ + pluginId: 'test', + moduleId: 'tester', + register(reg) { + reg.registerInit({ + deps: {}, + init: moduleInit, + }); + }, + }); + }, + }), + ); + await init.start(); + + expect(factory1).toHaveBeenCalled(); + expect(factory2).toHaveBeenCalled(); + expect(pluginInit).toHaveBeenCalled(); + expect(moduleInit).toHaveBeenCalled(); + }); + + it('should refuse to override already initialized services through loaded features', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + + const init = new BackendInitializer([ + ...baseFactories, + createServiceFactory({ + service: ref1, + deps: {}, + factory: () => ({ x: 1 }), + }), + ]); + init.add( + createBackendFeatureLoader({ + deps: { service1: ref1 }, + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: jest.fn(), + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + 'Unable to set service factory with id 1, service has already been instantiated', + ); + }); + + it('should refuse feature loader that depends on a plugin scoped service', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + }); + + const init = new BackendInitializer([ + ...baseFactories, + createServiceFactory({ + service: ref1, + deps: {}, + factory: () => ({ x: 1 }), + }), + ]); + init.add( + createBackendFeatureLoader({ + // @ts-expect-error + deps: { service1: ref1 }, + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: jest.fn(), + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + /^Feature loaders can only depend on root scoped services, but 'service1' is scoped to 'plugin'. Offending loader is created at '.*'$/, + ); + }); + it('should initialize plugin scoped services with eager initialization', async () => { const ref1 = createServiceRef<{ x: number }>({ id: '1', diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 902258b9b0..4bfbf6244f 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -54,6 +54,7 @@ export class BackendInitializer { #extensionPoints = new Map(); #serviceRegistry: ServiceRegistry; #registeredFeatures = new Array>(); + #registeredFeatureLoaders = new Array(); constructor(defaultApiFactories: ServiceFactory[]) { this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]); @@ -109,12 +110,9 @@ export class BackendInitializer { #addFeature(feature: BackendFeature) { if (isServiceFactory(feature)) { this.#serviceRegistry.add(feature); + } else if (isBackendFeatureLoader(feature)) { + this.#registeredFeatureLoaders.push(feature); } else if (isBackendRegistrations(feature)) { - if (feature.version !== 'v1') { - throw new Error( - `Failed to add feature, invalid version '${feature.version}'`, - ); - } this.#registrations.push(feature); } else { throw new Error( @@ -170,6 +168,8 @@ export class BackendInitializer { this.#serviceRegistry.checkForCircularDeps(); } + await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); + // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); @@ -385,6 +385,69 @@ export class BackendInitializer { throw new Error('Unexpected plugin lifecycle service implementation'); } + + async #applyBackendFeatureLoaders(loaders: InternalBackendFeatureLoader[]) { + for (const loader of loaders) { + const deps = new Map(); + const missingRefs = new Set(); + + for (const [name, ref] of Object.entries(loader.deps ?? {})) { + if (ref.scope !== 'root') { + throw new Error( + `Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`, + ); + } + const impl = await this.#serviceRegistry.get( + ref as ServiceRef, + 'root', + ); + if (impl) { + deps.set(name, impl); + } else { + missingRefs.add(ref); + } + } + + if (missingRefs.size > 0) { + const missing = Array.from(missingRefs).join(', '); + throw new Error( + `No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`, + ); + } + + const result = await loader + .loader(Object.fromEntries(deps)) + .catch(error => { + throw new ForwardedError( + `Feature loader ${loader.description} failed`, + error, + ); + }); + + let didAddServiceFactory = false; + const newLoaders = new Array(); + + for await (const feature of result) { + if (isBackendFeatureLoader(feature)) { + newLoaders.push(feature); + } else { + didAddServiceFactory = + didAddServiceFactory || isServiceFactory(feature); + this.#addFeature(feature); + } + } + + // Every time we add a new service factory we need to make sure that we don't have circular dependencies + if (didAddServiceFactory) { + this.#serviceRegistry.checkForCircularDeps(); + } + + // Apply loaders recursively, depth-first + if (newLoaders.length > 0) { + await this.#applyBackendFeatureLoaders(newLoaders); + } + } + } } function toInternalBackendFeature( @@ -410,10 +473,7 @@ function isServiceFactory( return true; } // Backwards compatibility for v1 registrations that use duck typing - if ('service' in internal) { - return true; - } - return false; + return 'service' in internal; } function isBackendRegistrations( @@ -424,8 +484,11 @@ function isBackendRegistrations( return true; } // Backwards compatibility for v1 registrations that use duck typing - if ('getRegistrations' in internal) { - return true; - } - return false; + return 'getRegistrations' in internal; +} + +function isBackendFeatureLoader( + feature: BackendFeature, +): feature is InternalBackendFeatureLoader { + return toInternalBackendFeature(feature).featureType === 'loader'; } From 072c00c0735bd44382f4d08343ae661a7511a780 Mon Sep 17 00:00:00 2001 From: Heidi Puotiniemi Date: Mon, 5 Aug 2024 14:59:18 +0300 Subject: [PATCH 108/204] fix output element overlappig w/ flex-wrap Signed-off-by: Heidi Puotiniemi --- .changeset/breezy-rings-fly.md | 5 +++++ .../components/TemplateOutputs/DefaultTemplateOutputs.tsx | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/breezy-rings-fly.md diff --git a/.changeset/breezy-rings-fly.md b/.changeset/breezy-rings-fly.md new file mode 100644 index 0000000000..315da5f950 --- /dev/null +++ b/.changeset/breezy-rings-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fixed bug in DefaultTableOutputs where output elements overlapped in below approx. 800 px wide screens diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index cbc299bf1c..70f3b1fa59 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -68,6 +68,7 @@ export const DefaultTemplateOutputs = (props: { justifyContent="center" display="flex" gridGap={16} + flexWrap="wrap" > Date: Mon, 5 Aug 2024 14:11:40 +0200 Subject: [PATCH 109/204] avoid excessive error listeners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/tasty-ads-rescue.md | 5 ++++ .../src/entrypoints/cache/CacheManager.ts | 28 +++++++++---------- 2 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 .changeset/tasty-ads-rescue.md diff --git a/.changeset/tasty-ads-rescue.md b/.changeset/tasty-ads-rescue.md new file mode 100644 index 0000000000..d522856bdd --- /dev/null +++ b/.changeset/tasty-ads-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Avoid excessive numbers of error listeners on cache clients diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 89fabfe977..873bcf9648 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -111,23 +111,10 @@ export class CacheManager { return { getClient: (defaultOptions = {}) => { const clientFactory = (options: CacheServiceOptions) => { - const concreteClient = this.getClientWithTtl( + return this.getClientWithTtl( pluginId, options.defaultTtl ?? this.defaultTtl, ); - - // Always provide an error handler to avoid stopping the process. - concreteClient.on('error', (err: Error) => { - // In all cases, just log the error. - this.logger?.error('Failed to create cache client', err); - - // Invoke any custom error handler if provided. - if (typeof this.errorHandler === 'function') { - this.errorHandler(err); - } - }); - - return concreteClient; }; return new DefaultCacheClient( @@ -149,11 +136,17 @@ export class CacheManager { return (pluginId, defaultTtl) => { if (!store) { store = new KeyvRedis(this.connection); + // Always provide an error handler to avoid stopping the process + store.on('error', (err: Error) => { + this.logger?.error('Failed to create redis cache client', err); + this.errorHandler?.(err); + }); } return new Keyv({ namespace: pluginId, ttl: defaultTtl, store, + emitErrors: false, useRedisSets: this.useRedisSets, }); }; @@ -165,10 +158,16 @@ export class CacheManager { return (pluginId, defaultTtl) => { if (!store) { store = new KeyvMemcache(this.connection); + // Always provide an error handler to avoid stopping the process + store.on('error', (err: Error) => { + this.logger?.error('Failed to create memcache cache client', err); + this.errorHandler?.(err); + }); } return new Keyv({ namespace: pluginId, ttl: defaultTtl, + emitErrors: false, store, }); }; @@ -180,6 +179,7 @@ export class CacheManager { new Keyv({ namespace: pluginId, ttl: defaultTtl, + emitErrors: false, store, }); } From 6ec1f48baf47f3a529c92698224d4864fd2bb9c6 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 16:15:07 +0200 Subject: [PATCH 110/204] chore: supporting overriding using a function Signed-off-by: blam --- .../wiring/createExtensionBlueprint.test.tsx | 12 ++++++---- .../src/wiring/createExtensionBlueprint.ts | 24 +++++++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 38e92fc26a..dfdbfface6 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -328,18 +328,20 @@ describe('createExtensionBlueprint', () => { }); it('should allow providing callback for properties to set with params', () => { + type TestParams = { test: string }; + const Blueprint = createExtensionBlueprint({ kind: 'test-extension', attachTo: { id: 'test', input: 'default' }, + name: (params: TestParams) => `${params.test}-name`, output: [coreExtensionData.reactElement], - namespace: props => props.test, - name: props => `${props.test}-name`, + namespace: (props: TestParams) => props.test, config: { - schema: props => ({ + schema: (props: TestParams) => ({ test: z => z.string().default(props.test), }), }, - factory(params: { test: string }) { + factory(params: TestParams) { return [coreExtensionData.reactElement(
{params.test}
)]; }, }); @@ -359,7 +361,7 @@ describe('createExtensionBlueprint', () => { "additionalProperties": false, "properties": { "test": { - "default": "test", + "default": "hello", "type": "string", }, }, diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 4a8f4a7c46..46db278bf8 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -47,12 +47,13 @@ export type CreateExtensionBlueprintOptions< UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > = { - kind: string | ((params: TParams) => string); + kind: string; namespace?: string | ((params: TParams) => string); attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; + name?: string | ((params: TParams) => string); config?: { schema: TConfigSchema | ((params: TParams) => TConfigSchema); }; @@ -250,15 +251,30 @@ class ExtensionBlueprintImpl< > > > { + const optionsSchema = + typeof this.options.config?.schema === 'function' + ? this.options.config?.schema(args.params!) + : this.options.config?.schema; + const schema = { - ...this.options.config?.schema, + ...optionsSchema, ...args.config?.schema, } as TConfigSchema & TExtensionConfigSchema; + const namespace = + typeof this.options.namespace === 'function' + ? this.options.namespace(args.params!) + : this.options.namespace; + + const name = + typeof this.options.name === 'function' + ? this.options.name(args.params!) + : this.options.name; + return createExtension({ kind: this.options.kind, - namespace: args.namespace ?? this.options.namespace, - name: args.name, + namespace: args.namespace ?? namespace, + name: args.name ?? name, attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, inputs: args.inputs ?? this.options.inputs, From 210d066af41579732ee9b2862f47bf1b6f36e217 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 16:16:51 +0200 Subject: [PATCH 111/204] chore: added changeset Signed-off-by: blam --- .changeset/chilly-days-peel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-days-peel.md diff --git a/.changeset/chilly-days-peel.md b/.changeset/chilly-days-peel.md new file mode 100644 index 0000000000..d82344d364 --- /dev/null +++ b/.changeset/chilly-days-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. From 8025cffc3f2bb95e32beb9debcac91f5b28717b9 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 5 Aug 2024 16:33:06 +0200 Subject: [PATCH 112/204] Update breezy-rings-fly.md Signed-off-by: Ben Lambert --- .changeset/breezy-rings-fly.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/breezy-rings-fly.md b/.changeset/breezy-rings-fly.md index 315da5f950..c3cc1c143d 100644 --- a/.changeset/breezy-rings-fly.md +++ b/.changeset/breezy-rings-fly.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-react': patch --- -Fixed bug in DefaultTableOutputs where output elements overlapped in below approx. 800 px wide screens +Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes From cfbffadf638a5758599223afb08fc4ad483ea82e Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 16:33:57 +0200 Subject: [PATCH 113/204] chore: api-reports Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index d37f0a3a56..8d8dd05bf2 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -662,7 +662,7 @@ export type CreateExtensionBlueprintOptions< }, > = { kind: string; - namespace?: string; + namespace?: string | ((params: TParams) => string); attachTo: { id: string; input: string; @@ -670,8 +670,9 @@ export type CreateExtensionBlueprintOptions< disabled?: boolean; inputs?: TInputs; output: Array; + name?: string | ((params: TParams) => string); config?: { - schema: TConfigSchema; + schema: TConfigSchema | ((params: TParams) => TConfigSchema); }; factory( params: TParams, From 604a504328e95a8330e0dec0ada377cd139cbfe4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 16:38:22 +0200 Subject: [PATCH 114/204] catalog: add more filters for alpha entity cards Signed-off-by: Patrik Oldsberg --- .changeset/bright-donkeys-buy.md | 5 +++++ plugins/catalog/src/alpha/entityCards.tsx | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/bright-donkeys-buy.md diff --git a/.changeset/bright-donkeys-buy.md b/.changeset/bright-donkeys-buy.md new file mode 100644 index 0000000000..ded16ccc2c --- /dev/null +++ b/.changeset/bright-donkeys-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 332bd49c55..8a18efabde 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -46,6 +46,7 @@ export const catalogLabelsEntityCard = createEntityCardExtension({ export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ name: 'depends-on-components', + filter: 'kind:component', loader: async () => import('../components/DependsOnComponentsCard').then(m => compatWrapper(), @@ -54,6 +55,7 @@ export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ name: 'depends-on-resources', + filter: 'kind:component', loader: async () => import('../components/DependsOnResourcesCard').then(m => compatWrapper(), @@ -62,6 +64,7 @@ export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ export const catalogHasComponentsEntityCard = createEntityCardExtension({ name: 'has-components', + filter: 'kind:system', loader: async () => import('../components/HasComponentsCard').then(m => compatWrapper(), @@ -70,6 +73,7 @@ export const catalogHasComponentsEntityCard = createEntityCardExtension({ export const catalogHasResourcesEntityCard = createEntityCardExtension({ name: 'has-resources', + filter: 'kind:system', loader: async () => import('../components/HasResourcesCard').then(m => compatWrapper(), @@ -78,6 +82,7 @@ export const catalogHasResourcesEntityCard = createEntityCardExtension({ export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({ name: 'has-subcomponents', + filter: 'kind:component', loader: async () => import('../components/HasSubcomponentsCard').then(m => compatWrapper(), @@ -86,6 +91,7 @@ export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({ export const catalogHasSubdomainsEntityCard = createEntityCardExtension({ name: 'has-subdomains', + filter: 'kind:domain', loader: async () => import('../components/HasSubdomainsCard').then(m => compatWrapper(), @@ -94,6 +100,7 @@ export const catalogHasSubdomainsEntityCard = createEntityCardExtension({ export const catalogHasSystemsEntityCard = createEntityCardExtension({ name: 'has-systems', + filter: 'kind:domain', loader: async () => import('../components/HasSystemsCard').then(m => compatWrapper(), From 012e3ebf715aa78ed183fd4069152db933607c7d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 16:38:55 +0200 Subject: [PATCH 115/204] catalog-react: enable entity cards and content by default in new system Signed-off-by: Patrik Oldsberg --- .changeset/witty-geese-battle.md | 5 +++++ plugins/catalog-react/src/alpha.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/witty-geese-battle.md diff --git a/.changeset/witty-geese-battle.md b/.changeset/witty-geese-battle.md new file mode 100644 index 0000000000..082784cc8b --- /dev/null +++ b/.changeset/witty-geese-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index e81b8e5ebf..42d69726b6 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -83,7 +83,7 @@ export function createEntityCardExtension< id: 'entity-content:catalog/overview', input: 'cards', }, - disabled: options.disabled ?? true, + disabled: options.disabled, output: { element: coreExtensionData.reactElement, filterFunction: catalogExtensionData.entityFilterFunction.optional(), @@ -137,7 +137,7 @@ export function createEntityContentExtension< id: 'page:catalog/entity', input: 'contents', }, - disabled: options.disabled ?? true, + disabled: options.disabled, output: { element: coreExtensionData.reactElement, path: coreExtensionData.routePath, From 5cedd9f828e8b408502ea5d4644c3de06ceaf6ba Mon Sep 17 00:00:00 2001 From: Kamil Markow Date: Mon, 5 Aug 2024 13:19:31 -0400 Subject: [PATCH 116/204] Fix TechDocs Edit URL link when trailing slash not present Signed-off-by: Kamil Markow --- .changeset/tricky-rules-destroy.md | 5 +++++ plugins/techdocs-node/src/stages/generate/helpers.test.ts | 1 + plugins/techdocs-node/src/stages/generate/helpers.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/tricky-rules-destroy.md diff --git a/.changeset/tricky-rules-destroy.md b/.changeset/tricky-rules-destroy.md new file mode 100644 index 0000000000..a709e5b429 --- /dev/null +++ b/.changeset/tricky-rules-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Fix TechDocs Edit URL for nexted docs diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index 6ee3870845..695f5b8f2c 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -115,6 +115,7 @@ describe('helpers', () => { url | repo_url | edit_uri ${'https://github.com/backstage/backstage'} | ${'https://github.com/backstage/backstage'} | ${undefined} ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/docs'} + ${'https://github.com/backstage/backstage/tree/main/examples/techdocs'} | ${'https://github.com/backstage/backstage/tree/main/examples/techdocs'} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/docs'} ${'https://github.com/backstage/backstage/tree/main/'} | ${'https://github.com/backstage/backstage/tree/main/'} | ${'https://github.com/backstage/backstage/edit/main/docs'} ${'https://gitlab.com/backstage/backstage'} | ${'https://gitlab.com/backstage/backstage'} | ${undefined} ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${'https://gitlab.com/backstage/backstage/-/edit/main/examples/techdocs/docs'} diff --git a/plugins/techdocs-node/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts index 5efe96877f..9c1996a772 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -117,7 +117,7 @@ export const getRepoUrlFromLocationAnnotation = ( const sourceFolder = integration.resolveUrl({ url: `./${docsFolder}`, - base: target, + base: target.endsWith('/') ? target : `${target}/`, }); return { repo_url: target, From b5679af43cbfe8a78323e9a3a8aa1512d752e9b9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 19:29:21 +0200 Subject: [PATCH 117/204] backend: add example usage of feature loader Signed-off-by: Patrik Oldsberg --- packages/backend/src/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1174b7bf6d..952f3315c4 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,9 +15,21 @@ */ import { createBackend } from '@backstage/backend-defaults'; +import { createBackendFeatureLoader } from '@backstage/backend-plugin-api'; const backend = createBackend(); +// An example of how to group together and load multiple features. You can also +// access root-scoped services by adding `deps`. +const searchLoader = createBackendFeatureLoader({ + *loader() { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + }, +}); + backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('./authModuleGithubProvider')); backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); @@ -36,13 +48,10 @@ backend.add(import('@backstage/plugin-permission-backend/alpha')); backend.add(import('@backstage/plugin-proxy-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); -backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-backstage-openapi'), ); -backend.add(import('@backstage/plugin-search-backend/alpha')); +backend.add(searchLoader); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); backend.add(import('@backstage/plugin-signals-backend')); backend.add(import('@backstage/plugin-notifications-backend')); From 18916691d7c998989f2e9e69d5a3beba5be74f21 Mon Sep 17 00:00:00 2001 From: Kamil Markow Date: Mon, 5 Aug 2024 13:33:57 -0400 Subject: [PATCH 118/204] Fix typo Signed-off-by: Kamil Markow --- .changeset/tricky-rules-destroy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tricky-rules-destroy.md b/.changeset/tricky-rules-destroy.md index a709e5b429..196702f8c4 100644 --- a/.changeset/tricky-rules-destroy.md +++ b/.changeset/tricky-rules-destroy.md @@ -2,4 +2,4 @@ '@backstage/plugin-techdocs-node': patch --- -Fix TechDocs Edit URL for nexted docs +Fix TechDocs Edit URL for nested docs From 8b1318318bda3fd794cbecd0201b6c1424fa4f86 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 19:29:35 +0200 Subject: [PATCH 119/204] changesets: add changesets for feature loaders Signed-off-by: Patrik Oldsberg --- .changeset/curvy-pillows-joke.md | 5 +++++ .changeset/cyan-shrimps-push.md | 5 +++++ .changeset/khaki-lamps-peel.md | 26 ++++++++++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 .changeset/curvy-pillows-joke.md create mode 100644 .changeset/cyan-shrimps-push.md create mode 100644 .changeset/khaki-lamps-peel.md diff --git a/.changeset/curvy-pillows-joke.md b/.changeset/curvy-pillows-joke.md new file mode 100644 index 0000000000..25966a51c0 --- /dev/null +++ b/.changeset/curvy-pillows-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. diff --git a/.changeset/cyan-shrimps-push.md b/.changeset/cyan-shrimps-push.md new file mode 100644 index 0000000000..cb326b4da3 --- /dev/null +++ b/.changeset/cyan-shrimps-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. diff --git a/.changeset/khaki-lamps-peel.md b/.changeset/khaki-lamps-peel.md new file mode 100644 index 0000000000..82a2cbc9de --- /dev/null +++ b/.changeset/khaki-lamps-peel.md @@ -0,0 +1,26 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + +A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + +Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + +```ts +const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, +}); +``` From 299791de57fe54e1a037510862a664cebd17fa3e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 20:27:44 +0200 Subject: [PATCH 120/204] Update docs/permissions/plugin-authors/05-frontend-authorization.md Co-authored-by: Ben Lambert Signed-off-by: Patrik Oldsberg --- docs/permissions/plugin-authors/05-frontend-authorization.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index 4d673e3b4f..937730a9be 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -200,7 +200,6 @@ const routes = ( {/* highlight-add-end */} - }> {/* ... */} From 8d89264eb33e4f6516552efb8762c83047944288 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 20:33:55 +0200 Subject: [PATCH 121/204] Update docs/permissions/plugin-authors/05-frontend-authorization.md Co-authored-by: solimant Signed-off-by: Patrik Oldsberg --- docs/permissions/plugin-authors/05-frontend-authorization.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index 937730a9be..4d673e3b4f 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -200,6 +200,7 @@ const routes = ( {/* highlight-add-end */} + }> {/* ... */} From 115697842b98d4d52964beb22f050943c2010ab9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 20:48:20 +0200 Subject: [PATCH 122/204] docs/backend-system: move naming pattern docs Signed-off-by: Patrik Oldsberg --- docs/auth/identity-resolver.md | 2 +- docs/backend-system/architecture/03-services.md | 2 +- docs/backend-system/architecture/04-plugins.md | 2 +- .../{07-naming-patterns.md => 08-naming-patterns.md} | 0 docs/backend-system/building-plugins-and-modules/01-index.md | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename docs/backend-system/architecture/{07-naming-patterns.md => 08-naming-patterns.md} (100%) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 6eb52e5708..dbebf3056c 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -193,7 +193,7 @@ backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); backend.add(customAuth); ``` -Check out [the naming patterns article](../backend-system/architecture/07-naming-patterns.md) for what rules +Check out [the naming patterns article](../backend-system/architecture/08-naming-patterns.md) for what rules apply regarding how to form valid IDs. In this example we also put the module declaration directly in `packages/backend/src/index.ts` but that's just for simplicity. You can place it anywhere you like, including in other packages, and diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 95e3d5305a..57f6b20b73 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -38,7 +38,7 @@ export const fooServiceRef = createServiceRef({ The `fooServiceRef` that we create above should be exported, and can then be used to declare a dependency on the `FooService` interface and receive an implementation of it at runtime. -When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'.'`. For more naming patterns surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page. +When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'.'`. For more naming patterns surrounding services, see the [naming patterns](./08-naming-patterns.md#services) page. A note on naming: the frontend and backend systems intentionally use the separate names "APIs" and "Services" for concepts that are quite similar. This is to avoid confusion between the two, both in documentation and discussion, but also in code. While the two systems are quite similar, they are not identical, and they can't be used interchangeably. diff --git a/docs/backend-system/architecture/04-plugins.md b/docs/backend-system/architecture/04-plugins.md index af59c2a48a..5aef71e24f 100644 --- a/docs/backend-system/architecture/04-plugins.md +++ b/docs/backend-system/architecture/04-plugins.md @@ -10,7 +10,7 @@ Plugins provide the actual base features of a Backstage backend. Each plugin ope ## Defining a Plugin -Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./07-naming-patterns.md) about proper naming patterns. +Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./08-naming-patterns.md) about proper naming patterns. ```ts // plugins/example-backend/src/plugin.ts diff --git a/docs/backend-system/architecture/07-naming-patterns.md b/docs/backend-system/architecture/08-naming-patterns.md similarity index 100% rename from docs/backend-system/architecture/07-naming-patterns.md rename to docs/backend-system/architecture/08-naming-patterns.md diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 379ce19587..4dfca00d4e 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -68,7 +68,7 @@ that's specific to your plugin. In the example above, the logger might tag messages with your plugin ID, and the HTTP router might prefix API routes with your plugin ID, depending on the implementation used. -See [the article on naming patterns](../architecture/07-naming-patterns.md) for +See [the article on naming patterns](../architecture/08-naming-patterns.md) for details on how to best choose names/IDs for plugins and related backend system items. @@ -124,7 +124,7 @@ export const catalogModuleExampleCustomProcessor = createBackendModule({ export { catalogModuleExampleCustomProcessor as default } from './module'; ``` -See [the article on naming patterns](../architecture/07-naming-patterns.md) for +See [the article on naming patterns](../architecture/08-naming-patterns.md) for details on how to best choose names/IDs for modules and related backend system items. From a8982b8ff6560556c610ddd580d4ea14df4f5616 Mon Sep 17 00:00:00 2001 From: darylgraham Date: Tue, 6 Aug 2024 00:38:31 +0000 Subject: [PATCH 123/204] Update rootLogger customisation link and new backend health check documentation Signed-off-by: darylgraham --- docs/plugins/observability.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index e13653706f..f58fc4ae3d 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -17,7 +17,12 @@ See how to install Datadog Events in your app ### New Backend -The backend supplies a central logging service, [`rootLogger`](../backend-system/core-services/root-logger.md), as well as a plugin based logger, [`logger`](../backend-system/core-services/logger.md) from `coreServices`. To add additional granularity to your logs, you can create children from the plugin based logger, using the `.child()` method and provide is with JSON data. For example, if you wanted to log items for a specific span in your plugin, you could do +The backend supplies a central logging service, +[`rootLogger`](../backend-system/core-services/root-logger.md), as well as a plugin +based logger, [`logger`](../backend-system/core-services/logger.md) from `coreServices`. +To add additional granularity to your logs, you can create children from the plugin +based logger, using the `.child()` method and provide is with JSON data. For example, +if you wanted to log items for a specific span in your plugin, you could do ```ts export function createRouter({ logger }) { @@ -37,7 +42,9 @@ export function createRouter({ logger }) { } ``` -You can also add additional metadata to all logs for your Backstage instance by overriding the `rootLogger` implementation, you can see an example in [the `logger` docs](../backend-system/core-services/logger.md#configuring-the-service). +You can also add additional metadata to all logs for your Backstage instance by +overriding the `rootLogger` implementation, you can see an example in +[the `rootLogger` docs](../backend-system/core-services/root-logger#configuring-the-service). ### Old Backend @@ -63,9 +70,19 @@ An example log line could look as follows: ## Health Checks -### New Backend +### New Backend (post 1.29.0) -The new backend is moving towards health checks being plugin-based, as such there is no current plugin for providing a health check route. You can add this yourself easily though, +The new backend provides a `RootHealthService` which implements +`/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints +to provide health checks for the entire backend instance. + +You can read more about this new service and how to customize it in the +[Root Health Service documentation](../backend-system/core-services/root-health/). + +### New Backend (pre 1.29.0) + +The new backend is moving towards health checks being plugin-based, as such there is no +current plugin for providing a health check route. You can add this yourself easily though, ```ts import { From c6ef739a165534f076208e17bf4c31e8e10d3303 Mon Sep 17 00:00:00 2001 From: darylgraham Date: Tue, 6 Aug 2024 17:19:38 +1000 Subject: [PATCH 124/204] Update bad links Signed-off-by: darylgraham --- docs/plugins/observability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index f58fc4ae3d..324e6751d4 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -44,7 +44,7 @@ export function createRouter({ logger }) { You can also add additional metadata to all logs for your Backstage instance by overriding the `rootLogger` implementation, you can see an example in -[the `rootLogger` docs](../backend-system/core-services/root-logger#configuring-the-service). +[the `rootLogger` docs](../backend-system/core-services/root-logger.md#configuring-the-service). ### Old Backend @@ -77,7 +77,7 @@ The new backend provides a `RootHealthService` which implements to provide health checks for the entire backend instance. You can read more about this new service and how to customize it in the -[Root Health Service documentation](../backend-system/core-services/root-health/). +[Root Health Service documentation](../backend-system/core-services/root-health.md). ### New Backend (pre 1.29.0) From 0d16b529b7b7d8062d2da47dba07ec74896d742f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 6 Aug 2024 11:50:34 +0200 Subject: [PATCH 125/204] Add access restrictions to the JWKS external access method config schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/angry-dolphins-camp.md | 5 ++++ packages/backend-defaults/config.d.ts | 40 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 .changeset/angry-dolphins-camp.md diff --git a/.changeset/angry-dolphins-camp.md b/.changeset/angry-dolphins-camp.md new file mode 100644 index 0000000000..3c598f4a66 --- /dev/null +++ b/.changeset/angry-dolphins-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Add access restrictions to the JWKS external access method config schema diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index ab1e443fc2..1c784bce0c 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -287,6 +287,46 @@ export interface Config { */ subjectPrefix?: string; }; + /** + * Restricts what types of access that are permitted for this access + * method. If no access restrictions are given, it'll have unlimited + * access. This access restriction applies for the framework level; + * individual plugins may have their own access control mechanisms + * on top of this. + */ + accessRestrictions?: Array<{ + /** + * Permit access to make requests to this plugin. + * + * Can be further refined by setting additional fields below. + */ + plugin: string; + /** + * If given, this method is limited to only performing actions + * with these named permissions in this plugin. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permission?: string | Array; + /** + * If given, this method is limited to only performing actions + * whose permissions have these attributes. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permissionAttribute?: { + /** + * One of more of 'create', 'read', 'update', or 'delete'. + */ + action?: string | Array; + }; + }>; } >; }; From 72754db000eea3655aec89761173c835206a81f3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 10:44:30 +0200 Subject: [PATCH 126/204] frontend-plugin-api: make all route refs optional at all times Signed-off-by: Patrik Oldsberg --- .changeset/friendly-chicken-cry.md | 8 ++ .changeset/small-ears-poke.md | 7 ++ .../frontend-system/architecture/07-routes.md | 87 +++++++------------ .../app-next/src/examples/pagesPlugin.tsx | 14 +-- packages/core-compat-api/api-report.md | 22 ++--- .../compatWrapper/ForwardsCompatProvider.tsx | 2 +- .../src/compatWrapper/compatWrapper.test.tsx | 2 +- .../src/convertLegacyRouteRef.test.ts | 5 +- .../src/convertLegacyRouteRef.ts | 25 ++---- packages/frontend-app-api/api-report.md | 2 +- .../src/extensions/AppNav.tsx | 7 +- .../src/routing/RouteResolver.test.ts | 37 ++------ .../src/routing/RouteResolver.ts | 2 +- .../src/routing/resolveRouteBindings.ts | 10 +-- packages/frontend-plugin-api/api-report.md | 25 ++---- .../apis/definitions/RouteResolutionApi.ts | 2 +- .../src/routing/ExternalRouteRef.test.ts | 67 ++++---------- .../src/routing/ExternalRouteRef.ts | 28 ++---- .../src/routing/useRouteRef.test.tsx | 19 +++- .../src/routing/useRouteRef.tsx | 42 +-------- .../src/app/createExtensionTester.tsx | 7 +- plugins/api-docs/api-report-alpha.md | 2 +- .../AppVisualizerPage/DetailedVisualizer.tsx | 10 +-- plugins/catalog-graph/api-report-alpha.md | 13 ++- plugins/catalog/api-report-alpha.md | 28 +++--- plugins/org/api-report-alpha.md | 2 +- plugins/scaffolder/api-report-alpha.md | 15 ++-- 27 files changed, 172 insertions(+), 318 deletions(-) create mode 100644 .changeset/friendly-chicken-cry.md create mode 100644 .changeset/small-ears-poke.md diff --git a/.changeset/friendly-chicken-cry.md b/.changeset/friendly-chicken-cry.md new file mode 100644 index 0000000000..3eadca8777 --- /dev/null +++ b/.changeset/friendly-chicken-cry.md @@ -0,0 +1,8 @@ +--- +'@backstage/frontend-test-utils': patch +'@backstage/frontend-app-api': patch +'@backstage/core-compat-api': patch +'@backstage/plugin-app-visualizer': patch +--- + +Updated usage of `useRouteRef`, which can now always return `undefined`. diff --git a/.changeset/small-ears-poke.md b/.changeset/small-ears-poke.md new file mode 100644 index 0000000000..de906a8643 --- /dev/null +++ b/.changeset/small-ears-poke.md @@ -0,0 +1,7 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +**BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + +This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 9c6617a06f..9db4d007b6 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -101,23 +101,26 @@ export const IndexPage = () => { return (

Index Page

-
- See "Foo" details - + {/* highlight-next-line */} + {getDetailsPath && ( + + See "Foo" details + + )}
); }; ``` -We use the `useRouteRef` hook to create a link generator function that returns the details page path. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to the "Foo" details page. +We use the `useRouteRef` hook to create a link generator function that returns the details page path. First we need to check whether the route is available, the link generator function will be `undefined` if it isn't. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to the "Foo" details page. Let's see how the details page can get the parameters from the URL: @@ -176,8 +179,11 @@ export const IndexPage = () => { return (

Index Page

- {/* highlight-next-line */} - Create Component + {/* highlight-start */} + {getCreateComponentPath && ( + Create Component + )} + {/* highlight-end */}
); }; @@ -289,42 +295,6 @@ export const createComponentExternalRouteRef = createExternalRouteRef({ }); ``` -### Optional External Route References - -It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. - -```tsx title="plugins/catalog/src/routes.ts" -import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; - -export const createComponentExternalRouteRef = createExternalRouteRef({ - // highlight-next-line - optional: true, -}); -``` - -When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken: - -```tsx title="plugins/catalog/src/components/IndexPage.tsx" -import React from 'react'; -import { useRouteRef } from '@backstage/frontend-plugin-api'; -import { createComponentExternalRouteRef } from '../routes'; - -export const IndexPage = () => { - const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef); - return ( -
-

Index Page

- {/* Rendering the link only if the getCreateComponentPath is defined */} - {/* highlight-start */} - {getCreateComponentPath && ( - Create Component - )} - {/* highlight-end */} -
- ); -}; -``` - ## Sub Route References The last kind of route ref that can be created is a `SubRouteRef`, which can be used to create a route ref with a fixed path relative to an absolute `RouteRef`. They are useful if you have a page that internally is mounted at a sub route of a page extension component, and you want other plugins to be able to route to that page. And they can be a useful utility to handle routing within a plugin itself as well. @@ -368,17 +338,21 @@ import { DetailsPage } from './DetailsPage'; export const IndexPage = () => { const { pathname } = useLocation(); + + // highlight-start const getIndexPath = useRouteRef(indexRouteRef); const getDetailsPath = useRouteRef(detailsSubRouteRef); + // highlight-end + return (

Index Page

{/* Linking to the details sub route */} - {pathname === getIndexPath() ? ( - // highlight-start + {/* highlight-start */} + {pathname === getIndexPath?.() ? ( { > Show details - // highlight-end + {/* highlight-end */} ) : ( - // highlight-next-line - Hide details + {/* highlight-next-line */} + Hide details )} {/* Registering the details sub route */} + {/* highlight-next-line */} } />
diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx index 826040a6cb..095c992e18 100644 --- a/packages/app-next/src/examples/pagesPlugin.tsx +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -47,9 +47,11 @@ const IndexPage = createPageExtension({ return (
op -
- Page 1 -
+ {page1Link && ( +
+ Page 1 +
+ )}
Home
@@ -82,10 +84,10 @@ const Page1 = createPageExtension({ return (

This is page 1

- Go back + {indexLink && Go back} Page 2 {/* Page 2 */} - Page X + {xLink && Page X}
Sub-page content: @@ -115,7 +117,7 @@ const ExternalPage = createPageExtension({ return (

This is page X

- Go back + {indexLink && Go back}
); }; diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index 3cd78e6f30..8d177b4dab 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -37,12 +37,9 @@ export function convertLegacyRouteRef( ): SubRouteRef_2; // @public -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: ExternalRouteRef, -): ExternalRouteRef_2; +export function convertLegacyRouteRef( + ref: ExternalRouteRef, +): ExternalRouteRef_2; // @public export function convertLegacyRouteRef( @@ -55,12 +52,9 @@ export function convertLegacyRouteRef( ): SubRouteRef; // @public -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: ExternalRouteRef_2, -): ExternalRouteRef; +export function convertLegacyRouteRef( + ref: ExternalRouteRef_2, +): ExternalRouteRef; // @public export function convertLegacyRouteRefs< @@ -93,8 +87,8 @@ export type ToNewRouteRef = ? RouteRef_2 : T extends SubRouteRef ? SubRouteRef_2 - : T extends ExternalRouteRef - ? ExternalRouteRef_2 + : T extends ExternalRouteRef + ? ExternalRouteRef_2 : never; // (No @packageDocumentation comment for this package) diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index b2200387cc..9c646d5b00 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -111,7 +111,7 @@ class CompatRouteResolutionApi implements RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions | undefined, ): RouteFunc | undefined { const legacyRef = convertLegacyRouteRef(anyRouteRef as RouteRef); diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index 6a27bf19e8..04e2361087 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -129,7 +129,7 @@ describe('ForwardsCompatProvider', () => { function Component() { const link = useNewRouteRef(routeRef); - return
link: {link()}
; + return
link: {link?.()}
; } await renderInOldTestApp(compatWrapper(), { diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts index bf82d1e034..f09060212c 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -123,13 +123,11 @@ describe('convertLegacyRouteRef', () => { 'routeRef{type=external,id=ref3}', ); expect(ref3Internal.getParams()).toEqual([]); - expect(ref3Internal.optional).toBe(false); expect(ref4Internal.getDefaultTarget()).toBe('ref2'); expect(ref4Internal.getDescription()).toBe( 'routeRef{type=external,id=ref4}', ); expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); - expect(ref4Internal.optional).toBe(true); }); it('converts new to old', () => { @@ -149,7 +147,6 @@ describe('convertLegacyRouteRef', () => { }); const ref3 = createNewExternalRouteRef(); const ref4 = createNewExternalRouteRef({ - optional: true, defaultTarget: 'ref2', params: ['p1', 'p2'], }); @@ -196,7 +193,7 @@ describe('convertLegacyRouteRef', () => { /^ExternalRouteRef\{created at '.*'\}$/, ); expect(ref3Converted.params).toEqual([]); - expect(ref3Converted.optional).toBe(false); + expect(ref3Converted.optional).toBe(true); expect(String(ref4Converted)).toMatch( /^ExternalRouteRef\{created at '.*'\}$/, ); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index 24c589bb87..40a95a81ff 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -51,8 +51,8 @@ export type ToNewRouteRef< ? RouteRef : T extends LegacySubRouteRef ? SubRouteRef - : T extends LegacyExternalRouteRef - ? ExternalRouteRef + : T extends LegacyExternalRouteRef + ? ExternalRouteRef : never; /** @@ -109,12 +109,9 @@ export function convertLegacyRouteRef( * * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. */ -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: LegacyExternalRouteRef, -): ExternalRouteRef; +export function convertLegacyRouteRef( + ref: LegacyExternalRouteRef, +): ExternalRouteRef; /** * A temporary helper to convert a new route ref to the legacy system. @@ -148,12 +145,9 @@ export function convertLegacyRouteRef( * * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. */ -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: ExternalRouteRef, -): LegacyExternalRouteRef; +export function convertLegacyRouteRef( + ref: ExternalRouteRef, +): LegacyExternalRouteRef; export function convertLegacyRouteRef( ref: | LegacyRouteRef @@ -209,6 +203,7 @@ function convertNewToOld( const newRef = toInternalExternalRouteRef(ref); return Object.assign(ref, { [routeRefType]: 'external', + optional: true, params: newRef.getParams(), defaultTarget: newRef.getDefaultTarget(), } as Omit) as unknown as LegacyExternalRouteRef; @@ -282,7 +277,6 @@ function convertOldToNew( const newRef = toInternalExternalRouteRef( createExternalRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], - optional: legacyRef.optional, defaultTarget: 'getDefaultTarget' in legacyRef ? (legacyRef.getDefaultTarget as () => string | undefined)() @@ -293,7 +287,6 @@ function convertOldToNew( $$type: '@backstage/ExternalRouteRef' as const, version: 'v1', T: newRef.T, - optional: newRef.optional, getParams() { return newRef.getParams(); }, diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 0375a67482..27d486b6a8 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -44,7 +44,7 @@ export type CreateAppRouteBinder = < externalRoutes: TExternalRoutes, targetRoutes: PartialKeys< TargetRouteMap, - KeysWithType> + KeysWithType> >, ) => void; diff --git a/packages/frontend-app-api/src/extensions/AppNav.tsx b/packages/frontend-app-api/src/extensions/AppNav.tsx index 761c6d5a24..ffef1c7c61 100644 --- a/packages/frontend-app-api/src/extensions/AppNav.tsx +++ b/packages/frontend-app-api/src/extensions/AppNav.tsx @@ -73,9 +73,12 @@ const SidebarNavItem = ( props: (typeof createNavItemExtension.targetDataRef)['T'], ) => { const { icon: Icon, title, routeRef } = props; - const to = useRouteRef(routeRef)(); + const link = useRouteRef(routeRef); + if (!link) { + return null; + } // TODO: Support opening modal, for example, the search one - return ; + return ; }; export const AppNav = createExtension({ diff --git a/packages/frontend-app-api/src/routing/RouteResolver.test.ts b/packages/frontend-app-api/src/routing/RouteResolver.test.ts index ba0a66a380..497e7397cb 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.test.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.test.ts @@ -41,9 +41,7 @@ const subRef2 = createSubRouteRef({ parent: ref1, path: '/foo/:a' }); const subRef3 = createSubRouteRef({ parent: ref2, path: '/bar' }); const subRef4 = createSubRouteRef({ parent: ref2, path: '/bar/:a' }); const externalRef1 = createExternalRouteRef(); -const externalRef2 = createExternalRouteRef({ optional: true }); -const externalRef3 = createExternalRouteRef({ params: ['x'] }); -const externalRef4 = createExternalRouteRef({ optional: true, params: ['x'] }); +const externalRef2 = createExternalRouteRef({ params: ['x'] }); function src(sourcePath: string) { return { sourcePath }; @@ -62,9 +60,7 @@ describe('RouteResolver', () => { undefined, ); expect(r.resolve(externalRef1, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe(undefined); + expect(r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toBe(undefined); }); it('should resolve an absolute route', () => { @@ -87,9 +83,7 @@ describe('RouteResolver', () => { undefined, ); expect(r.resolve(externalRef1, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe(undefined); + expect(r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toBe(undefined); }); it('should resolve an absolute route with a param and with a parent', () => { @@ -112,8 +106,7 @@ describe('RouteResolver', () => { ], new Map([ [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], + [externalRef2, subRef3], ]), '', ); @@ -133,11 +126,7 @@ describe('RouteResolver', () => { '/my-route/my-parent/4x/bar/4a', ); expect(r.resolve(externalRef1, src('/'))?.()).toBe('/my-route'); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe( - '/my-route/my-parent/5x', - ); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe( + expect(r.resolve(externalRef2, src('/'))?.({ x: '6x' })).toBe( '/my-route/my-parent/6x/bar', ); }); @@ -230,8 +219,7 @@ describe('RouteResolver', () => { ], new Map([ [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], + [externalRef2, subRef3], ]), '', ); @@ -282,17 +270,10 @@ describe('RouteResolver', () => { expect(() => r.resolve(externalRef1, src('/'))?.()).toThrow( /^Cannot route.*with parent.*as it has parameters$/, ); - expect(r.resolve(externalRef2, src(l))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src(l))?.({ x: '5x' })).toBe( - '/my-grandparent/my-y/my-parent/5x', + expect(r.resolve(externalRef2, src(l))?.({ x: '5x' })).toBe( + '/my-grandparent/my-y/my-parent/5x/bar', ); - expect(() => r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(externalRef4, src(l))?.({ x: '6x' })).toBe( - '/my-grandparent/my-y/my-parent/6x/bar', - ); - expect(() => r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toThrow( + expect(() => r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toThrow( /^Cannot route.*with parent.*as it has parameters$/, ); }); diff --git a/packages/frontend-app-api/src/routing/RouteResolver.ts b/packages/frontend-app-api/src/routing/RouteResolver.ts index 1ec1822fe9..ff7966c4db 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.ts @@ -195,7 +195,7 @@ export class RouteResolver implements RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined { // First figure out what our target absolute ref is, as well as our target path. diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts index 81d9eddef3..a8f641daff 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts @@ -53,8 +53,7 @@ type TargetRouteMap< ExternalRoutes extends { [name: string]: ExternalRouteRef }, > = { [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< - infer Params, - any + infer Params > ? RouteRef | SubRouteRef : never; @@ -72,7 +71,7 @@ export type CreateAppRouteBinder = < externalRoutes: TExternalRoutes, targetRoutes: PartialKeys< TargetRouteMap, - KeysWithType> + KeysWithType> >, ) => void; @@ -95,11 +94,6 @@ export function resolveRouteBindings( if (!externalRoute) { throw new Error(`Key ${key} is not an existing external route`); } - if (!value && !externalRoute.optional) { - throw new Error( - `External route ${key} is required but was undefined`, - ); - } if (value) { result.set(externalRoute, value); } diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index d37f0a3a56..93f300c591 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -799,13 +799,11 @@ export function createExternalRouteRef< [param in TParamKeys]: string; } | undefined = undefined, - TOptional extends boolean = false, TParamKeys extends string = string, >(options?: { readonly params?: string extends TParamKeys ? (keyof TParams)[] : TParamKeys[]; - optional?: TOptional; defaultTarget?: string; }): ExternalRouteRef< keyof TParams extends never @@ -814,8 +812,7 @@ export function createExternalRouteRef< ? TParams : { [param in TParamKeys]: string; - }, - TOptional + } >; // @public @@ -1300,13 +1297,10 @@ export interface ExtensionOverridesOptions { // @public export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, > { // (undocumented) readonly $$type: '@backstage/ExternalRouteRef'; // (undocumented) - readonly optional: TOptional; - // (undocumented) readonly T: TParams; } @@ -1570,7 +1564,7 @@ export interface RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined; } @@ -1631,18 +1625,13 @@ export function useComponentRef( ref: ComponentRef, ): ComponentType; -// @public -export function useRouteRef< - TOptional extends boolean, - TParams extends AnyRouteRefParams, ->( - routeRef: ExternalRouteRef, -): TOptional extends true ? RouteFunc | undefined : RouteFunc; - // @public export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; + routeRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, +): RouteFunc | undefined; // @public export function useRouteRefParams( diff --git a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts index 06388cc966..30569bde15 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts @@ -60,7 +60,7 @@ export interface RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined; } diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts index 727dc1f53e..97d3dd2264 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts @@ -26,7 +26,6 @@ describe('ExternalRouteRef', () => { const routeRef: ExternalRouteRef = createExternalRouteRef(); const internal = toInternalExternalRouteRef(routeRef); expect(internal.getParams()).toEqual([]); - expect(internal.optional).toBe(false); expect(String(internal)).toMatch( /^ExternalRouteRef\{created at '.*ExternalRouteRef\.test\.ts.*'\}$/, @@ -35,16 +34,6 @@ describe('ExternalRouteRef', () => { expect(String(internal)).toBe('ExternalRouteRef{some-id}'); }); - it('should be created as optional', () => { - const routeRef: ExternalRouteRef = createExternalRouteRef({ - params: [], - optional: true, - }); - const internal = toInternalExternalRouteRef(routeRef); - expect(internal.getParams()).toEqual([]); - expect(internal.optional).toEqual(true); - }); - it('should be created with params', () => { const routeRef: ExternalRouteRef<{ x: string; @@ -52,63 +41,39 @@ describe('ExternalRouteRef', () => { }> = createExternalRouteRef({ params: ['x', 'y'] }); const internal = toInternalExternalRouteRef(routeRef); expect(internal.getParams()).toEqual(['x', 'y']); - expect(internal.optional).toEqual(false); - }); - - it('should be created as optional with params', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ params: ['x', 'y'], optional: true }); - const internal = toInternalExternalRouteRef(routeRef); - expect(internal.getParams()).toEqual(['x', 'y']); - expect(internal.optional).toEqual(true); }); it('should properly infer and validate parameter types and assignments', () => { - function checkRouteRef< - T extends AnyRouteRefParams, - TOptional extends boolean, - TCheck extends TOptional, - >( - _ref: ExternalRouteRef, + function checkRouteRef( + _ref: ExternalRouteRef, _params: T extends undefined ? undefined : T, - _optional: TCheck, ) {} - const _1 = createExternalRouteRef({ params: ['notX'] }); - checkRouteRef(_1, { notX: '' }, false); + const _1 = createExternalRouteRef(); + checkRouteRef(_1, undefined); // @ts-expect-error - checkRouteRef(_1, { x: '' }, false); + checkRouteRef(_1, { x: '' }); - const _2 = createExternalRouteRef({ params: ['x'], optional: true }); - checkRouteRef(_2, { x: '' }, true); + const _2 = createExternalRouteRef({ params: ['x'] }); + checkRouteRef(_2, { x: '' }); // @ts-expect-error - checkRouteRef(_2, undefined, false); + checkRouteRef(_2, { notX: '' }); + // @ts-expect-error + checkRouteRef(_2, undefined); const _3 = createExternalRouteRef({ params: ['x', 'y'] }); - checkRouteRef(_3, { x: '', y: '' }, false); + checkRouteRef(_3, { x: '', y: '' }); // @ts-expect-error - checkRouteRef(_3, { x: '' }, false); + checkRouteRef(_3, { x: '' }); // @ts-expect-error - checkRouteRef(_3, { x: '', y: '', z: '' }, false); + checkRouteRef(_3, { x: '', y: '', z: '' }); const _4 = createExternalRouteRef({ params: [] }); - checkRouteRef(_4, undefined, false); + checkRouteRef(_4, undefined); // @ts-expect-error - checkRouteRef(_4, { x: '' }); - - const _5 = createExternalRouteRef(); - checkRouteRef(_5, undefined, false); - // @ts-expect-error - checkRouteRef(_5, { x: '' }); - - const _6 = createExternalRouteRef({ optional: true }); - checkRouteRef(_6, undefined, true); - // @ts-expect-error - checkRouteRef(_6, undefined, false); + checkRouteRef(_4, { x: '' }); // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String)); + expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); }); }); diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts index 7a754ffa7b..6ce36707c0 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts @@ -29,18 +29,15 @@ import { AnyRouteRefParams } from './types'; */ export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, > { readonly $$type: '@backstage/ExternalRouteRef'; readonly T: TParams; - readonly optional: TOptional; } /** @internal */ export interface InternalExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, -> extends ExternalRouteRef { +> extends ExternalRouteRef { readonly version: 'v1'; getParams(): string[]; getDescription(): string; @@ -52,11 +49,8 @@ export interface InternalExternalRouteRef< /** @internal */ export function toInternalExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, ->( - resource: ExternalRouteRef, -): InternalExternalRouteRef { - const r = resource as InternalExternalRouteRef; +>(resource: ExternalRouteRef): InternalExternalRouteRef { + const r = resource as InternalExternalRouteRef; if (r.$$type !== '@backstage/ExternalRouteRef') { throw new Error(`Invalid ExternalRouteRef, bad type '${r.$$type}'`); } @@ -79,7 +73,6 @@ class ExternalRouteRefImpl readonly $$type = '@backstage/ExternalRouteRef' as any; constructor( - readonly optional: boolean, readonly params: string[] = [], readonly defaultTarget: string | undefined, creationSite: string, @@ -104,7 +97,6 @@ class ExternalRouteRefImpl */ export function createExternalRouteRef< TParams extends { [param in TParamKeys]: string } | undefined = undefined, - TOptional extends boolean = false, TParamKeys extends string = string, >(options?: { /** @@ -114,14 +106,6 @@ export function createExternalRouteRef< ? (keyof TParams)[] : TParamKeys[]; - /** - * Whether or not this route is optional, defaults to false. - * - * Optional external routes are not required to be bound in the app, and - * if they aren't, `useExternalRouteRef` will return `undefined`. - */ - optional?: TOptional; - /** * The route (typically in another plugin) that this should map to by default. * @@ -134,13 +118,11 @@ export function createExternalRouteRef< ? undefined : string extends TParamKeys ? TParams - : { [param in TParamKeys]: string }, - TOptional + : { [param in TParamKeys]: string } > { return new ExternalRouteRefImpl( - Boolean(options?.optional), options?.params as string[] | undefined, options?.defaultTarget, describeParentCallSite(), - ) as ExternalRouteRef; + ); } diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx b/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx index 4f43cf8d01..bdac95a5a0 100644 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx @@ -45,7 +45,7 @@ describe('v1 consumer', () => { }); const routeFunc = renderedHook.result.current; - expect(routeFunc()).toBe('/hello'); + expect(routeFunc?.()).toBe('/hello'); expect(resolve).toHaveBeenCalledWith( routeRef, expect.objectContaining({ @@ -54,6 +54,23 @@ describe('v1 consumer', () => { ); }); + it('should ignore missing routes', () => { + const routeRef = createRouteRef(); + + const renderedHook = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + undefined }]]} + > + + + ), + }); + + const routeFunc = renderedHook.result.current; + expect(routeFunc).toBeUndefined(); + }); + it('re-resolves the routeFunc when the search parameters change', () => { const resolve = jest.fn(() => () => '/hello'); diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.tsx b/packages/frontend-plugin-api/src/routing/useRouteRef.tsx index fdf831953d..2d2b3a5458 100644 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.tsx +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.tsx @@ -30,47 +30,14 @@ import { RouteFunc, routeResolutionApiRef, useApi } from '../apis'; * See {@link https://backstage.io/docs/plugins/composability#routing-system} * * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. - * @public - */ -export function useRouteRef< - TOptional extends boolean, - TParams extends AnyRouteRefParams, ->( - routeRef: ExternalRouteRef, -): TOptional extends true ? RouteFunc | undefined : RouteFunc; - -/** - * React hook for constructing URLs to routes. - * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system} - * - * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. - * @public - */ -export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; - -/** - * React hook for constructing URLs to routes. - * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system} - * - * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. + * @returns A function that will in turn return the concrete URL of the `routeRef`, or `undefined` if the route is not available. * @public */ export function useRouteRef( routeRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, ): RouteFunc | undefined { const { pathname } = useLocation(); const routeResolutionApi = useApi(routeResolutionApiRef); @@ -80,10 +47,5 @@ export function useRouteRef( [routeResolutionApi, routeRef, pathname], ); - const isOptional = 'optional' in routeRef && routeRef.optional; - if (!routeFunc && !isOptional) { - throw new Error(`No path for ${routeRef}`); - } - return routeFunc; } diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 1d41958a09..bbe52c3fbc 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -56,10 +56,13 @@ const NavItem = (props: { icon: IconComponent; }) => { const { routeRef, title, icon: Icon } = props; - const to = useRouteRef(routeRef)(); + const link = useRouteRef(routeRef); + if (!link) { + return null; + } return (
  • - + {title}
  • diff --git a/plugins/api-docs/api-report-alpha.md b/plugins/api-docs/api-report-alpha.md index 1d22283884..f17ba0fa3c 100644 --- a/plugins/api-docs/api-report-alpha.md +++ b/plugins/api-docs/api-report-alpha.md @@ -13,7 +13,7 @@ const _default: BackstagePlugin< root: RouteRef; }, { - registerApi: ExternalRouteRef; + registerApi: ExternalRouteRef; } >; export default _default; diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 2356f38cfa..b2c9685bab 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -189,18 +189,12 @@ function OutputLink(props: { }) { const routeRef = props.node?.instance?.getData(coreExtensionData.routeRef); - let link: string | undefined = undefined; - try { - // eslint-disable-next-line react-hooks/rules-of-hooks - link = useRouteRef(routeRef as RouteRef)(); - } catch { - /* ignore */ - } + const link = useRouteRef(routeRef as RouteRef); return ( {props.dataRef.id}}> - {link ? link : null} + {link ? link : null} ); diff --git a/plugins/catalog-graph/api-report-alpha.md b/plugins/catalog-graph/api-report-alpha.md index 3895f12fcf..88b3a38b76 100644 --- a/plugins/catalog-graph/api-report-alpha.md +++ b/plugins/catalog-graph/api-report-alpha.md @@ -13,14 +13,11 @@ const _default: BackstagePlugin< catalogGraph: RouteRef; }, { - catalogEntity: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; + catalogEntity: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; } >; export default _default; diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index f79d65394a..336bf4bc2b 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -121,23 +121,17 @@ const _default: BackstagePlugin< }>; }, { - viewTechDoc: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; - createComponent: ExternalRouteRef; - createFromTemplate: ExternalRouteRef< - { - namespace: string; - templateName: string; - }, - true - >; - unregisterRedirect: ExternalRouteRef; + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + createComponent: ExternalRouteRef; + createFromTemplate: ExternalRouteRef<{ + namespace: string; + templateName: string; + }>; + unregisterRedirect: ExternalRouteRef; } >; export default _default; diff --git a/plugins/org/api-report-alpha.md b/plugins/org/api-report-alpha.md index 8c5391d857..e7c4db2c6d 100644 --- a/plugins/org/api-report-alpha.md +++ b/plugins/org/api-report-alpha.md @@ -10,7 +10,7 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; const _default: BackstagePlugin< {}, { - catalogIndex: ExternalRouteRef; + catalogIndex: ExternalRouteRef; } >; export default _default; diff --git a/plugins/scaffolder/api-report-alpha.md b/plugins/scaffolder/api-report-alpha.md index 3f0b60cd95..4628563be2 100644 --- a/plugins/scaffolder/api-report-alpha.md +++ b/plugins/scaffolder/api-report-alpha.md @@ -30,15 +30,12 @@ const _default: BackstagePlugin< edit: SubRouteRef; }, { - registerComponent: ExternalRouteRef; - viewTechDoc: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; + registerComponent: ExternalRouteRef; + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; } >; export default _default; From f71e33bc6de94a785048c20d78a86b97b0926f14 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 13:26:28 +0200 Subject: [PATCH 127/204] Update .changeset/neat-socks-cheer.md Signed-off-by: Patrik Oldsberg --- .changeset/neat-socks-cheer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/neat-socks-cheer.md b/.changeset/neat-socks-cheer.md index 5e2879b0a3..ffc76bec3f 100644 --- a/.changeset/neat-socks-cheer.md +++ b/.changeset/neat-socks-cheer.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-node': patch +'@backstage/plugin-auth-node': minor --- -allow declarative sign in resolvers declared in `signInResolverFactories` to take precedence over the statically defined sign in resolvers in `signInResolver` for the `createOAuthProviderFactory`. +**BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. From 0724159d17160e59682e351e2cd33c4c860ab855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 6 Aug 2024 13:32:32 +0200 Subject: [PATCH 128/204] tech insights has been moved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- microsite/data/plugins/tech-insights.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/tech-insights.yaml b/microsite/data/plugins/tech-insights.yaml index fbf96df3e6..3bd40d576a 100644 --- a/microsite/data/plugins/tech-insights.yaml +++ b/microsite/data/plugins/tech-insights.yaml @@ -6,7 +6,7 @@ category: Discovery description: Visualize, understand and optimize your team's tech health. documentation: https://roadie.io/backstage/plugins/tech-insights/ iconUrl: https://roadie.io/images/logos/tech-insights.png -npmPackageName: '@backstage/plugin-tech-insights' +npmPackageName: '@backstage-community/plugin-tech-insights' tags: - tech-insights - reporting From 25a4c77a2ec11914f61457560dc7b7f4ae417d47 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 5 Aug 2024 20:48:30 +0200 Subject: [PATCH 129/204] docs/backend-system: add feature loader docs Signed-off-by: Patrik Oldsberg --- .../architecture/07-feature-loaders.md | 101 ++++++++++++++++++ microsite/sidebars.json | 1 + 2 files changed, 102 insertions(+) create mode 100644 docs/backend-system/architecture/07-feature-loaders.md diff --git a/docs/backend-system/architecture/07-feature-loaders.md b/docs/backend-system/architecture/07-feature-loaders.md new file mode 100644 index 0000000000..cf52a1feb3 --- /dev/null +++ b/docs/backend-system/architecture/07-feature-loaders.md @@ -0,0 +1,101 @@ +--- +id: feature-loaders +title: Backend Feature Loaders +sidebar_label: Feature Loaders +# prettier-ignore +description: Backend feature loaders +--- + +Backend feature loaders are used to programmatically select and install features in a Backstage backend. They can service a wide range of use cases, such as enabling or disabling features based on static configuration, dynamically load features at runtime, or conditionally load features based on the state of a system. + +Feature loaders are defined using the `createBackendFeatureLoader` function, exported by `@backstage/backend-plugin-api`. It accepts a `loader` function, as well as an optional `deps` object for declaring service dependencies. Unlike plugins and modules, feature loaders are limited to only depending on root-scoped services, but that still allows access to for example the [root config](../core-services/root-config.md) and [root logger](../core-services/root-logger.md) services. + +The `loader` function can be defined in many different ways, with the main requirement being that it returns a list of `BackendFeature`s in some form. A backend feature is the kind of object that you can pass to `backend.add(...)`, for example services factories, plugins, modules, or even other feature loaders. The `loader` function can be synchronous or asynchronous, and can be defined as a generator function to allow for more complex logic. + +## Examples + +The following are a few example of how feature loaders can be used: + +### Simple list of features + +A feature loader can simply return a list of features to be installed: + +```ts +export default createBackendFeatureLoader({ + loader() { + return [ + import('@backstage/plugin-search-backend/alpha'), + import('@backstage/plugin-search-backend-module-catalog/alpha'), + import('@backstage/plugin-search-backend-module-explore/alpha'), + import('@backstage/plugin-search-backend-module-techdocs/alpha'), + ]; + }, +}); +``` + +It can also encapsulate a collection of custom features: + +```ts +export default createBackendFeatureLoader({ + // Async loader is fine too + async loader() { + return [ + createBackendPlugin({ + ... + }), + createBackendModule({ + ... + }), + ] + }, +}); +``` + +### Conditional loading + +A feature loader can access root-scoped services, such as the config service. This allows for conditional loading of features based on configuration. It is often convenient to use a generator function for this purpose: + +```ts +export default createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + // The `*` in front of the function name makes it a generator function + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, +}); +``` + +### Dynamic logic + +A feature loader can also be asynchronous, and for example fetch data from an external source to determine which features to load: + +```ts +export default createBackendFeatureLoader({ + // The `async *` in front of the function name makes it an async generator function. + async *loader() { + const localMetadata = await readMetadataFromDisk(); + + if (localMetadata.enableSearch) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + + const remoteMetadata = await fetchMetadata(); + + if (remoteMetadata.enableExplore) { + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + } + if (remoteMetadata.enableTechDocs) { + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + } + }, +}); +``` diff --git a/microsite/sidebars.json b/microsite/sidebars.json index d13ebc71c6..dafd3d1d5e 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -393,6 +393,7 @@ "backend-system/architecture/plugins", "backend-system/architecture/extension-points", "backend-system/architecture/modules", + "backend-system/architecture/feature-loaders", "backend-system/architecture/naming-patterns" ] }, From c7126d657bfddba371f18f2dc975cc7f73dbd5e6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 13:42:02 +0200 Subject: [PATCH 130/204] review fixes Signed-off-by: Patrik Oldsberg --- .../backend-app-api/src/wiring/BackendInitializer.ts | 11 +++++------ .../src/routing/describeParentCallSite.ts | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 4bfbf6244f..c0da9eda62 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -25,14 +25,14 @@ import { } from '@backstage/backend-plugin-api'; import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication -// eslint-disable-next-line @backstage/no-forbidden-package-imports +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { InternalBackendFeature, InternalBackendFeatureLoader, InternalBackendRegistrations, -} from '@backstage/backend-plugin-api/src/wiring/types'; -// eslint-disable-next-line @backstage/no-forbidden-package-imports -import type { InternalServiceFactory } from '@backstage/backend-plugin-api/src/services/system/types'; +} from '../../../backend-plugin-api/src/wiring/types'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types'; import { ForwardedError, ConflictError } from '@backstage/errors'; import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; @@ -431,8 +431,7 @@ export class BackendInitializer { if (isBackendFeatureLoader(feature)) { newLoaders.push(feature); } else { - didAddServiceFactory = - didAddServiceFactory || isServiceFactory(feature); + didAddServiceFactory ||= isServiceFactory(feature); this.#addFeature(feature); } } diff --git a/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts b/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts index 72997e07db..ab5fc2f034 100644 --- a/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts +++ b/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts @@ -16,6 +16,8 @@ const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo'; +// NOTE: This function is also imported and used in backend code + /** * Internal helper that describes the location of the parent caller. * @internal From 6058a95955643828268d0eac55615d6ecf15cd5b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 14:13:23 +0200 Subject: [PATCH 131/204] backend-plugin-api: backwards compat for service ref multiton field Signed-off-by: Patrik Oldsberg --- packages/backend-plugin-api/api-report.md | 2 +- .../src/services/system/types.test.ts | 18 ++++++++++++++++++ .../src/services/system/types.ts | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 99c7b1b9fc..16574ac94e 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -703,7 +703,7 @@ export type ServiceRef< > = { id: string; scope: TScope; - multiton: TInstances extends 'multiton' ? true : false; + multiton?: TInstances extends 'multiton' ? true : false; T: TService; $$type: '@backstage/ServiceRef'; }; diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts index adf097761b..70d165fd8d 100644 --- a/packages/backend-plugin-api/src/services/system/types.test.ts +++ b/packages/backend-plugin-api/src/services/system/types.test.ts @@ -324,6 +324,24 @@ describe('createServiceFactory', () => { metaFactory(); }); + it('should support old service refs without a multiton field', () => { + const oldPluginDep = pluginDep as Omit; // Old refs don't have a multiton field + const metaFactory = createServiceFactory({ + service: ref, + deps: { + plugin: oldPluginDep, + }, + async factory({ plugin }) { + const plugin1: boolean = plugin; + // @ts-expect-error + const plugin2: number = plugin; + unused(plugin1, plugin2); + return 'x'; + }, + }); + expect(metaFactory).toEqual(expect.any(Function)); + }); + it('should only allow objects as options', () => { // @ts-expect-error const metaFactory = createServiceFactory((_opts: string) => ({ diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 7cd18da4d2..a6f33d7941 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -39,7 +39,7 @@ export type ServiceRef< */ scope: TScope; - multiton: TInstances extends 'multiton' ? true : false; + multiton?: TInstances extends 'multiton' ? true : false; /** * Utility for getting the type of the service, using `typeof serviceRef.T`. @@ -189,7 +189,7 @@ type ServiceRefsToInstances< > = { [key in keyof T as T[key]['scope'] extends TScope ? key - : never]: T[key]['multiton'] extends true + : never]: T[key]['multiton'] extends true | undefined ? Array : T[key]['T']; }; From 13d564e44fe94cfb52415435bc36429a5bae7d8a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 14:21:28 +0200 Subject: [PATCH 132/204] backend-defaults: rename and export urlReaderFactoriesServiceRef Signed-off-by: Patrik Oldsberg --- packages/backend-defaults/api-report-urlReader.md | 8 ++++++++ .../src/entrypoints/urlReader/index.ts | 5 ++++- .../urlReader/urlReaderServiceFactory.ts | 15 +++++++-------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md index ad732101dd..f561c2cbf5 100644 --- a/packages/backend-defaults/api-report-urlReader.md +++ b/packages/backend-defaults/api-report-urlReader.md @@ -22,6 +22,7 @@ import { HarnessIntegration } from '@backstage/integration'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Readable } from 'stream'; import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeOptions } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeResponse } from '@backstage/backend-plugin-api'; @@ -413,6 +414,13 @@ export type ReadUrlResponseFactoryFromStreamOptions = { lastModifiedAt?: Date; }; +// @public +export const urlReaderFactoriesServiceRef: ServiceRef< + ReaderFactory, + 'plugin', + 'multiton' +>; + // @public export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/index.ts b/packages/backend-defaults/src/entrypoints/urlReader/index.ts index cf927ea031..05271966c9 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/index.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/index.ts @@ -15,4 +15,7 @@ */ export * from './lib'; -export { urlReaderServiceFactory } from './urlReaderServiceFactory'; +export { + urlReaderServiceFactory, + urlReaderFactoriesServiceRef, +} from './urlReaderServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts index b5c22402ff..2639e99d83 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts @@ -30,7 +30,7 @@ import { * Creating a service factory implementation for a Custom URL Reader. * ```ts * createServiceFactory({ - * service: urlReaderProviderFactoriesServiceRef, + * service: urlReaderFactoriesServiceRef, * deps: {}, * async factory() { * return CustomUrlReader.factory; @@ -38,12 +38,11 @@ import { * }); * ``` */ -export const urlReaderProviderFactoriesServiceRef = - createServiceRef({ - id: 'core.urlReader.factories', - scope: 'plugin', - multiton: true, - }); +export const urlReaderFactoriesServiceRef = createServiceRef({ + id: 'core.urlReader.factories', + scope: 'plugin', + multiton: true, +}); /** * Reading content from external systems. @@ -59,7 +58,7 @@ export const urlReaderServiceFactory = createServiceFactory({ deps: { config: coreServices.rootConfig, logger: coreServices.logger, - factories: urlReaderProviderFactoriesServiceRef, + factories: urlReaderFactoriesServiceRef, }, async factory({ config, logger, factories }) { return UrlReaders.default({ From 60325c471bc7ffc757de0743b40e5fd884b8d655 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 14:38:51 +0200 Subject: [PATCH 133/204] changesets: fix changeset spelling Signed-off-by: Patrik Oldsberg --- .changeset/thirty-adults-grab.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-adults-grab.md b/.changeset/thirty-adults-grab.md index 391e7a39bb..ed986a8289 100644 --- a/.changeset/thirty-adults-grab.md +++ b/.changeset/thirty-adults-grab.md @@ -3,4 +3,4 @@ '@backstage/plugin-catalog-node': patch --- -Explicit declare if the service ref accepts `single` ou `multiple` implementations. +Explicit declare if the service ref accepts `single` or `multiple` implementations. From 120319d2b573cd2e165573573c71fd8c706af1e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Aug 2024 13:15:32 +0000 Subject: [PATCH 134/204] Version Packages (next) --- .changeset/pre.json | 39 + docs/releases/v1.30.0-next.3-changelog.md | 2199 +++++++++++++++++ package.json | 2 +- packages/app-defaults/CHANGELOG.md | 11 + packages/app-defaults/package.json | 2 +- packages/app-next-example-plugin/CHANGELOG.md | 8 + packages/app-next-example-plugin/package.json | 2 +- packages/app-next/CHANGELOG.md | 43 + packages/app-next/package.json | 2 +- packages/app/CHANGELOG.md | 41 + packages/app/package.json | 2 +- packages/backend-app-api/CHANGELOG.md | 21 + packages/backend-app-api/package.json | 2 +- packages/backend-common/CHANGELOG.md | 17 + packages/backend-common/package.json | 2 +- packages/backend-defaults/CHANGELOG.md | 25 + packages/backend-defaults/package.json | 2 +- .../CHANGELOG.md | 26 + .../package.json | 2 +- packages/backend-legacy/CHANGELOG.md | 41 + packages/backend-legacy/package.json | 2 +- packages/backend-openapi-utils/CHANGELOG.md | 8 + packages/backend-openapi-utils/package.json | 2 +- packages/backend-plugin-api/CHANGELOG.md | 94 + packages/backend-plugin-api/package.json | 2 +- packages/backend-tasks/CHANGELOG.md | 12 + packages/backend-tasks/package.json | 2 +- packages/backend-test-utils/CHANGELOG.md | 16 + packages/backend-test-utils/package.json | 2 +- packages/backend/CHANGELOG.md | 37 + packages/backend/package.json | 2 +- packages/cli/CHANGELOG.md | 17 + packages/cli/package.json | 2 +- packages/config-loader/CHANGELOG.md | 11 + packages/config-loader/package.json | 2 +- packages/core-compat-api/CHANGELOG.md | 11 + packages/core-compat-api/package.json | 2 +- packages/create-app/CHANGELOG.md | 8 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 14 + packages/dev-utils/package.json | 2 +- packages/e2e-test/CHANGELOG.md | 9 + packages/e2e-test/package.json | 2 +- packages/frontend-app-api/CHANGELOG.md | 16 + packages/frontend-app-api/package.json | 2 +- packages/frontend-plugin-api/CHANGELOG.md | 17 + packages/frontend-plugin-api/package.json | 2 +- packages/frontend-test-utils/CHANGELOG.md | 13 + packages/frontend-test-utils/package.json | 2 +- packages/repo-tools/CHANGELOG.md | 12 + packages/repo-tools/package.json | 2 +- .../techdocs-cli-embedded-app/CHANGELOG.md | 19 + .../techdocs-cli-embedded-app/package.json | 2 +- packages/techdocs-cli/CHANGELOG.md | 11 + packages/techdocs-cli/package.json | 2 +- packages/test-utils/CHANGELOG.md | 13 + packages/test-utils/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 16 + plugins/api-docs/package.json | 2 +- plugins/app-backend/CHANGELOG.md | 15 + plugins/app-backend/package.json | 2 +- plugins/app-node/CHANGELOG.md | 8 + plugins/app-node/package.json | 2 +- plugins/app-visualizer/CHANGELOG.md | 10 + plugins/app-visualizer/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- plugins/auth-backend/CHANGELOG.md | 31 + plugins/auth-backend/package.json | 2 +- plugins/auth-node/CHANGELOG.md | 18 + plugins/auth-node/package.json | 2 +- plugins/bitbucket-cloud-common/CHANGELOG.md | 8 + plugins/bitbucket-cloud-common/package.json | 2 +- .../catalog-backend-module-aws/CHANGELOG.md | 17 + .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/CHANGELOG.md | 14 + .../catalog-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 17 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../catalog-backend-module-gcp/CHANGELOG.md | 13 + .../catalog-backend-module-gcp/package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 18 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../catalog-backend-module-ldap/CHANGELOG.md | 14 + .../catalog-backend-module-ldap/package.json | 2 +- .../catalog-backend-module-logs/CHANGELOG.md | 9 + .../catalog-backend-module-logs/package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 23 + plugins/catalog-backend/package.json | 2 +- plugins/catalog-common/CHANGELOG.md | 9 + plugins/catalog-common/package.json | 2 +- plugins/catalog-graph/CHANGELOG.md | 14 + plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/CHANGELOG.md | 18 + plugins/catalog-import/package.json | 2 +- plugins/catalog-node/CHANGELOG.md | 15 + plugins/catalog-node/package.json | 2 +- plugins/catalog-react/CHANGELOG.md | 19 + plugins/catalog-react/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- plugins/catalog/CHANGELOG.md | 26 + plugins/catalog/package.json | 2 +- plugins/devtools-backend/CHANGELOG.md | 17 + plugins/devtools-backend/package.json | 2 +- plugins/devtools-common/CHANGELOG.md | 8 + plugins/devtools-common/package.json | 2 +- plugins/devtools/CHANGELOG.md | 13 + plugins/devtools/package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../events-backend-module-azure/CHANGELOG.md | 8 + .../events-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../events-backend-module-gerrit/CHANGELOG.md | 8 + .../events-backend-module-gerrit/package.json | 2 +- .../events-backend-module-github/CHANGELOG.md | 9 + .../events-backend-module-github/package.json | 2 +- .../events-backend-module-gitlab/CHANGELOG.md | 9 + .../events-backend-module-gitlab/package.json | 2 +- .../events-backend-test-utils/CHANGELOG.md | 7 + .../events-backend-test-utils/package.json | 2 +- plugins/events-backend/CHANGELOG.md | 10 + plugins/events-backend/package.json | 2 +- plugins/events-node/CHANGELOG.md | 7 + plugins/events-node/package.json | 2 +- .../example-todo-list-backend/CHANGELOG.md | 10 + .../example-todo-list-backend/package.json | 2 +- plugins/example-todo-list-common/CHANGELOG.md | 7 + plugins/example-todo-list-common/package.json | 2 +- plugins/home/CHANGELOG.md | 17 + plugins/home/package.json | 2 +- plugins/kubernetes-backend/CHANGELOG.md | 23 + plugins/kubernetes-backend/package.json | 2 +- plugins/kubernetes-cluster/CHANGELOG.md | 12 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes-common/CHANGELOG.md | 9 + plugins/kubernetes-common/package.json | 2 +- plugins/kubernetes-node/CHANGELOG.md | 11 + plugins/kubernetes-node/package.json | 2 +- plugins/kubernetes-react/CHANGELOG.md | 12 + plugins/kubernetes-react/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 14 + plugins/kubernetes/package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 18 + plugins/notifications-backend/package.json | 2 +- plugins/notifications-node/CHANGELOG.md | 13 + plugins/notifications-node/package.json | 2 +- plugins/org-react/CHANGELOG.md | 11 + plugins/org-react/package.json | 2 +- plugins/org/CHANGELOG.md | 13 + plugins/org/package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/permission-backend/CHANGELOG.md | 14 + plugins/permission-backend/package.json | 2 +- plugins/permission-common/CHANGELOG.md | 11 + plugins/permission-common/package.json | 2 +- plugins/permission-node/CHANGELOG.md | 17 + plugins/permission-node/package.json | 2 +- plugins/permission-react/CHANGELOG.md | 9 + plugins/permission-react/package.json | 2 +- plugins/proxy-backend/CHANGELOG.md | 11 + plugins/proxy-backend/package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 32 + plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-common/CHANGELOG.md | 9 + plugins/scaffolder-common/package.json | 2 +- .../scaffolder-node-test-utils/CHANGELOG.md | 10 + .../scaffolder-node-test-utils/package.json | 2 +- plugins/scaffolder-node/CHANGELOG.md | 14 + plugins/scaffolder-node/package.json | 2 +- plugins/scaffolder-react/CHANGELOG.md | 18 + plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 25 + plugins/scaffolder/package.json | 2 +- .../CHANGELOG.md | 18 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- plugins/search-backend-module-pg/CHANGELOG.md | 11 + plugins/search-backend-module-pg/package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 19 + .../package.json | 2 +- plugins/search-backend-node/CHANGELOG.md | 15 + plugins/search-backend-node/package.json | 2 +- plugins/search-backend/CHANGELOG.md | 18 + plugins/search-backend/package.json | 2 +- plugins/search-common/CHANGELOG.md | 9 + plugins/search-common/package.json | 2 +- plugins/search-react/CHANGELOG.md | 14 + plugins/search-react/package.json | 2 +- plugins/search/CHANGELOG.md | 17 + plugins/search/package.json | 2 +- plugins/signals-backend/CHANGELOG.md | 14 + plugins/signals-backend/package.json | 2 +- plugins/signals-node/CHANGELOG.md | 12 + plugins/signals-node/package.json | 2 +- .../techdocs-addons-test-utils/CHANGELOG.md | 15 + .../techdocs-addons-test-utils/package.json | 2 +- plugins/techdocs-backend/CHANGELOG.md | 19 + plugins/techdocs-backend/package.json | 2 +- plugins/techdocs-node/CHANGELOG.md | 15 + plugins/techdocs-node/package.json | 2 +- plugins/techdocs/CHANGELOG.md | 24 + plugins/techdocs/package.json | 2 +- plugins/user-settings-backend/CHANGELOG.md | 14 + plugins/user-settings-backend/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 17 + plugins/user-settings/package.json | 2 +- 305 files changed, 4584 insertions(+), 152 deletions(-) create mode 100644 docs/releases/v1.30.0-next.3-changelog.md diff --git a/.changeset/pre.json b/.changeset/pre.json index 8b33f2b80b..4699170212 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -190,41 +190,62 @@ "@backstage/plugin-techdocs-common": "0.0.0" }, "changesets": [ + "angry-dolphins-camp", "big-eagles-grab", "blue-pumas-cheer", "breezy-jeans-tie", + "breezy-rings-fly", + "bright-donkeys-buy", "bright-trainers-brake", + "calm-crabs-drop", + "chilly-days-peel", "chilly-trains-sleep", + "clever-pans-brake", "cool-insects-remember", "cool-schools-vanish", "create-app-1722413762", "cuddly-zebras-crash", + "curvy-pillows-joke", + "cyan-shrimps-push", "dry-squids-tap", "dull-ghosts-double", "early-trees-dance", "eighty-emus-leave", + "eighty-jokes-deny", "eighty-mirrors-flow", + "fair-hairs-mix", + "fair-pumas-hang", + "fast-bulldogs-relax", "few-wasps-hug", "flat-plums-grow", "forty-ties-agree", "forty-ties-disagree", + "friendly-cherries-applaud", + "friendly-chicken-cry", "friendly-feet-refuse", "gentle-dryers-smile", "good-steaks-report", "green-planets-reflect", "grumpy-owls-suffer", + "healthy-timers-divide", "heavy-numbers-love", + "hip-fishes-guess", "hip-hairs-exist", + "khaki-lamps-peel", "late-games-protect", "light-pianos-exercise", "little-bulldogs-guess", "little-suns-fly", "mean-apricots-perform", + "metal-planes-nail", + "metal-rice-call", + "mighty-apricots-taste", "mighty-dolls-retire", "mighty-geckos-kiss", "modern-parrots-protect", "neat-bears-divide", "neat-gifts-join", + "neat-socks-cheer", "new-scissors-try", "nice-peas-retire", "nine-cherries-decide", @@ -236,16 +257,26 @@ "orange-gifts-protect", "pink-gorillas-brake", "plenty-tools-exist", + "purple-carrots-crash", "quick-roses-juggle", "rare-foxes-compete", "red-radios-promise", + "renovate-147ac48", "renovate-f04beb1", "rich-mugs-dress", "selfish-bees-think", "seven-eggs-admire", "shaggy-dodos-applaud", + "shy-games-poke", + "shy-waves-share", + "silly-candles-sin", + "silly-cycles-tan", "six-rats-kick", + "slow-ducks-rush", + "slow-ligers-drum", "slow-toes-jog", + "small-bottles-cough", + "small-ears-poke", "small-spoons-shout", "smooth-countries-relate", "soft-gorillas-refuse", @@ -258,15 +289,23 @@ "sweet-oranges-buy", "swift-kings-sparkle", "tall-snakes-fix", + "tasty-ads-rescue", "thick-hotels-know", + "thirty-adults-grab", "thirty-paws-hope", "tiny-oranges-pretend", + "tough-goats-hang", "tough-lies-repair", + "tricky-ducks-juggle", "two-emus-work", "violet-jokes-wave", + "warm-monkeys-marry", "wicked-bobcats-teach", "wild-eggs-exist", + "wise-spiders-walk", "witty-bears-behave", + "witty-geese-battle", + "witty-timers-marry", "young-games-visit", "young-peaches-shake" ] diff --git a/docs/releases/v1.30.0-next.3-changelog.md b/docs/releases/v1.30.0-next.3-changelog.md new file mode 100644 index 0000000000..4fa86da821 --- /dev/null +++ b/docs/releases/v1.30.0-next.3-changelog.md @@ -0,0 +1,2199 @@ +# Release v1.30.0-next.3 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.30.0-next.3) + +## @backstage/backend-plugin-api@0.8.0-next.2 + +### Minor Changes + +- 7c5f3b0: The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + + We're looking for ways to make it possible to augment services without the need to replace the entire service. + + Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + + ```diff + // @backstage/backend-defaults + + + export const urlReaderFactoriesServiceRef = createServiceRef({ + + id: 'core.urlReader.factories', + + scope: 'plugin', + + multiton: true, + + }); + + ... + + export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + + factories: urlReaderFactoriesServiceRef, + }, + - async factory({ config, logger }) { + + async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, + + factories, + }); + }, + }); + ``` + + With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + + ```ts + // packages/backend/index.ts + import { createServiceFactory } from '@backstage/backend-plugin-api'; + import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; + ... + + backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, + })); + + ... + + ``` + +### Patch Changes + +- 6061061: Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. + +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. + +- 8b13183: Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + + A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + + Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + + ```ts + const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, + }); + ``` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/frontend-plugin-api@0.7.0-next.2 + +### Minor Changes + +- 72754db: **BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + + This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. + +### Patch Changes + +- 210d066: Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-auth-node@0.5.0-next.2 + +### Minor Changes + +- 579afd0: **BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog@1.22.0-next.2 + +### Minor Changes + +- 6925dcb: Introduces the HasSubdomainsCard component that displays the subdomains of a given domain + +### Patch Changes + +- 604a504: The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder@1.24.0-next.2 + +### Minor Changes + +- 3fca643: Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/app-defaults@1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/backend-app-api@0.8.1-next.2 + +### Patch Changes + +- 8b13183: Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 7c5f3b0: Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. +- 80a0737: Added configuration for the `packages` options to config schema +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-common@0.23.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.4.2-next.2 + +### Patch Changes + +- 0d16b52: Add access restrictions to the JWKS external access method config schema +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 7c5f3b0: Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. +- 1d5f298: Avoid excessive numbers of error listeners on cache clients +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-dynamic-feature-service@0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-openapi-utils@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/backend-tasks@0.5.28-next.2 + +### Patch Changes + +- ba9abf4: The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.5-next.2 + +### Patch Changes + +- 8b13183: Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. +- 7c5f3b0: Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/cli@0.27.0-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/config-loader@1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.8-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- 16cf96c: Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.18-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.5-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.12-next.2 + +### Patch Changes + +- 8209449: Added new APIs for testing extensions +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/repo-tools@0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## @backstage/plugin-api-docs@0.11.8-next.2 + +### Patch Changes + +- 4b6d2cb: Updated dependency `@graphiql/react` to `^0.23.0`. +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-app-backend@0.3.72-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + +## @backstage/plugin-app-visualizer@0.1.9-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + +### Patch Changes + +- 3fca643: Added method `listBranchesByRepository` to `BitbucketCloudClient` +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + +## @backstage/plugin-catalog-backend@1.24.1-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-aws@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-azure@0.1.43-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.37-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.40-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-logs@0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-common@1.0.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-graph@0.4.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + +## @backstage/plugin-catalog-node@1.12.5-next.2 + +### Patch Changes + +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-react@1.12.3-next.2 + +### Patch Changes + +- 012e3eb: Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-devtools@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-devtools-backend@0.3.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools-common@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend@0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-gerrit@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-github@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-test-utils@0.1.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-node@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + +## @backstage/plugin-home@0.7.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## @backstage/plugin-kubernetes@0.11.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## @backstage/plugin-kubernetes-backend@0.18.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 8c1aa06: Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. + This was already documented and supported by the plugin. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## @backstage/plugin-kubernetes-common@0.8.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-node@0.1.17-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-react@0.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications-backend@0.3.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-notifications-backend-module-email@0.2.0-next.2 + +### Patch Changes + +- cdb630d: Add support for stream transport for debugging purposes +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-notifications-node@0.2.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-org@0.6.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-org-react@0.1.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-permission-backend@0.5.47-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + +## @backstage/plugin-permission-common@0.8.1-next.1 + +### Patch Changes + +- df784fe: Add the MetadataResponse type from @backstage/plugin-permission-node, since this + type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-node@0.8.1-next.2 + +### Patch Changes + +- df784fe: The MetadataResponse type has been moved to @backstage/plugin-permission-common + to match the recent move of MetadataResponseSerializedRule, and should be + imported from there going forward. To avoid an immediate breaking change, this + type is still re-exported from this package, but is marked as deprecated and + will be removed in a future release. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-permission-react@0.4.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-proxy-backend@0.5.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.23.1-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 3fca643: Added autocompletion support for resource `branches` +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 382e868: Added test cases for sentry:project:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-common@1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.9-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-react@1.11.0-next.2 + +### Patch Changes + +- 072c00c: Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes +- 04759f2: Fix null check in `isJsonObject` utility function for scaffolder review state component +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search@1.4.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search-backend@1.5.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.16-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-node@1.2.28-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-common@1.2.14-next.1 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-react@1.7.14-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-signals-backend@0.1.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-techdocs@1.10.8-next.2 + +### Patch Changes + +- 67e76f2: TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. +- bdc5471: Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-backend@1.10.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-techdocs-node@1.12.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-user-settings@0.8.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.100-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-devtools@0.1.17-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## example-app-next@0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-app-visualizer@0.1.9-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## app-next-example-plugin@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + +## example-backend@0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-notifications-backend@0.3.4-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/catalog-model@1.5.0 + +## example-backend-legacy@0.2.101-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## e2e-test@0.2.19-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.99-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @internal/plugin-todo-list-backend@1.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @internal/plugin-todo-list-common@1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 diff --git a/package.json b/package.json index 8e7a047d7b..d9b384fb7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.30.0-next.2", + "version": "1.30.0-next.3", "private": true, "repository": { "type": "git", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index ef2ecbce78..410841fec9 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 1.5.10-next.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 6bed796754..284e832ae5 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.5.10-next.1", + "version": "1.5.10-next.2", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 13cd327ef1..62a7b18df0 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # app-next-example-plugin +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + ## 0.0.14-next.1 ### Patch Changes diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index 64be34d516..ad179ebbe5 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.14-next.1", + "version": "0.0.14-next.2", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 7eb9535771..6382efa811 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,48 @@ # example-app-next +## 0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-app-visualizer@0.1.9-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 0.0.14-next.2 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 8a9d97d560..a12371a135 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.14-next.2", + "version": "0.0.14-next.3", "private": true, "repository": { "type": "git", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 4bcd5461a7..88e1134800 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,46 @@ # example-app +## 0.2.100-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-devtools@0.1.17-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 0.2.100-next.2 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 855d32fce7..c4c0c18391 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.100-next.2", + "version": "0.2.100-next.3", "backstage": { "role": "frontend" }, diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index cbbb9e949e..c7c8883302 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/backend-app-api +## 0.8.1-next.2 + +### Patch Changes + +- 8b13183: Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 7c5f3b0: Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. +- 80a0737: Added configuration for the `packages` options to config schema +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.8.1-next.1 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 6a4e3a669b..e809d1c2f0 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "0.8.1-next.1", + "version": "0.8.1-next.2", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 2bd24bc8d5..5eadae4c0f 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/backend-common +## 0.23.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.23.4-next.1 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 17913d87d4..ccf44fba0f 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-common", - "version": "0.23.4-next.1", + "version": "0.23.4-next.2", "description": "Common functionality library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 387c9d197e..44b0a6b9b0 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/backend-defaults +## 0.4.2-next.2 + +### Patch Changes + +- 0d16b52: Add access restrictions to the JWKS external access method config schema +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 7c5f3b0: Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. +- 1d5f298: Avoid excessive numbers of error listeners on cache clients +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.2-next.1 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 196c9b188a..cef9641de6 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.4.2-next.1", + "version": "0.4.2-next.2", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index ede70bfbca..14111873a1 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/backend-dynamic-feature-service +## 0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.16-next.1 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 1282d39d6c..d6ac21a3ae 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-dynamic-feature-service", "description": "Backstage dynamic feature service", - "version": "0.2.16-next.1", + "version": "0.2.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-legacy/CHANGELOG.md b/packages/backend-legacy/CHANGELOG.md index f036362cd2..a601890055 100644 --- a/packages/backend-legacy/CHANGELOG.md +++ b/packages/backend-legacy/CHANGELOG.md @@ -1,5 +1,46 @@ # example-backend-legacy +## 0.2.101-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.2.101-next.1 ### Patch Changes diff --git a/packages/backend-legacy/package.json b/packages/backend-legacy/package.json index 3f59103f4c..a650924720 100644 --- a/packages/backend-legacy/package.json +++ b/packages/backend-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-legacy", - "version": "0.2.101-next.1", + "version": "0.2.101-next.2", "backstage": { "role": "backend" }, diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 37bb91f625..682036d984 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-openapi-utils +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/errors@1.2.4 + ## 0.1.16-next.1 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index ccf49f2be7..35db691c25 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-openapi-utils", "description": "OpenAPI typescript support.", - "version": "0.1.16-next.1", + "version": "0.1.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index 9f5ced6b18..d404d05b33 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,99 @@ # @backstage/backend-plugin-api +## 0.8.0-next.2 + +### Minor Changes + +- 7c5f3b0: The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + + We're looking for ways to make it possible to augment services without the need to replace the entire service. + + Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + + ```diff + // @backstage/backend-defaults + + + export const urlReaderFactoriesServiceRef = createServiceRef({ + + id: 'core.urlReader.factories', + + scope: 'plugin', + + multiton: true, + + }); + + ... + + export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + + factories: urlReaderFactoriesServiceRef, + }, + - async factory({ config, logger }) { + + async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, + + factories, + }); + }, + }); + ``` + + With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + + ```ts + // packages/backend/index.ts + import { createServiceFactory } from '@backstage/backend-plugin-api'; + import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; + ... + + backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, + })); + + ... + + ``` + +### Patch Changes + +- 6061061: Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 8b13183: Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + + A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + + Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + + ```ts + const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, + }); + ``` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.1-next.1 ### Patch Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 267fdf6a01..61f8b9353d 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "0.7.1-next.1", + "version": "0.8.0-next.2", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 2313e04748..1a5879f14e 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/backend-tasks +## 0.5.28-next.2 + +### Patch Changes + +- ba9abf4: The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.5.28-next.1 ### Patch Changes diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index a6c88c2be4..3f482b38e6 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.5.28-next.1", + "version": "0.5.28-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 288bc2fd1d..cbca6bdcab 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/backend-test-utils +## 0.4.5-next.2 + +### Patch Changes + +- 8b13183: Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. +- 7c5f3b0: Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.5-next.1 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index bb0786b02c..44373e733f 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "0.4.5-next.1", + "version": "0.4.5-next.2", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 0c58a76dd1..8fbce11752 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,42 @@ # example-backend +## 0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-notifications-backend@0.3.4-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/catalog-model@1.5.0 + ## 0.0.29-next.1 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 91c63d7553..cafe89a527 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.29-next.1", + "version": "0.0.29-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index df526b2b00..887445aac7 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/cli +## 0.27.0-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + ## 0.27.0-next.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 79740ea4fa..ba45b8f514 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.27.0-next.2", + "version": "0.27.0-next.3", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index a026e7549d..78f2123970 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/config-loader +## 1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.9.0-next.1 ### Minor Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 1b49af9cf0..a47a17c12f 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.9.0-next.1", + "version": "1.9.0-next.2", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 587d78242b..34c134f731 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-compat-api +## 0.2.8-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- 16cf96c: Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + ## 0.2.8-next.1 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 4a143a50c7..0434b206d9 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.2.8-next.1", + "version": "0.2.8-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 85587de268..dce729b811 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/create-app +## 0.5.18-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + ## 0.5.18-next.2 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index df46c8ffde..8886ed9a6d 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.5.18-next.2", + "version": "0.5.18-next.3", "publishConfig": { "access": "public" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 30aeba5a08..5ea08c7762 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/dev-utils +## 1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + ## 1.0.37-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index f3f835b015..b2b6eb3292 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.0.37-next.1", + "version": "1.0.37-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 488f48fd4b..4609771fb5 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,14 @@ # e2e-test +## 0.2.19-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + ## 0.2.19-next.2 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 190d66c7b3..481b011d01 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.19-next.2", + "version": "0.2.19-next.3", "private": true, "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 8593bb38e5..dd419c284b 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/frontend-app-api +## 0.7.5-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.7.5-next.1 ### Patch Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 11059214c2..0d462e2721 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.7.5-next.1", + "version": "0.7.5-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 40961b558f..302d541b18 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/frontend-plugin-api +## 0.7.0-next.2 + +### Minor Changes + +- 72754db: **BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + + This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. + +### Patch Changes + +- 210d066: Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.6.8-next.1 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 5ee4667c3a..8f729a5c4d 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.6.8-next.1", + "version": "0.7.0-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 8f770b7b31..4ba38f5065 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/frontend-test-utils +## 0.1.12-next.2 + +### Patch Changes + +- 8209449: Added new APIs for testing extensions +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.12-next.1 ### Patch Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 737d30363a..e8152cfdda 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.1.12-next.1", + "version": "0.1.12-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 35c7f35836..f74dd57ea0 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/repo-tools +## 0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + ## 0.9.5-next.1 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 7e03d78758..63be4d526e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.9.5-next.1", + "version": "0.9.5-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 728145048a..57fb204390 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,24 @@ # techdocs-cli-embedded-app +## 0.2.99-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 0.2.99-next.2 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 09635b74ed..472936193a 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.99-next.2", + "version": "0.2.99-next.3", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index d6d5128e4d..76f9891bf0 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,16 @@ # @techdocs/cli +## 1.8.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + ## 1.8.17-next.1 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index fc28ab1458..c655649ceb 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.8.17-next.1", + "version": "1.8.17-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 6684b8b4b5..a6c013a34a 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/test-utils +## 1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + ## 1.5.10-next.1 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 32a69da9df..3007c734bd 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.5.10-next.1", + "version": "1.5.10-next.2", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index f649d4e0b8..c87bfcaafa 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-api-docs +## 0.11.8-next.2 + +### Patch Changes + +- 4b6d2cb: Updated dependency `@graphiql/react` to `^0.23.0`. +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.11.8-next.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 3633471588..96b52002d8 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.11.8-next.1", + "version": "0.11.8-next.2", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index c0a59fca0e..305d90e2ec 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-app-backend +## 0.3.72-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.72-next.1 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 2bd66c7ca0..d6ac29953e 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.72-next.1", + "version": "0.3.72-next.2", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index efd406fc95..25861bc8a1 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-node +## 0.1.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + ## 0.1.23-next.1 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 3d02ac0388..e5e4d19a47 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.23-next.1", + "version": "0.1.23-next.2", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index 0a676a49cf..a7f8d8a012 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-app-visualizer +## 0.1.9-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index 784848a573..0d825be137 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index 74b7d191c4..e2014d6749 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index c6416f1cfc..995b980124 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The atlassian-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index bf64404405..ce99594759 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index cb1649a23e..2ddace0546 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index cfc576d6e1..471f86842e 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 521318cb37..f48e7ce40b 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-azure-easyauth-provider", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "description": "The azure-easyauth-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md index 87da903953..e70bd7f8ee 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 912f0c99a7..ae9692bfe8 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "description": "The bitbucket-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md index f1423273d7..493b57d26a 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.1.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index 2cd60873d3..c388ead3cb 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 6d000cede5..3b4f24a64a 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 098416f347..20b3f88606 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", - "version": "0.2.18-next.1", + "version": "0.2.18-next.2", "description": "A GCP IAP auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index cb68d5ab5c..83db20aaa5 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 13504d3a2f..e773518782 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "The github-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index 4bf32bad63..8f502140b7 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index c77669aaac..88fd39a53b 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "The gitlab-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index 08739b52cf..64d59dc5aa 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 209a422980..ed2e536396 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "A Google auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md index 94904ce214..4c5df385c7 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index 9ddc9f5877..d41612d32a 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "description": "The guest-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index 4e640b0b73..9bbf02ba3c 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.1.18-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.18-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index e80adbac1a..f18104ba15 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", - "version": "0.1.18-next.1", + "version": "0.1.18-next.2", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 066695c188..63c6662332 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index b475437611..77a00a52d6 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The oauth2-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index acad1b96a8..a9d6523201 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index ff6d31ebee..4df4e14dfe 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", - "version": "0.1.16-next.1", + "version": "0.1.16-next.2", "description": "The oauth2-proxy-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index b0686455b5..6c32155ebe 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 90fd9c4b63..5f59a4e0d1 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The oidc-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index 12266f8aba..38fe3bed5d 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.0.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.0.16-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index a2460cc5f3..ad0c5c5ecb 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", - "version": "0.0.16-next.1", + "version": "0.0.16-next.2", "description": "The okta-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md index b2ab669eca..476df484d2 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index c299519e45..1d1feee009 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-onelogin-provider", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "description": "The onelogin-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index 3dcce4d28f..8b23915d26 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index a33c321292..c720d9b501 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", - "version": "0.1.17-next.1", + "version": "0.1.17-next.2", "description": "The pinniped-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index cc50745204..9f81a53961 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 8fcf5fb2a4..166b7e8e82 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The vmware-cloud-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index d17fdabb26..f99c8d0453 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-auth-backend +## 0.22.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.22.10-next.1 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 8a3d2966a8..8dec959418 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.22.10-next.1", + "version": "0.22.10-next.2", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index e8b2a4f4b9..1905329e0a 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-auth-node +## 0.5.0-next.2 + +### Minor Changes + +- 579afd0: **BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.18-next.1 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index bb0057f42a..348db40dd7 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.4.18-next.1", + "version": "0.5.0-next.2", "backstage": { "role": "node-library", "pluginId": "auth", diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index c4ce7148f4..30e2e77a6e 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.2.22-next.1 + +### Patch Changes + +- 3fca643: Added method `listBranchesByRepository` to `BitbucketCloudClient` +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + ## 0.2.22-next.0 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index 06fd4e6f3a..d2f22cc966 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", - "version": "0.2.22-next.0", + "version": "0.2.22-next.1", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 705712aac6..69c16e9875 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 96c57eb821..443dfebb87 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.3.18-next.1", + "version": "0.3.18-next.2", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index d7b4c90862..89d6847511 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.43-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + ## 0.1.43-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 39f889cb54..de18ae5895 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.1.43-next.1", + "version": "0.1.43-next.2", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 17b20064ba..771702d86d 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index d854bbd8e5..d9ba70c806 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.2.6-next.1", + "version": "0.2.6-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 58c54afce9..ec27de045c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.2.10-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 03ae26f4c2..3f4389788c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.2.10-next.1", + "version": "0.2.10-next.2", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 762d73c1b6..46469b747a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.37-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.37-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 7d6f1a38b3..eeafae91d5 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.1.37-next.1", + "version": "0.1.37-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 0a5ed326fd..f5f805edd9 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.1.24-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index 9abc3da395..0673dd1805 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.1.24-next.1", + "version": "0.1.24-next.2", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index 78f6172a3d..d4b173cc3f 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.40-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.40-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 6358590dae..b10e48d34b 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.1.40-next.1", + "version": "0.1.40-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index f0f37b1c03..ebaa50af30 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.1.18-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 562b20401a..e1f60aec67 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.1.18-next.1", + "version": "0.1.18-next.2", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index e42812d75d..c7a032c43e 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-github +## 0.6.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.6.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index c6af27c2ce..dfde8e9718 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.6.6-next.1", + "version": "0.6.6-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index 33ce83336c..5611260f87 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.0.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 79c43d6529..68e1c8f252 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.0.6-next.1", + "version": "0.0.6-next.2", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 2b8cffa0d6..737f79de59 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.3.22-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.3.22-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 2b44a0b75e..d15deafd08 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.3.22-next.1", + "version": "0.3.22-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index d43ff7902d..e56e14b95f 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.28-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 8d3d25188d..f3744d9b07 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.4.28-next.1", + "version": "0.4.28-next.2", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index a93f095557..8b8907bd7b 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index efda8a6b40..fa627cb55f 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.7.1-next.1", + "version": "0.7.1-next.2", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-logs/CHANGELOG.md b/plugins/catalog-backend-module-logs/CHANGELOG.md index a62e7d7984..4637f19052 100644 --- a/plugins/catalog-backend-module-logs/CHANGELOG.md +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-logs +## 0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.0.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json index 2cf01d9e2c..a6609c7227 100644 --- a/plugins/catalog-backend-module-logs/package.json +++ b/plugins/catalog-backend-module-logs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-logs", - "version": "0.0.2-next.1", + "version": "0.0.2-next.2", "description": "A module that subscribes to catalog releated events and logs them.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 86bf7e2e45..2efe171ec1 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.5.31-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index a889d6a22a..daba6c4b9f 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.5.31-next.1", + "version": "0.5.31-next.2", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 8daf794314..eb0dbcf08d 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.41-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index c6147804f9..aa4ec5a803 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.1.41-next.1", + "version": "0.1.41-next.2", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 924e0a9df0..209160b4e1 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.1.29-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index fd9384b567..977379fefa 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.1.29-next.1", + "version": "0.1.29-next.2", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 262105e62a..0813e4b6ab 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-model@1.5.0 + ## 0.1.21-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 0b3b1a9534..02c8a09ae9 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.1.21-next.1", + "version": "0.1.21-next.2", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index 435637c3d8..37413e5f02 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.4.10-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index a1078106eb..eaf86d75ed 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.4.10-next.1", + "version": "0.4.10-next.2", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 826f0c5897..fe707b2a9a 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-catalog-backend +## 1.24.1-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.24.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index cb3cc31cca..0a046b43e4 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "1.24.1-next.1", + "version": "1.24.1-next.2", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 615bcc538e..2d0fde55da 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-common +## 1.0.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/catalog-model@1.5.0 + ## 1.0.26-next.0 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 167c1b0a91..cfcfee74d8 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-common", - "version": "1.0.26-next.0", + "version": "1.0.26-next.1", "description": "Common functionalities for the catalog plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 5648055a3e..695af8d4fe 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-graph +## 0.4.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + ## 0.4.8-next.2 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 04fe835f5c..08a5497dc0 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.8-next.2", + "version": "0.4.8-next.3", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 43e132823a..2063f9e2f9 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-import +## 0.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + ## 0.12.2-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 397d3c92d9..44076a3fbc 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.12.2-next.1", + "version": "0.12.2-next.2", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index f276e0a93e..24f1446c6d 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-node +## 1.12.5-next.2 + +### Patch Changes + +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.12.5-next.1 ### Patch Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 3e755330e3..330427c7b5 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.12.5-next.1", + "version": "1.12.5-next.2", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 38c15e8957..24a1cf6572 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-react +## 1.12.3-next.2 + +### Patch Changes + +- 012e3eb: Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.12.3-next.1 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index fdbd942529..7300729db5 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.12.3-next.1", + "version": "1.12.3-next.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md index e3dc87146f..b299631a21 100644 --- a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-unprocessed-entities-common +## 0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + ## 0.0.4-next.0 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities-common/package.json b/plugins/catalog-unprocessed-entities-common/package.json index 8bded795b4..e5f88ebba4 100644 --- a/plugins/catalog-unprocessed-entities-common/package.json +++ b/plugins/catalog-unprocessed-entities-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities-common", - "version": "0.0.4-next.0", + "version": "0.0.4-next.1", "description": "Common functionalities for the catalog-unprocessed-entities plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 0a229ff17a..48daaa7774 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog +## 1.22.0-next.2 + +### Minor Changes + +- 6925dcb: Introduces the HasSubdomainsCard component that displays the subdomains of a given domain + +### Patch Changes + +- 604a504: The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + ## 1.21.2-next.1 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e747170032..0208271d1a 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.21.2-next.1", + "version": "1.22.0-next.2", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 323385dc82..db4d04fc6e 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-devtools-backend +## 0.3.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.9-next.1 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index 630ea213b8..64cfc4f883 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.3.9-next.1", + "version": "0.3.9-next.2", "backstage": { "role": "backend-plugin", "pluginId": "devtools", diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md index 09e8a85edb..80d0584c77 100644 --- a/plugins/devtools-common/CHANGELOG.md +++ b/plugins/devtools-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-devtools-common +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + ## 0.1.12-next.0 ### Patch Changes diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json index be1fab2967..d8a87753ca 100644 --- a/plugins/devtools-common/package.json +++ b/plugins/devtools-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-common", - "version": "0.1.12-next.0", + "version": "0.1.12-next.1", "description": "Common functionalities for the devtools plugin", "backstage": { "role": "common-library", diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index 496ff5cf52..afebfeefe2 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-devtools +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index a6956161c6..264f7af95b 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.17-next.1", + "version": "0.1.17-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 53a4e8a3fa..a4a4da2b17 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.3.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 0a4ef481ff..7b1da53130 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.3.9-next.1", + "version": "0.3.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 0c13627d71..804293b869 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index c24b8743ac..5d7fc3f1bf 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 0c333e1d8f..edef56d946 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 87fb26f2e5..a58bacdf8c 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 2cd77d9933..fcdda38e9b 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 43fd0446eb..91e598511f 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 068b54c0b0..38b1e277e8 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-github +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 57516356ea..f77c2c9df8 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index b717e5886d..a7986891fa 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index 34643977cb..c5a0abfeb8 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 33ad7e0318..f408883694 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.1.33-next.1 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index a20f4fa7c7..0cff95bf35 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.33-next.1", + "version": "0.1.33-next.2", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 1bbf2359e4..8c2e2f767b 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-events-backend +## 0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.3.10-next.1 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index e563551d5b..01310be5d8 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.3.10-next.1", + "version": "0.3.10-next.2", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index a01292b23e..ec7c864d23 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-node +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + ## 0.3.9-next.1 ### Patch Changes diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 7943aadc8f..db2a6f4271 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.3.9-next.1", + "version": "0.3.9-next.2", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index cff91b2e5c..370053d097 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @internal/plugin-todo-list-backend +## 1.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + ## 1.0.30-next.1 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 2e84c6c2b7..b9b04c178f 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.30-next.1", + "version": "1.0.30-next.2", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index f780b14906..b2f2175573 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-common +## 1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + ## 1.0.21-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 6071032963..cca7f5aa4b 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-common", - "version": "1.0.21-next.0", + "version": "1.0.21-next.1", "backstage": { "role": "common-library", "pluginId": "todo-list", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 2cd0a184fd..08cf9bec5f 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-home +## 0.7.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + ## 0.7.9-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 792f7a1525..e566b46e78 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.7.9-next.1", + "version": "0.7.9-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index c286d49164..1255d6d0fd 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-kubernetes-backend +## 0.18.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 8c1aa06: Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. + This was already documented and supported by the plugin. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.18.4-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 4b69c912b8..70f640e0ba 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.18.4-next.1", + "version": "0.18.4-next.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index cab3826e64..801c2018c9 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + ## 0.0.14-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index f2b1feb906..75c0758ac5 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.14-next.1", + "version": "0.0.14-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 9ff6547ad4..f8ac4b4768 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-common +## 0.8.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.8.2-next.0 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 7ac04abea5..1e05ed873e 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-common", - "version": "0.8.2-next.0", + "version": "0.8.2-next.1", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index 6e738ab3aa..42e5885c49 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kubernetes-node +## 0.1.17-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index f238334824..5377f26b4d 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.1.17-next.1", + "version": "0.1.17-next.2", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 8859563287..34e9b259be 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-react +## 0.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.2-next.1 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 094fdaa5e2..dbf89ae0cf 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.4.2-next.1", + "version": "0.4.2-next.2", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 384f6574bc..1b5ca07506 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-kubernetes +## 0.11.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + ## 0.11.13-next.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index d711c5bdb5..ab8fe84d9b 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.11.13-next.1", + "version": "0.11.13-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 1a9e5ad4bc..a0b4bc8caa 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-notifications-backend-module-email +## 0.2.0-next.2 + +### Patch Changes + +- cdb630d: Add support for stream transport for debugging purposes +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.2.0-next.1 ### Minor Changes diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 425186fcad..33451341ea 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.2.0-next.1", + "version": "0.2.0-next.2", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 9afeff1a51..61cba9e3a6 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-notifications-backend +## 0.3.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.3.4-next.1 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 8bb3fd2acf..9bc26f7a1f 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.3.4-next.1", + "version": "0.3.4-next.2", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index 6907885ce7..627cb53ea0 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-notifications-node +## 0.2.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index d71fb7af82..533ebe7ce2 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index e5994b4ec4..4bbf343849 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-org-react +## 0.1.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.27-next.1 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 3dca7e029b..d91d73c4ca 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.27-next.1", + "version": "0.1.27-next.2", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 69d6b3fc0f..923d5d1d23 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-org +## 0.6.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.6.28-next.1 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 72ea35013d..44ae3d10f6 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.28-next.1", + "version": "0.6.28-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 559697105d..7c0765ad43 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json index fb40f025ce..0ba16a61f7 100644 --- a/plugins/permission-backend-module-policy-allow-all/package.json +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend-module-allow-all-policy", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "Allow all policy backend module for the permission plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 9f0f3cf8ac..89955c0f69 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-permission-backend +## 0.5.47-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.5.47-next.1 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index a5b6a9fb59..7a331c1ccd 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.47-next.1", + "version": "0.5.47-next.2", "backstage": { "role": "backend-plugin", "pluginId": "permission", diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index 9194c9cae6..8ef1eb314f 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-permission-common +## 0.8.1-next.1 + +### Patch Changes + +- df784fe: Add the MetadataResponse type from @backstage/plugin-permission-node, since this + type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.8.1-next.0 ### Patch Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 323dea5638..faeaf4a972 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-common", - "version": "0.8.1-next.0", + "version": "0.8.1-next.1", "description": "Isomorphic types and client for Backstage permissions and authorization", "backstage": { "role": "common-library", diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 3e29fe0c2a..282ea5fda8 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-permission-node +## 0.8.1-next.2 + +### Patch Changes + +- df784fe: The MetadataResponse type has been moved to @backstage/plugin-permission-common + to match the recent move of MetadataResponseSerializedRule, and should be + imported from there going forward. To avoid an immediate breaking change, this + type is still re-exported from this package, but is marked as deprecated and + will be removed in a future release. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.8.1-next.1 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 4e024a3f10..b2c2d06f11 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-node", - "version": "0.8.1-next.1", + "version": "0.8.1-next.2", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 8b2e8e3250..83f4dea7ed 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-react +## 0.4.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.4.25-next.0 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 8bd566a34c..394f56ff02 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.25-next.0", + "version": "0.4.25-next.1", "backstage": { "role": "web-library", "pluginId": "permission", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index bfec70e942..138b98902b 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-proxy-backend +## 0.5.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.5.4-next.1 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 655ccfed9d..091e637886 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.5.4-next.1", + "version": "0.5.4-next.2", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index fe925ffb7d..90541db310 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 19470655e6..92d64ec7f0 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "description": "The azure module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 0d52996a88..604b44d3da 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 3fca643: Added autocompletion support for resource `branches` +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 0446c39b0c..1806189dd4 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 7d4b793d10..221a626169 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 0ce8ac90e8..5c08e23b6f 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index 8ab0a80df0..062d54226d 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.2.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index e29e75ee12..a9f499ec74 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", - "version": "0.2.13-next.1", + "version": "0.2.13-next.2", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 9c9b493c1d..2621b1d8f3 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.24-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.24-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index a0d5631608..b4b10f7d4c 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", - "version": "0.2.24-next.1", + "version": "0.2.24-next.2", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 71ce038f43..160953da23 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.47-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index ec1899099b..7749830304 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.2.47-next.1", + "version": "0.2.47-next.2", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md index 551141df6c..c759549d90 100644 --- a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-gcp +## 0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gcp/package.json b/plugins/scaffolder-backend-module-gcp/package.json index 1c2feb106a..33459ba5b6 100644 --- a/plugins/scaffolder-backend-module-gcp/package.json +++ b/plugins/scaffolder-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gcp", - "version": "0.1.1-next.1", + "version": "0.1.1-next.2", "description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index 6827109801..811e9a80cc 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index f39e67cedc..0e50124013 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index 983d3cebb0..0a05340f77 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 14b0305b10..cb888b65ad 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index d39143c7c4..e9d841aa0c 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 68a0475622..c373839d13 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.4.1-next.1", + "version": "0.4.1-next.2", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index c0099a16c6..f62e5b4db8 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.5-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 410a6111f5..dde6e48a6e 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.4.5-next.1", + "version": "0.4.5-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md index 31f8197923..85da251d02 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.0.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index c4a10f6a92..204bf597e9 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-notifications", - "version": "0.0.6-next.1", + "version": "0.0.6-next.2", "description": "The notifications backend module for the scaffolder plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index ff68b1d78c..ed34f47a58 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.40-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.40-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 38088ccfc8..8cbd5e1067 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.4.40-next.1", + "version": "0.4.40-next.2", "description": "A module for the scaffolder backend that lets you template projects using Rails", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 7515c6494b..c4421d314b 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 382e868: Added test cases for sentry:project:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.31-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 81171ea78d..f683e23d09 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.31-next.1", + "version": "0.1.31-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 069390e33f..4833302170 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + - @backstage/types@1.1.1 + ## 0.3.7-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index a820392630..36aed409cc 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.3.7-next.1", + "version": "0.3.7-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index b1bb14c6a7..0ed359768e 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-scaffolder-backend +## 1.23.1-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.23.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 8f304690f6..974125ad94 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "1.23.1-next.1", + "version": "1.23.1-next.2", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 6c2b31d18b..4810656ef4 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-common +## 1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 1.5.5-next.0 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index e37ee5462c..949a831906 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.5.5-next.0", + "version": "1.5.5-next.1", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md index d49e076730..c7d37fb0c8 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/types@1.1.1 + ## 0.1.10-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index f3ee13fcbe..707ffa1388 100644 --- a/plugins/scaffolder-node-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node-test-utils", - "version": "0.1.10-next.1", + "version": "0.1.10-next.2", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 3dc6b6b952..42fc13a644 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-node +## 0.4.9-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.9-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 544c40d5b7..6e84b278c2 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.4.9-next.1", + "version": "0.4.9-next.2", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index ab664aecb3..1661f3d1c7 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-scaffolder-react +## 1.11.0-next.2 + +### Patch Changes + +- 072c00c: Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes +- 04759f2: Fix null check in `isJsonObject` utility function for scaffolder review state component +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.11.0-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index d4bdd70081..b47bbd731f 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.11.0-next.1", + "version": "1.11.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 8bb5dc093d..3c4b39541c 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-scaffolder +## 1.24.0-next.2 + +### Minor Changes + +- 3fca643: Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + ## 1.23.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 5d0b842be6..43b94a9361 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.23.1-next.1", + "version": "1.24.0-next.2", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index 14b950872a..8c8e397620 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.29-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 9914ab599e..8eda12eac2 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.1.29-next.1", + "version": "0.1.29-next.2", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index d851114cc3..f6866f0878 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.5.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/config@1.2.0 + ## 1.5.4-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 56a1dcd6e2..e4f6a80774 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", - "version": "1.5.4-next.1", + "version": "1.5.4-next.2", "description": "A module for the search backend that implements search using ElasticSearch", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index 344969254c..4ea47e64cc 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + ## 0.1.29-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 9c6197e8ca..0b9f84e86e 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-explore", - "version": "0.1.29-next.1", + "version": "0.1.29-next.2", "description": "A module for the search backend that exports explore modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 03a33c2081..bc3a4e1ef4 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/config@1.2.0 + ## 0.5.33-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 62b5309b06..76e5de5af1 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.5.33-next.1", + "version": "0.5.33-next.2", "description": "A module for the search backend that implements search using PostgreSQL", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md index a3c64851bb..caada51643 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.1.16-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index 08236889af..91a45a3a65 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", - "version": "0.1.16-next.1", + "version": "0.1.16-next.2", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 9f5b9e41da..743b2a32ee 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.28-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.1.28-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index 0dc3a362d3..92b59c209c 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.1.28-next.1", + "version": "0.1.28-next.2", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 9597076338..90c4b9fa8b 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-search-backend-node +## 1.2.28-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 1.2.28-next.1 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 01337796b0..a7c53e61d1 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "1.2.28-next.1", + "version": "1.2.28-next.2", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 16e4c8ae4f..b6f4e9c001 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend +## 1.5.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.5.15-next.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 0e39ff2638..105a317a39 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "1.5.15-next.1", + "version": "1.5.15-next.2", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin", diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index dc76382d5c..9b6f2ed89c 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-common +## 1.2.14-next.1 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + ## 1.2.14-next.0 ### Patch Changes diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index c58aff0c21..073f5ead39 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-common", - "version": "1.2.14-next.0", + "version": "1.2.14-next.1", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", "backstage": { "role": "common-library", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index f3c5688145..4b8307c516 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-search-react +## 1.7.14-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.7.14-next.1 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 87ebe36f14..21ee4e4904 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.7.14-next.1", + "version": "1.7.14-next.2", "backstage": { "role": "web-library", "pluginId": "search", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 5e04d7a198..ad3703a51e 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-search +## 1.4.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.4.15-next.1 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index e1cbfcd76a..9cad64fcd5 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.15-next.1", + "version": "1.4.15-next.2", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index d95a9d74a3..ad5f012dbd 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-signals-backend +## 0.1.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 05e7189beb..81627acd13 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "backstage": { "role": "backend-plugin", "pluginId": "signals", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index ac1b4d835d..c9b4ed0d9f 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-signals-node +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index 7b98587482..7dd3df3902 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-node", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "description": "Node.js library for the signals plugin", "backstage": { "role": "node-library", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 728f9e073d..e6556fd543 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 1.0.37-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index a13624e2d8..9ef481927e 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.37-next.1", + "version": "1.0.37-next.2", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index cc0383d7f4..0fc47107da 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-techdocs-backend +## 1.10.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + ## 1.10.10-next.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 448f88e800..9c0355956e 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "1.10.10-next.1", + "version": "1.10.10-next.2", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 40c203014e..22cf92a28c 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs-node +## 1.12.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + ## 1.12.9-next.1 ### Patch Changes diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 8553169546..f6fa8a9948 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.12.9-next.1", + "version": "1.12.9-next.2", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", "backstage": { "role": "node-library", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 0318bf45aa..237612750d 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-techdocs +## 1.10.8-next.2 + +### Patch Changes + +- 67e76f2: TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. +- bdc5471: Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 1.10.8-next.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index c08dd92f09..9ff5e3e2f4 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.10.8-next.1", + "version": "1.10.8-next.2", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 9101a22bef..144d27d627 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-user-settings-backend +## 0.2.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.2.22-next.1 ### Patch Changes diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 244209a267..808f59fec4 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.2.22-next.1", + "version": "0.2.22-next.2", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index dafa984b09..a1aaa74828 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-user-settings +## 0.8.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.8.11-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index b229e625b6..4217cf3329 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.11-next.1", + "version": "0.8.11-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", From fed70efe37f27ce7f589108475c3be29aa4d69d0 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 16:03:35 +0200 Subject: [PATCH 135/204] chore: working through the inputs merging Signed-off-by: blam --- .../wiring/createExtensionBlueprint.test.tsx | 66 +++++++++++++++++++ .../src/wiring/createExtensionBlueprint.ts | 39 +++++++---- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index dfdbfface6..5f1324fe7c 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -384,4 +384,70 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + + it('should allow merging of inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef], { + singleton: true, + }), + }, + output: [coreExtensionData.reactElement.optional()], + factory(_params: { x?: string }, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + unused(ref); + return []; + }, + }); + + blueprint.make({ + inputs: { + test2: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + }, + factory(origFactory, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + const el: JSX.Element = inputs.test2.get( + coreExtensionData.reactElement, + ); + + unused(ref, el); + + return origFactory({}); + }, + }); + + expect(true).toBe(true); + }); + + it('should not allow overriding inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef]), + }, + output: [coreExtensionData.reactElement.optional()], + factory() { + return []; + }, + }); + + blueprint.make({ + inputs: { + // @ts-expect-error + test: createExtensionInput([]), // Overrides are not allowed + }, + factory(origFactory) { + return origFactory({}); + }, + }); + + expect(true).toBe(true); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 46db278bf8..90736703a0 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -83,7 +83,6 @@ export interface ExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown }, TConfigInput extends { [key in string]: unknown }, TDataRefs extends { [name in string]: AnyExtensionDataRef }, @@ -101,13 +100,23 @@ export interface ExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UExtraOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, >( args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; disabled?: boolean; - inputs?: TInputs; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; output?: Array; config?: { schema: TExtensionConfigSchema & { @@ -132,7 +141,7 @@ export interface ExtensionBlueprint< ReturnType >; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; } & VerifyExtensionFactoryOutput< @@ -172,7 +181,6 @@ class ExtensionBlueprintImpl< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > { @@ -196,12 +204,22 @@ class ExtensionBlueprintImpl< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UExtraOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, >(args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; disabled?: boolean; - inputs?: TInputs; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; output?: Array; params?: TParams; config?: { @@ -228,7 +246,7 @@ class ExtensionBlueprintImpl< } & { [key in keyof TConfigSchema]: z.infer>; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; }): ExtensionDefinition< @@ -277,7 +295,7 @@ class ExtensionBlueprintImpl< name: args.name ?? name, attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, - inputs: args.inputs ?? this.options.inputs, + inputs: { ...args.inputs, ...this.options.inputs }, output: [...(args.output ?? []), ...this.options.output], config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { @@ -297,7 +315,7 @@ class ExtensionBlueprintImpl< return this.options.factory(innerParams, { node, config: innerContext?.config ?? config, - inputs: innerContext?.inputs ?? inputs, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden }); }, { @@ -317,7 +335,7 @@ class ExtensionBlueprintImpl< }, } as CreateExtensionOptions< UOutput, - TInputs, + TInputs & TExtraInputs, { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -357,7 +375,6 @@ export function createExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: AnyExtensionDataRef } = never, @@ -374,7 +391,6 @@ export function createExtensionBlueprint< TParams, UOutput, TInputs, - UExtraOutput, string extends keyof TConfigSchema ? {} : { [key in keyof TConfigSchema]: z.infer> }, @@ -391,7 +407,6 @@ export function createExtensionBlueprint< TParams, UOutput, TInputs, - UExtraOutput, string extends keyof TConfigSchema ? {} : { From 65a1f99d0320179a22129956d5f6291f1765d73a Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 17:11:56 +0200 Subject: [PATCH 136/204] chore: Fixing some typescript errors Signed-off-by: blam --- .../src/wiring/createExtensionBlueprint.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 90736703a0..465bb10ee6 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -328,7 +328,10 @@ class ExtensionBlueprintImpl< return this.options.factory(args.params, { node, config, - inputs, + // TODO: Figure out types once legacy data map input type is gone + inputs: inputs as unknown as Expand< + ResolvedExtensionInputs + >, }); } throw new Error('Either params or factory must be provided'); From 34f1b2aeb1234428fe74e4581161845dd8fbfdff Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 17:13:50 +0200 Subject: [PATCH 137/204] chore: changeset Signed-off-by: blam --- .changeset/six-mails-smell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-mails-smell.md diff --git a/.changeset/six-mails-smell.md b/.changeset/six-mails-smell.md new file mode 100644 index 0000000000..db39259632 --- /dev/null +++ b/.changeset/six-mails-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Support merging of `inputs` in `Blueprints` From 1533b959ef56550d5dfa38ecd4591845796522b1 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 17:35:13 +0200 Subject: [PATCH 138/204] chore: steps to making outputs and factories look pretty Signed-off-by: blam --- .../src/wiring/createExtension.ts | 36 +++++---- .../wiring/createExtensionBlueprint.test.tsx | 76 +++++++++++++++++++ .../src/wiring/createExtensionBlueprint.ts | 6 +- 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index a8bf4ef17c..101c5cd43a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -148,7 +148,9 @@ export interface LegacyCreateExtensionOptions< /** @ignore */ export type VerifyExtensionFactoryOutput< UDeclaredOutput extends AnyExtensionDataRef, - UFactoryOutput extends ExtensionDataValue, + UFactoryOutput extends + | ExtensionDataValue + | ExtensionDataContainer, > = ( UDeclaredOutput extends any ? UDeclaredOutput['config']['optional'] extends true @@ -156,21 +158,29 @@ export type VerifyExtensionFactoryOutput< : UDeclaredOutput['id'] : never ) extends infer IRequiredOutputIds - ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] - ? {} + ? ( + UFactoryOutput extends ExtensionDataValue + ? UFactoryOutput['id'] + : UFactoryOutput extends ExtensionDataContainer + ? IDataRefs['id'] + : never + ) extends infer IFactoryOutputIds + ? [IRequiredOutputIds] extends [IFactoryOutputIds] + ? [IFactoryOutputIds] extends [UDeclaredOutput['id']] + ? {} + : { + 'Error: The extension factory has undeclared output(s)': Exclude< + IFactoryOutputIds, + UDeclaredOutput['id'] + >; + } : { - 'Error: The extension factory has undeclared output(s)': Exclude< - UFactoryOutput['id'], - UDeclaredOutput['id'] + 'Error: The extension factory is missing the following output(s)': Exclude< + IRequiredOutputIds, + IFactoryOutputIds >; } - : { - 'Error: The extension factory is missing the following output(s)': Exclude< - IRequiredOutputIds, - UFactoryOutput['id'] - >; - } + : never : never; /** @public */ diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 5f1324fe7c..fed8185df1 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -21,6 +21,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; +import { toInternalExtensionDefinition } from './createExtension'; function unused(..._any: any[]) {} @@ -450,4 +451,79 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + + it('should replace the outputs when provided through make', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + const ext = toInternalExtensionDefinition( + blueprint.make({ + output: [testDataRef2], + factory(origFactory) { + const parent = origFactory({}); + return [testDataRef2(`${parent.get(testDataRef1)}bar`)]; + }, + }), + ); + + expect(ext.output).toEqual([testDataRef2]); + }); + + it('should allow returning of the parent data container', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield origFactory({}); + yield testDataRef2('bar'); + }, + }); + + expect(true).toBe(true); + // todo: test that the data is actually available + }); + + it('should not allow returning parent output if outputs are overridden', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.make({ + output: [testDataRef2.optional()], + *factory(origFactory) { + // yield testDataRef1('bar'); + yield testDataRef2('bar'); + }, + }); + + expect(true).toBe(true); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 465bb10ee6..e56c30410d 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -204,7 +204,7 @@ class ExtensionBlueprintImpl< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, - UExtraOutput extends AnyExtensionDataRef, + UNewOutput extends AnyExtensionDataRef, TExtraInputs extends { [inputName in string]: ExtensionInput< AnyExtensionDataRef, @@ -220,7 +220,7 @@ class ExtensionBlueprintImpl< [KName in keyof TInputs]?: `Error: Input '${KName & string}' is already defined in parent definition`; }; - output?: Array; + output?: Array; params?: TParams; config?: { schema: TExtensionConfigSchema; @@ -296,7 +296,7 @@ class ExtensionBlueprintImpl< attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, inputs: { ...args.inputs, ...this.options.inputs }, - output: [...(args.output ?? []), ...this.options.output], + output: args.output ?? this.options.output, config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { if (args.factory) { From eda2165093264814f648d0e9db7f495149701a79 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 15:47:57 +0200 Subject: [PATCH 139/204] frontend-plugin-api: working types for extension data containers as iterables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtension.ts | 12 +++++++++++- .../src/wiring/createExtensionBlueprint.test.tsx | 12 +++++++++--- .../src/wiring/createExtensionBlueprint.ts | 14 +++++++------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 101c5cd43a..2c82c44ee1 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -65,7 +65,17 @@ export type ExtensionDataValues = { /** @public */ export type ExtensionDataContainer = - { + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { get( ref: ExtensionDataRef, ): UExtensionData extends ExtensionDataRef diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index fed8185df1..70d874d9ad 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -494,11 +494,18 @@ describe('createExtensionBlueprint', () => { blueprint.make({ output: [testDataRef1, testDataRef2], *factory(origFactory) { - yield origFactory({}); + yield* origFactory({}); yield testDataRef2('bar'); }, }); + blueprint.make({ + output: [testDataRef1, testDataRef2], + factory(origFactory) { + return [...origFactory({}), testDataRef2('bar')]; + }, + }); + expect(true).toBe(true); // todo: test that the data is actually available }); @@ -518,8 +525,7 @@ describe('createExtensionBlueprint', () => { blueprint.make({ output: [testDataRef2.optional()], - *factory(origFactory) { - // yield testDataRef1('bar'); + *factory() { yield testDataRef2('bar'); }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index e56c30410d..603ffda621 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -18,6 +18,7 @@ import { AppNode } from '../apis'; import { Expand } from '../types'; import { CreateExtensionOptions, + ExtensionDataContainer, ExtensionDefinition, ResolvedExtensionInputs, VerifyExtensionFactoryOutput, @@ -27,7 +28,6 @@ import { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, - ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; @@ -100,7 +100,7 @@ export interface ExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, - UExtraOutput extends AnyExtensionDataRef, + UNewOutput extends AnyExtensionDataRef, TExtraInputs extends { [inputName in string]: ExtensionInput< AnyExtensionDataRef, @@ -117,7 +117,7 @@ export interface ExtensionBlueprint< [KName in keyof TInputs]?: `Error: Input '${KName & string}' is already defined in parent definition`; }; - output?: Array; + output?: Array; config?: { schema: TExtensionConfigSchema & { [KName in keyof TConfig]?: `Error: Config key '${KName & @@ -133,7 +133,7 @@ export interface ExtensionBlueprint< config?: TConfig; inputs?: Expand>; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: TConfig & { @@ -145,7 +145,7 @@ export interface ExtensionBlueprint< }, ): Iterable; } & VerifyExtensionFactoryOutput< - UOutput & UExtraOutput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, UFactoryOutput >) | { @@ -236,7 +236,7 @@ class ExtensionBlueprintImpl< }; inputs?: Expand>; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: { @@ -311,7 +311,7 @@ class ExtensionBlueprintImpl< }; inputs?: Expand>; }, - ): Iterable> => { + ): ExtensionDataContainer => { return this.options.factory(innerParams, { node, config: innerContext?.config ?? config, From e8867e17f1fb751cb47b85d273fb93a2ba4e3bc3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:23:26 +0200 Subject: [PATCH 140/204] frontend-plugin-api: extension data container runtime + tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../wiring/createExtensionBlueprint.test.tsx | 99 +++++++++++++++---- .../src/wiring/createExtensionBlueprint.ts | 37 ++++++- 2 files changed, 113 insertions(+), 23 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 70d874d9ad..66c3939a96 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -21,10 +21,21 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; -import { toInternalExtensionDefinition } from './createExtension'; +import { + ExtensionDefinition, + toInternalExtensionDefinition, +} from './createExtension'; function unused(..._any: any[]) {} +function factoryOutput(ext: ExtensionDefinition) { + const int = toInternalExtensionDefinition(ext); + if (int.version !== 'v2') { + throw new Error('Expected v2 extension'); + } + return Array.from(int.factory({} as any)); +} + describe('createExtensionBlueprint', () => { it('should allow creation of extension blueprints', () => { const TestExtensionBlueprint = createExtensionBlueprint({ @@ -476,6 +487,8 @@ describe('createExtensionBlueprint', () => { ); expect(ext.output).toEqual([testDataRef2]); + + expect(factoryOutput(ext)).toEqual([testDataRef2('foobar')]); }); it('should allow returning of the parent data container', () => { @@ -491,23 +504,28 @@ describe('createExtensionBlueprint', () => { }, }); - blueprint.make({ - output: [testDataRef1, testDataRef2], - *factory(origFactory) { - yield* origFactory({}); - yield testDataRef2('bar'); - }, - }); + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); - blueprint.make({ - output: [testDataRef1, testDataRef2], - factory(origFactory) { - return [...origFactory({}), testDataRef2('bar')]; - }, - }); - - expect(true).toBe(true); - // todo: test that the data is actually available + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef1, testDataRef2], + factory(origFactory) { + return [...origFactory({}), testDataRef2('bar')]; + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); }); it('should not allow returning parent output if outputs are overridden', () => { @@ -523,13 +541,58 @@ describe('createExtensionBlueprint', () => { }, }); + // @ts-expect-error blueprint.make({ output: [testDataRef2.optional()], *factory() { + yield testDataRef1('foo'); yield testDataRef2('bar'); }, }); - expect(true).toBe(true); + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef2.optional()], + *factory() { + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef2('bar')]); + }); + + it('should not rely on optional outputs when forwarding from parent', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1, testDataRef2.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + // @ts-expect-error + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + }, + }); + + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 603ffda621..2b80dac81e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -28,6 +28,7 @@ import { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, + ExtensionDataRef, ExtensionDataValue, } from './createExtensionDataRef'; @@ -169,6 +170,30 @@ export interface ExtensionBlueprint< >; } +/** @internal */ +function createDataContainer( + values: Iterable< + UData extends ExtensionDataRef + ? ExtensionDataValue + : never + >, +): ExtensionDataContainer { + const container = new Map>(); + + for (const output of values) { + container.set(output.id, output); + } + + return { + get(ref) { + return container.get(ref.id)?.value; + }, + [Symbol.iterator]() { + return container.values(); + }, + } as ExtensionDataContainer; +} + /** * @internal */ @@ -312,11 +337,13 @@ class ExtensionBlueprintImpl< inputs?: Expand>; }, ): ExtensionDataContainer => { - return this.options.factory(innerParams, { - node, - config: innerContext?.config ?? config, - inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden - }); + return createDataContainer( + this.options.factory(innerParams, { + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }), + ); }, { node, From 8015a2d81c730c28be30598cc1cb9cd5a9fa5dcc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:27:18 +0200 Subject: [PATCH 141/204] changesets: update blueprints changeset Signed-off-by: Patrik Oldsberg --- .changeset/six-mails-smell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/six-mails-smell.md b/.changeset/six-mails-smell.md index db39259632..3845c72626 100644 --- a/.changeset/six-mails-smell.md +++ b/.changeset/six-mails-smell.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': patch --- -Support merging of `inputs` in `Blueprints` +Support merging of `inputs` in extension blueprints, but stop merging `output`. In addition, the original factory in extension blueprints now returns a data container that both provides access to the returned data, but can also be forwarded as output. From 51a4ef638b0406c7bbb1ade7a267ae33af3d91b9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:29:14 +0200 Subject: [PATCH 142/204] frontend-plugin-api: update API report Signed-off-by: Patrik Oldsberg --- packages/frontend-plugin-api/api-report.md | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index cbe92463ba..38b980fcbf 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -603,7 +603,6 @@ export function createExtensionBlueprint< } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -624,7 +623,6 @@ export function createExtensionBlueprint< TParams, UOutput, TInputs, - UExtraOutput, string extends keyof TConfigSchema ? {} : { @@ -1090,7 +1088,6 @@ export interface ExtensionBlueprint< } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown; }, @@ -1108,6 +1105,16 @@ export interface ExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, >( args: { namespace?: string; @@ -1117,8 +1124,11 @@ export interface ExtensionBlueprint< input: string; }; disabled?: boolean; - inputs?: TInputs; - output?: Array; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; config?: { schema: TExtensionConfigSchema & { [KName in keyof TConfig]?: `Error: Config key '${KName & @@ -1134,7 +1144,7 @@ export interface ExtensionBlueprint< config?: TConfig; inputs?: Expand>; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: TConfig & { @@ -1142,11 +1152,11 @@ export interface ExtensionBlueprint< ReturnType >; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; } & VerifyExtensionFactoryOutput< - UOutput & UExtraOutput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, UFactoryOutput >) | { @@ -1186,7 +1196,17 @@ export interface ExtensionBoundaryProps { // @public (undocumented) export type ExtensionDataContainer = - { + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { get( ref: ExtensionDataRef, ): UExtensionData extends ExtensionDataRef @@ -1356,7 +1376,6 @@ export const IconBundleBlueprint: ExtensionBlueprint< } >; }, - AnyExtensionDataRef, { icons: string; test: string; From 2212c3ef08c63cd54ef19333200c63da9e7d895f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:32:28 +0200 Subject: [PATCH 143/204] frontend-plugin-api: clean up output verification Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtension.ts | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 2c82c44ee1..2a29fe1ab4 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -158,9 +158,7 @@ export interface LegacyCreateExtensionOptions< /** @ignore */ export type VerifyExtensionFactoryOutput< UDeclaredOutput extends AnyExtensionDataRef, - UFactoryOutput extends - | ExtensionDataValue - | ExtensionDataContainer, + UFactoryOutput extends ExtensionDataValue, > = ( UDeclaredOutput extends any ? UDeclaredOutput['config']['optional'] extends true @@ -168,29 +166,21 @@ export type VerifyExtensionFactoryOutput< : UDeclaredOutput['id'] : never ) extends infer IRequiredOutputIds - ? ( - UFactoryOutput extends ExtensionDataValue - ? UFactoryOutput['id'] - : UFactoryOutput extends ExtensionDataContainer - ? IDataRefs['id'] - : never - ) extends infer IFactoryOutputIds - ? [IRequiredOutputIds] extends [IFactoryOutputIds] - ? [IFactoryOutputIds] extends [UDeclaredOutput['id']] - ? {} - : { - 'Error: The extension factory has undeclared output(s)': Exclude< - IFactoryOutputIds, - UDeclaredOutput['id'] - >; - } + ? [IRequiredOutputIds] extends [UFactoryOutput['id']] + ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] + ? {} : { - 'Error: The extension factory is missing the following output(s)': Exclude< - IRequiredOutputIds, - IFactoryOutputIds + 'Error: The extension factory has undeclared output(s)': Exclude< + UFactoryOutput['id'], + UDeclaredOutput['id'] >; } - : never + : { + 'Error: The extension factory is missing the following output(s)': Exclude< + IRequiredOutputIds, + UFactoryOutput['id'] + >; + } : never; /** @public */ From 8543e723a5d40b61e7fb32bd5f6d6c9f0df1883a Mon Sep 17 00:00:00 2001 From: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:55:39 -0400 Subject: [PATCH 144/204] Add UI element to notify user before redirect occurs. (#25911) * Add UI element to notify user before redirect occurs. Allow user to skip to redirect from notification. --------- Signed-off-by: Sydney Achinger --- .changeset/popular-panthers-hear.md | 5 + .../transformers/handleMetaRedirects.test.ts | 30 ++++- .../transformers/handleMetaRedirects.ts | 59 -------- .../transformers/handleMetaRedirects.tsx | 126 ++++++++++++++++++ 4 files changed, 160 insertions(+), 60 deletions(-) create mode 100644 .changeset/popular-panthers-hear.md delete mode 100644 plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts create mode 100644 plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx diff --git a/.changeset/popular-panthers-hear.md b/.changeset/popular-panthers-hear.md new file mode 100644 index 0000000000..c31586ea82 --- /dev/null +++ b/.changeset/popular-panthers-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs redirect feature now includes a notification to the user before they are redirected. diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts index 8fbd63428d..12a552577a 100644 --- a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts @@ -16,6 +16,7 @@ import { handleMetaRedirects } from './handleMetaRedirects'; import { createTestShadowDom } from '../../test-utils'; +import { screen } from '@testing-library/react'; describe('handleMetaRedirects', () => { const navigate = jest.fn(); @@ -35,15 +36,19 @@ describe('handleMetaRedirects', () => { }, writable: true, }); - return await createTestShadowDom(html, { preTransformers: [], postTransformers: [handleMetaRedirects(navigate, entityName)], }); }; + beforeEach(() => { + jest.useFakeTimers(); + }); afterEach(() => { jest.clearAllMocks(); + jest.useRealTimers(); + document.body.innerHTML = ''; }); it('should navigate to relative URL if meta redirect tag is present', async () => { @@ -52,6 +57,13 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); expect(navigate).toHaveBeenCalledWith( 'http://localhost/docs/default/component/testEntity/anotherPage', ); @@ -63,6 +75,13 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); expect(navigate).toHaveBeenCalledWith('/docs/default/component/testEntity'); }); @@ -72,6 +91,13 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); expect(navigate).toHaveBeenCalledWith('http://localhost/test'); }); @@ -81,6 +107,8 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + jest.runAllTimers(); expect(navigate).not.toHaveBeenCalled(); }); }); diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts deleted file mode 100644 index 4c56d4d363..0000000000 --- a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts +++ /dev/null @@ -1,59 +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 { Transformer } from './transformer'; -import { normalizeUrl } from './rewriteDocLinks'; - -export const handleMetaRedirects = ( - navigate: (to: string) => void, - entityName: string, -): Transformer => { - return dom => { - for (const elem of Array.from(dom.querySelectorAll('meta'))) { - if (elem.getAttribute('http-equiv') === 'refresh') { - const metaContentParameters = elem - .getAttribute('content') - ?.split('url='); - if (!metaContentParameters || metaContentParameters.length < 2) { - continue; - } - - const metaUrl = metaContentParameters[1]; - const normalizedCurrentUrl = normalizeUrl(window.location.href); - // If metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href when creating URL object. - const absoluteRedirectObj = new URL(metaUrl, normalizedCurrentUrl); - const isExternalRedirect = - absoluteRedirectObj.hostname !== window.location.hostname; - - if (isExternalRedirect) { - // If the redirect is external, navigate to the documentation site home instead of the external url. - const currentTechDocPath = window.location.pathname; - const indexOfSiteHome = currentTechDocPath.indexOf(entityName); - const siteHomePath = currentTechDocPath.slice( - 0, - indexOfSiteHome + entityName.length, - ); - navigate(siteHomePath); - } else { - // The navigate function from dom.tsx is a wrapper around react-router navigate function that helps absolute url redirects. - navigate(absoluteRedirectObj.href); - } - return dom; - } - } - return dom; - }; -}; diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx new file mode 100644 index 0000000000..2d74f4cbf5 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx @@ -0,0 +1,126 @@ +/* + * 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 { Transformer } from './transformer'; +import { normalizeUrl } from './rewriteDocLinks'; +import Snackbar from '@material-ui/core/Snackbar'; +import React, { useState } from 'react'; +import { renderReactElement } from './renderReactElement'; +import Button from '@material-ui/core/Button'; +import { makeStyles } from '@material-ui/core/styles'; + +type RedirectNotificationProps = { + handleButtonClick: () => void; + message: string; + autoHideDuration: number; +}; + +const useStyles = makeStyles(theme => ({ + button: { + color: theme.palette.primary.light, + textDecoration: 'underline', + }, +})); +const RedirectNotification = ({ + message, + handleButtonClick, + autoHideDuration, +}: RedirectNotificationProps) => { + const classes = useStyles(); + const [open, setOpen] = useState(true); + const handleClose = () => { + setOpen(prev => !prev); + }; + + return ( + + Redirect now + + } + /> + ); +}; + +export const handleMetaRedirects = ( + navigate: (to: string) => void, + entityName: string, +): Transformer => { + const redirectAfterMs = 4000; + const determineRedirectURL = (metaUrl: string) => { + const normalizedCurrentUrl = normalizeUrl(window.location.href); + // If metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href when creating URL object. + const absoluteRedirectObj = new URL(metaUrl, normalizedCurrentUrl); + const isExternalRedirect = + absoluteRedirectObj.hostname !== window.location.hostname; + + if (isExternalRedirect) { + const currentTechDocPath = window.location.pathname; + const indexOfSiteHome = currentTechDocPath.indexOf(entityName); + const siteHomePath = currentTechDocPath.slice( + 0, + indexOfSiteHome + entityName.length, + ); + return siteHomePath; + } + // The navigate function from dom.tsx is a wrapper around react-router navigate function that helps absolute url redirects. + return absoluteRedirectObj.href; + }; + + return dom => { + for (const elem of Array.from(dom.querySelectorAll('meta'))) { + if (elem.getAttribute('http-equiv') === 'refresh') { + const metaContentParameters = elem + .getAttribute('content') + ?.split('url='); + if (!metaContentParameters || metaContentParameters.length < 2) { + continue; + } + const metaUrl = metaContentParameters[1]; + const redirectURL = determineRedirectURL(metaUrl); + const container = document.createElement('div'); + + renderReactElement( + navigate(redirectURL)} + autoHideDuration={redirectAfterMs} + />, + container, + ); + document.body.appendChild(container); + + setTimeout(() => { + navigate(redirectURL); + }, redirectAfterMs); + + return dom; + } + } + return dom; + }; +}; From d18f4eba43f6fe589c09b62b47a3bb9fe099be23 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 7 Aug 2024 11:24:46 +0530 Subject: [PATCH 145/204] scaffolder: remove undefined from the title of Scaffolder Run page Signed-off-by: Himanshu Mishra --- .changeset/real-lizards-sit.md | 5 +++++ .../scaffolder/src/components/OngoingTask/OngoingTask.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/real-lizards-sit.md diff --git a/.changeset/real-lizards-sit.md b/.changeset/real-lizards-sit.md new file mode 100644 index 0000000000..747f041508 --- /dev/null +++ b/.changeset/real-lizards-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Fix undefined in the title of Scaffolder Runs on the page load diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index 31c2870352..459ba682d2 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -173,7 +173,9 @@ export const OngoingTask = (props: { return (
    Run of {templateName} From b9cd50536fa43f1c9c8ac62a819c2db826f526a7 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 11:21:31 +0200 Subject: [PATCH 146/204] chore: fix Signed-off-by: blam Signed-off-by: blam --- .../src/wiring/createExtensionBlueprint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 2b80dac81e..88b2523111 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -420,7 +420,7 @@ export function createExtensionBlueprint< ): ExtensionBlueprint< TParams, UOutput, - TInputs, + string extends keyof TInputs ? {} : TInputs, string extends keyof TConfigSchema ? {} : { [key in keyof TConfigSchema]: z.infer> }, @@ -436,7 +436,7 @@ export function createExtensionBlueprint< return new ExtensionBlueprintImpl(options) as ExtensionBlueprint< TParams, UOutput, - TInputs, + string extends keyof TInputs ? {} : TInputs, string extends keyof TConfigSchema ? {} : { From 6f72c2b9905c86fc7cf4d30ced9887facf017e58 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 11:22:37 +0200 Subject: [PATCH 147/204] chore: updating changeset Signed-off-by: blam --- .changeset/breezy-cats-kiss.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/breezy-cats-kiss.md diff --git a/.changeset/breezy-cats-kiss.md b/.changeset/breezy-cats-kiss.md new file mode 100644 index 0000000000..3bcbfaf00a --- /dev/null +++ b/.changeset/breezy-cats-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Fixing issue with `Blueprints` `inputs` merging From 9a0147e6daa458ecd1add64d1a20ed3c48781b24 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 11:39:13 +0200 Subject: [PATCH 148/204] chore: fixing api reports Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 38b980fcbf..4dc9c99c78 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -622,7 +622,7 @@ export function createExtensionBlueprint< ): ExtensionBlueprint< TParams, UOutput, - TInputs, + string extends keyof TInputs ? {} : TInputs, string extends keyof TConfigSchema ? {} : { @@ -1367,15 +1367,7 @@ export const IconBundleBlueprint: ExtensionBlueprint< 'core.icons', {} >, - { - [x: string]: ExtensionInput< - AnyExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; - }, + {}, { icons: string; test: string; From f4612847a2090aec61e05798f7a758328c4df3c1 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Tue, 6 Aug 2024 16:05:57 +0200 Subject: [PATCH 149/204] Add install instructions for older nevironments Signed-off-by: Marek Libra --- docs/notifications/getting-started.md | 104 +++++++++++++++++++++++- plugins/notifications-backend/README.md | 12 +-- plugins/notifications/README.md | 35 +------- plugins/signals-backend/README.md | 39 +-------- plugins/signals/README.md | 18 +--- 5 files changed, 107 insertions(+), 101 deletions(-) diff --git a/docs/notifications/getting-started.md b/docs/notifications/getting-started.md index 583be64cc8..dc006e21b2 100644 --- a/docs/notifications/getting-started.md +++ b/docs/notifications/getting-started.md @@ -6,7 +6,7 @@ description: How to get started with the notifications and signals The Backstage Notifications System provides a way for plugins and external services to send notifications to Backstage users. These notifications are displayed in the dedicated page of the Backstage frontend UI or by frontend plugins per specific scenarios. -Additionally, plugins can implement processors to send notifications through external channels like email, Slack, or MS Teams. +Additionally, notifications can be sent to external channels (like email) via "processors" implemented within plugins. Notifications can be optionally integrated with the signals (a push mechanism) to ensure users receive them immediately. @@ -31,6 +31,104 @@ Example of use-cases: - Notifications for individuals: e.g., updates you have subscribed to, new required training courses - Notifications pertaining to a particular entity in the catalog: A notification might apply to an entity and the owning team. +## Installation in Older Environments + +Newer versions of instances created by the create-app have both the notifications and signals plugins included by default, this section can be skipped right to the Configuration. + +Following installation instructions are valid for enabling the plugins in older environments. + +### Add Notifications Backend + +```bash +yarn workspace backend add @backstage/plugin-notifications-backend +``` + +Add the notifications to your `backend/src/index.ts`: + +```ts +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-notifications-backend')); +``` + +### Add Notifications Frontend + +```bash +yarn workspace app add @backstage/notifications +``` + +To add the notifications main menu, add following to your `packages/app/src/components/Root/Root.tsx`: + +```tsx +import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; + + + + + // ... + + + +; +``` + +Also add the route to notifications to `packages/app/src/App.tsx`: + +```tsx +import { NotificationsPage } from '@backstage/plugin-notifications'; + + + // ... + } /> +; +``` + +### Optional: Add Signals Backend + +Optionally add Signals to your backend by + +```bash +yarn workspace backend add @backstage/plugin-signals-backend +``` + +Add the signals to your `backend/src/index.ts`: + +```ts +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-signals-backend')); +``` + +### Optional: Signals Frontend + +The use of signals is optional but improves user experience. + +Start with: + +```bash +yarn workspace app add @backstage/plugin-signals +``` + +To install the plugin, you have to add the following to your `packages/app/src/plugins.ts`: + +```ts +export { signalsPlugin } from '@backstage/plugin-signals'; +``` + +And make sure that your `packages/app/src/App.tsx` contains: + +```ts +import * as plugins from './plugins'; + +const app = createApp({ + // ... + plugins: Object.values(plugins), + // ... +}); +``` + +If the signals plugin is properly configured, it will be automatically discovered by the notifications plugin and used. + ## Configuration ### Notifications Backend @@ -209,7 +307,9 @@ curl -X POST https://[BACKSTAGE_BACKEND]/api/notifications -H "Content-Type: app ## Additional info -Additional details can be found in the plugins' implementation: +An example of a backend plugin sending notifications can be found in https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-notifications. + +Sources of the notifications and signal plugins: - https://github.com/backstage/backstage/blob/master/plugins/notifications diff --git a/plugins/notifications-backend/README.md b/plugins/notifications-backend/README.md index ebd129f572..a6b6615d9f 100644 --- a/plugins/notifications-backend/README.md +++ b/plugins/notifications-backend/README.md @@ -4,17 +4,7 @@ Welcome to the notifications backend plugin! ## Getting started -```bash -yarn workspace backend add @backstage/notifications-backend -``` - -Add the notifications to your backend: - -```ts -const backend = createBackend(); -// ... -backend.add(import('@backstage/plugin-notifications-backend')); -``` +To install, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. For users to be able to see notifications in real-time, you have to install also the signals plugin (`@backstage/plugin-signals-node`, `@backstage/plugin-signals-backend`, and diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md index 32fbb8c701..78bcf4e753 100644 --- a/plugins/notifications/README.md +++ b/plugins/notifications/README.md @@ -4,40 +4,9 @@ Welcome to the notifications plugin! ## Getting started -First, install the `@backstage/plugin-notifications-backend` and `@backstage/plugin-notifications-node` packages. -See the documentation for installation instructions. +To install, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. -Then add this frontend package: - -```bash -yarn workspace app add @backstage/notifications -``` - -To add the notifications main menu, add the following to your `packages/app/src/components/Root/Root.tsx`: - -```tsx -import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; - - - - - // ... - - - -; -``` - -Also add the route to notifications to `packages/app/src/App.tsx`: - -```tsx -import { NotificationsPage } from '@backstage/plugin-notifications'; - - - // ... - } /> -; -``` +Please mind installing the `@backstage/plugin-notifications-backend` and `@backstage/plugin-notifications-node` packages before this frontend plugin. ## Real-time notifications diff --git a/plugins/signals-backend/README.md b/plugins/signals-backend/README.md index d76038a38e..24d8832d26 100644 --- a/plugins/signals-backend/README.md +++ b/plugins/signals-backend/README.md @@ -6,41 +6,4 @@ Signals plugin allows backend plugins to publish messages to frontend plugins. ## Getting started -First install the `@backstage/plugin-signals-node` plugin to get the `SignalsService` set up. - -Next, add Signals router to your backend in `packages/backend/src/plugins/signals.ts`: - -```ts -import { Router } from 'express'; -import { createRouter } from '@backstage/plugin-signals-backend'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - eventBroker: env.eventBroker, - identity: env.identity, - discovery: env.discovery, - }); -} -``` - -Now add the signals to `packages/backend/src/index.ts`: - -```ts -// ... -import signals from './plugins/signals'; - -async function main() { - // ... - const signalsEnv = useHotMemoize(module, () => createEnv('signals')); - - const apiRouter = Router(); - // ... - apiRouter.use('/signals', await signals(signalsEnv)); - apiRouter.use(notFoundHandler()); - // ... -} -``` +To install this signals backend plugin, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. diff --git a/plugins/signals/README.md b/plugins/signals/README.md index 38dc18808b..5b08983621 100644 --- a/plugins/signals/README.md +++ b/plugins/signals/README.md @@ -9,23 +9,7 @@ Signals plugin allows backend plugins to publish messages to frontend plugins. This plugin contains client that can receive messages from the backend. To get started, see installation instructions from `@backstage/plugin-signals-node`, `@backstage/plugin-signals-backend`. -To install the plugin, you have to add the following to your `packages/app/src/plugins.ts`: - -```ts -export { signalsPlugin } from '@backstage/plugin-signals'; -``` - -And make sure that your `packages/app/src/App.tsx` contains: - -```ts -import * as plugins from './plugins'; - -const app = createApp({ - // ... - plugins: Object.values(plugins), - // ... -}); -``` +To install this signals frontend plugin, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. Now you can utilize the API from other plugins using the `@backstage/plugin-signals-react` package or simply by: From 3a8e23d8ff1b03bc1caaae1441b7fb828f67e5b1 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 11:47:47 +0200 Subject: [PATCH 150/204] chore: updating changeset wording Signed-off-by: blam --- .changeset/breezy-cats-kiss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/breezy-cats-kiss.md b/.changeset/breezy-cats-kiss.md index 3bcbfaf00a..900016c1e8 100644 --- a/.changeset/breezy-cats-kiss.md +++ b/.changeset/breezy-cats-kiss.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': patch --- -Fixing issue with `Blueprints` `inputs` merging +Fixing issue with extension blueprints `inputs` merging. From 3fbf4a8719270ed227f8496ec0edeebca7abd271 Mon Sep 17 00:00:00 2001 From: Evan Kelly <47178446+EvanGKelly@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:13:29 +0100 Subject: [PATCH 151/204] Update docs/integrations/datadog-rum/installation.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Evan Kelly <47178446+EvanGKelly@users.noreply.github.com> --- docs/integrations/datadog-rum/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 414317d593..8213b8296f 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -28,7 +28,7 @@ app: If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. -Please note, there's a bug where env value MUST be specified at build time +Please note that the env value MUST be specified at build time :::note In case after a proper configuration, the events still arent being capture copy and paste this section in to your `packages/app/public/index.html` under the `` tag. From 807bd071759be7e431d09c300839177498850bc9 Mon Sep 17 00:00:00 2001 From: Evan Kelly <47178446+EvanGKelly@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:13:39 +0100 Subject: [PATCH 152/204] Update docs/integrations/datadog-rum/installation.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Evan Kelly <47178446+EvanGKelly@users.noreply.github.com> --- docs/integrations/datadog-rum/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 8213b8296f..6659bec182 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -31,7 +31,7 @@ If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8 Please note that the env value MUST be specified at build time :::note -In case after a proper configuration, the events still arent being capture copy and paste this section in to your `packages/app/public/index.html` under the `` tag. +In case after a proper configuration, the events still are not being captured: Copy and paste this section in to your `packages/app/public/index.html` under the `` tag. ```html <% if (config.has('app.datadogRum')) { %> From ddde5fe772b3cb910fa8397db67b8fe98472e135 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 7 Aug 2024 12:48:09 +0200 Subject: [PATCH 153/204] backend-plugin-api: fix for plugins and modules depending on multion services Signed-off-by: Patrik Oldsberg --- .changeset/seven-days-film.md | 5 ++ .changeset/silver-pillows-begin.md | 5 ++ .../config/vocabularies/Backstage/accept.txt | 1 + .../src/wiring/BackendInitializer.test.ts | 56 +++++++++++++++++++ .../src/compat/legacy/legacy.ts | 8 ++- packages/backend-plugin-api/api-report.md | 20 +++---- .../src/wiring/createBackendModule.test.ts | 33 +++++++++++ .../src/wiring/createBackendPlugin.test.ts | 48 ++++++++++++++++ .../backend-plugin-api/src/wiring/types.ts | 35 ++++++++---- 9 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 .changeset/seven-days-film.md create mode 100644 .changeset/silver-pillows-begin.md diff --git a/.changeset/seven-days-film.md b/.changeset/seven-days-film.md new file mode 100644 index 0000000000..9d757b0611 --- /dev/null +++ b/.changeset/seven-days-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Fixed a type issue where plugin and modules depending on multiton services would not receive the correct type. diff --git a/.changeset/silver-pillows-begin.md b/.changeset/silver-pillows-begin.md new file mode 100644 index 0000000000..8fe1ccc230 --- /dev/null +++ b/.changeset/silver-pillows-begin.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Internal type refactor. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 42b0e4895c..bad61de7ff 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -245,6 +245,7 @@ Monorepo monorepos msgraph msw +multiton mutex mutexes mysql diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 6f60d65861..cf8845fcec 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -342,6 +342,62 @@ describe('BackendInitializer', () => { await init.start(); }); + it('should allow plugins and modules depend on multiton services', async () => { + expect.assertions(2); + + const multiServiceRef = createServiceRef({ + id: 'a', + multiton: true, + }); + const init = new BackendInitializer(baseFactories); + + init.add( + createServiceFactory({ + service: multiServiceRef, + deps: {}, + factory: () => 'x', + }), + ); + init.add( + createServiceFactory({ + service: multiServiceRef, + deps: {}, + factory: () => 'y', + }), + ); + + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: { multi: multiServiceRef }, + async init({ multi }) { + expect(multi).toEqual(['x', 'y']); + }, + }); + }, + }), + ); + + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { multi: multiServiceRef }, + async init({ multi }) { + expect(multi).toEqual(['x', 'y']); + }, + }); + }, + }), + ); + + await init.start(); + }); + it('should forward errors when plugins fail to start', async () => { const init = new BackendInitializer([]); init.add( diff --git a/packages/backend-common/src/compat/legacy/legacy.ts b/packages/backend-common/src/compat/legacy/legacy.ts index c779daacd2..4dc7d1a09a 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -18,6 +18,7 @@ import { AuthService, coreServices, createBackendPlugin, + HttpRouterService, ServiceRef, } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; @@ -107,7 +108,10 @@ export function makeLegacyPlugin< return [key, transform(dep)]; } if (key === 'tokenManager') { - return [key, wrapTokenManager(dep as TokenManager, _auth)]; + return [ + key, + wrapTokenManager(dep as TokenManager, _auth as AuthService), + ]; } return [key, dep]; }), @@ -115,7 +119,7 @@ export function makeLegacyPlugin< const router = await createRouter( pluginEnv as TransformedEnv, ); - _router.use(router); + (_router as HttpRouterService).use(router); }, }); }, diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index e5189dce4d..319ddf0bc6 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -82,14 +82,12 @@ export interface BackendModuleRegistrationPoints { ): void; // (undocumented) registerInit< - Deps extends { - [name in string]: unknown; + TDeps extends { + [name in string]: ServiceRef | ExtensionPoint; }, >(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -105,14 +103,12 @@ export interface BackendPluginRegistrationPoints { ): void; // (undocumented) registerInit< - Deps extends { - [name in string]: unknown; + TDeps extends { + [name in string]: ServiceRef; }, >(options: { - deps: { - [name in keyof Deps]: ServiceRef; - }; - init(deps: Deps): Promise; + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } diff --git a/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts index e3c9f9007a..3426d5a90c 100644 --- a/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import { createServiceRef } from '../services'; import { createBackendModule } from './createBackendModule'; +import { createExtensionPoint } from './createExtensionPoint'; import { InternalBackendRegistrations } from './types'; describe('createBackendModule', () => { @@ -54,4 +56,35 @@ describe('createBackendModule', () => { // @ts-expect-error expect(module({ a: 'a' })).toBeDefined(); }); + + it('should be able to depend on all types of dependencies', () => { + const extensionPoint = createExtensionPoint({ id: 'point' }); + const singleServiceRef = createServiceRef({ id: 'single' }); + const multiServiceRef = createServiceRef({ + id: 'multi', + multiton: true, + }); + + const plugin = createBackendModule({ + pluginId: 'x', + moduleId: 'y', + register(r) { + r.registerInit({ + deps: { + point: extensionPoint, + single: singleServiceRef, + multi: multiServiceRef, + }, + async init({ point, single, multi }) { + const a: string = point; + const b: string = single; + const c: string[] = multi; + expect([a, b, c]).toBe('unused'); + }, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); }); diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts index db4d60ee19..f4a0877b85 100644 --- a/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import { createServiceRef } from '../services'; import { createBackendPlugin } from './createBackendPlugin'; +import { createExtensionPoint } from './createExtensionPoint'; import { InternalBackendRegistrations } from './types'; describe('createBackendPlugin', () => { @@ -52,4 +54,50 @@ describe('createBackendPlugin', () => { // @ts-expect-error expect(plugin({ a: 'a' })).toBeDefined(); }); + + it('should be able to depend on all compatible dependencies', () => { + const singleServiceRef = createServiceRef({ id: 'single' }); + const multiServiceRef = createServiceRef({ + id: 'multi', + multiton: true, + }); + + const plugin = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ + deps: { + single: singleServiceRef, + multi: multiServiceRef, + }, + async init({ single, multi }) { + const a: string = single; + const b: string[] = multi; + expect([a, b]).toBe('unused'); + }, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); + + it('should not be able to depend on extension points', () => { + const extensionPoint = createExtensionPoint({ id: 'point' }); + + const plugin = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ + deps: { + // @ts-expect-error + point: extensionPoint, + }, + async init() {}, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); }); diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 01ab8c2e3e..1e8c3910a3 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -36,6 +36,17 @@ export type ExtensionPoint = { $$type: '@backstage/ExtensionPoint'; }; +/** @ignore */ +type DepsToInstances< + TDeps extends { + [key in string]: ServiceRef | ExtensionPoint; + }, +> = { + [key in keyof TDeps]: TDeps[key] extends ServiceRef + ? Array + : TDeps[key]['T']; +}; + /** * The callbacks passed to the `register` method of a backend plugin. * @@ -46,11 +57,13 @@ export interface BackendPluginRegistrationPoints { ref: ExtensionPoint, impl: TExtensionPoint, ): void; - registerInit(options: { - deps: { - [name in keyof Deps]: ServiceRef; - }; - init(deps: Deps): Promise; + registerInit< + TDeps extends { + [name in string]: ServiceRef; + }, + >(options: { + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -64,11 +77,13 @@ export interface BackendModuleRegistrationPoints { ref: ExtensionPoint, impl: TExtensionPoint, ): void; - registerInit(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; + registerInit< + TDeps extends { + [name in string]: ServiceRef | ExtensionPoint; + }, + >(options: { + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } From 66523e8d01fbb57e35bcfd164c20b11ba7fcae26 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 30 Jul 2024 15:47:45 +0200 Subject: [PATCH 154/204] chore: wip Signed-off-by: blam --- packages/app-next/src/App.tsx | 48 ++++++- .../src/extensions/createPageExtension.tsx | 131 ++++++++++++++++++ plugins/home/src/alpha.tsx | 58 +++++++- 3 files changed, 223 insertions(+), 14 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index b57c680474..25d1aacf8f 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -20,6 +20,7 @@ import { pagesPlugin } from './examples/pagesPlugin'; import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin, { + extensions, titleExtensionDataRef, } from '@backstage/plugin-home/alpha'; @@ -74,13 +75,46 @@ TODO: /* app.tsx */ -const homePageExtension = createExtension({ - name: 'myhomepage', - attachTo: { id: 'page:home', input: 'props' }, - output: { - children: coreExtensionData.reactElement, - title: titleExtensionDataRef, - }, +// const homePageExtension = createExtension({ +// name: 'myhomepage', +// attachTo: { id: 'page:home', input: 'props' }, +// output: { +// children: coreExtensionData.reactElement, +// title: titleExtensionDataRef, +// }, +// factory() { +// return { children: homePage, title: 'just a title' }; +// }, +// }); + +// const homePage = createPageExtension({ +// defaultPath: '/home', +// routeRef: rootRouteRef, +// inputs: { +// props: createExtensionInput( +// { +// children: coreExtensionData.reactElement.optional(), +// title: titleExtensionDataRef.optional(), +// }, + +// { +// singleton: true, +// optional: true, +// }, +// ), +// }, +// loader: ({ inputs }) => +// import('./components/').then(m => +// compatWrapper( +// , +// ), +// ), +// }); + +const homePageExtension = extensions.homePage.override({ factory() { return { children: homePage, title: 'just a title' }; }, diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index cd434a3ffc..b10901f1bd 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -22,6 +22,9 @@ import { createExtension, ResolvedExtensionInputs, AnyExtensionInputMap, + createExtensionBlueprint, + ExtensionInput, + AnyExtensionDataRef, } from '../wiring'; import { RouteRef } from '../routing'; import { Expand } from '../types'; @@ -95,3 +98,131 @@ export function createPageExtension< }, }); } + +export function createNewPageExtension< + TConfig extends { path: string }, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, +>( + options: ( + | { + defaultPath: string; + } + | { + configSchema: PortableSchema; + } + ) & { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): ExtensionDefinition { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ path: z.string().default(options.defaultPath) }), + ) as PortableSchema); + + return createExtension({ + kind: 'page', + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo ?? { id: 'app/routes', input: 'routes' }, + configSchema, + inputs: options.inputs, + disabled: options.disabled, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + factory({ config, inputs, node }) { + const ExtensionComponent = lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + const outputs = [ + coreExtensionData.routePath(config.path), + coreExtensionData.reactElement( + + + , + ), + ]; + + if (options.routeRef) { + return [...outputs, coreExtensionData.routeRef(options.routeRef)]; + } + + return outputs; + }, + }); +} +/** + * A blueprint for creating extensions for routable React page components. + * @public + */ +export const PageExtensionBlueprint = createExtensionBlueprint({ + kind: 'page', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + config: { + schema: { + path: z => z.string().optional(), + }, + }, + factory( + { + defaultPath, + loader, + routeRef, + }: { + defaultPath?: string; + // TODO(blam) This type is impossible to type properly here as we don't have access + // to the input type generic. Maybe not a deal breaker though. + // It means we have to override the factory function in the `.make` method instead. + loader: (opts: unknown) => Promise; + routeRef?: RouteRef; + }, + { config, inputs, node }, + ) { + const ExtensionComponent = lazy(() => + loader({ config, inputs }).then(element => ({ default: () => element })), + ); + + // TODO(blam): this is a little awkward for optional returns. I wonder if we should be using generators or yield instead + // for a better API here. + const outputs = [ + coreExtensionData.routePath(config.path ?? defaultPath!), + coreExtensionData.reactElement( + + + , + ), + ]; + + if (routeRef) { + return [...outputs, coreExtensionData.routeRef(routeRef)]; + } + + return outputs; + }, +}); diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index b13c2a2e0e..109eea333e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -25,6 +25,10 @@ import { createRouteRef, } from '@backstage/frontend-plugin-api'; import { compatWrapper } from '@backstage/core-compat-api'; +import { + PageExtensionBlueprint, + createNewPageExtension, +} from '@backstage/frontend-plugin-api/src/extensions/createPageExtension'; const rootRouteRef = createRouteRef(); @@ -35,15 +39,15 @@ export const titleExtensionDataRef = createExtensionDataRef().with({ id: 'title', }); -const homePage = createPageExtension({ +const homePage = createNewPageExtension({ defaultPath: '/home', routeRef: rootRouteRef, inputs: { props: createExtensionInput( - { - children: coreExtensionData.reactElement.optional(), - title: titleExtensionDataRef.optional(), - }, + [ + coreExtensionData.reactElement.optional(), + titleExtensionDataRef.optional(), + ], { singleton: true, @@ -55,13 +59,53 @@ const homePage = createPageExtension({ import('./components/').then(m => compatWrapper( , ), ), }); +homePage.override({ + factory() {}, +}); + +// const homePage2 = PageExtensionBlueprint.make({ +// inputs: { +// props: createExtensionInput( +// [ +// coreExtensionData.reactElement.optional(), +// titleExtensionDataRef.optional(), +// ], +// { +// singleton: true, +// optional: true, +// }, +// ), +// }, +// factory(origFactory, { config, inputs, node }) { +// return origFactory({ +// defaultPath: '/home', +// routeRef: rootRouteRef, +// loader: () => +// import('./components/').then(m => +// compatWrapper( +// , +// ), +// }, +// }); + +/** + * @alpha + * This will not be the way to export extensions eventually, + * will be something like `homePlugin.extensions.homePage` or `homePlugin.extensions.get('page')` + * Just haven't worked out a nice way to fix the types yet + */ +export const extensions = { homePage }; + /** * @alpha */ From f30705fc23121030aa875f11b2d0bf12c04cf795 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 14:00:06 +0200 Subject: [PATCH 155/204] chore: some more exploration Signed-off-by: blam --- packages/app-next/src/App.tsx | 4 +- .../src/extensions/createPageExtension.tsx | 2 +- .../src/wiring/createExtension.ts | 94 ++++++++++++++++++- plugins/home/src/alpha.tsx | 4 - 4 files changed, 95 insertions(+), 9 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 25d1aacf8f..a33b2de8dc 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -115,8 +115,8 @@ TODO: // }); const homePageExtension = extensions.homePage.override({ - factory() { - return { children: homePage, title: 'just a title' }; + factory(origi) { + return origi(); }, }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index b10901f1bd..efe6ab54d9 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -127,7 +127,7 @@ export function createNewPageExtension< inputs: Expand>; }) => Promise; }, -): ExtensionDefinition { +) { const configSchema = 'configSchema' in options ? options.configSchema diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 2a29fe1ab4..6684323b51 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -20,6 +20,7 @@ import { Expand } from '../types'; import { AnyExtensionDataRef, ExtensionDataRef, + ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput'; @@ -234,6 +235,79 @@ export interface ExtensionDefinition { readonly configSchema?: PortableSchema; } +export type OverrideExtensionOptions< + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfig, + TConfigInput, + TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, +> = { + kind?: string; + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output: Array; + /** @deprecated - use `config.schema` instead */ + configSchema?: PortableSchema; + config?: { + schema: TConfigSchema; + }; + factory?( + originalFactory: (context?: { + config?: { + [key in keyof TConfigSchema]: z.infer>; + }; + inputs?: Expand>; + }) => Iterable>, + context: { + node: AppNode; + config: TConfig & + (string extends keyof TConfigSchema + ? {} + : { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }); + inputs: Expand>; + }, + ): Iterable; +} & VerifyExtensionFactoryOutput; + +/** @public */ +export type OverridableExtension< + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfig, + TConfigInput, + TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, +> = { + override( + options: OverrideExtensionOptions< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + UFactoryOutput + >, + ): ExtensionDefinition; +}; + /** @internal */ export type InternalExtensionDefinition = ExtensionDefinition & @@ -329,7 +403,15 @@ export function createExtension< [key in keyof TConfigSchema]: ReturnType; }> >) ->; +> & + OverridableExtension< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + UFactoryOutput + >; /** * @public * @deprecated - use the array format of `output` instead, see TODO-doc-link @@ -409,7 +491,15 @@ export function createExtension< [key in keyof TConfigSchema]: ReturnType; }> >) -> { +> & + OverridableExtension< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + UFactoryOutput + > { const newConfigSchema = options.config?.schema; if (newConfigSchema && options.configSchema) { throw new Error(`Cannot provide both configSchema and config.schema`); diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 109eea333e..31851726e5 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -66,10 +66,6 @@ const homePage = createNewPageExtension({ ), }); -homePage.override({ - factory() {}, -}); - // const homePage2 = PageExtensionBlueprint.make({ // inputs: { // props: createExtensionInput( From 665448b21fe97ed448894ddd226b850ad95b6647 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 15:01:22 +0200 Subject: [PATCH 156/204] chore: overriding of config seems to work in typescript Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 31 +++++++++++++++++++ .../src/wiring/createExtension.ts | 26 +++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index ab6a6c19a5..8da3504434 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -561,4 +561,35 @@ describe('createExtension', () => { }), ).toMatchObject({ version: 'v2' }); }); + + describe('overrides', () => { + it('should allow overriding of the default factory', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + config: { + schema: { + bar: z => z.string().optional(), + }, + }, + factory(_, { config }) { + return [stringDataRef(config.foo ?? config.bar ?? 'default')]; + }, + }); + + expect(true).toBe(true); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 6684323b51..f1db16b4d2 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -247,6 +247,10 @@ export type OverrideExtensionOptions< TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + TConfigSchemaOverrides extends { + [key: string]: (zImpl: typeof z) => z.ZodType; + }, + UOutputOverrides extends AnyExtensionDataRef = UOutput, > = { kind?: string; namespace?: string; @@ -254,11 +258,11 @@ export type OverrideExtensionOptions< attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; - output: Array; + output?: Array; /** @deprecated - use `config.schema` instead */ configSchema?: PortableSchema; config?: { - schema: TConfigSchema; + schema: TConfigSchemaOverrides; }; factory?( originalFactory: (context?: { @@ -273,6 +277,10 @@ export type OverrideExtensionOptions< (string extends keyof TConfigSchema ? {} : { + [key in keyof TConfigSchemaOverrides]: z.infer< + ReturnType + >; + } & { [key in keyof TConfigSchema]: z.infer< ReturnType >; @@ -280,7 +288,7 @@ export type OverrideExtensionOptions< inputs: Expand>; }, ): Iterable; -} & VerifyExtensionFactoryOutput; +} & VerifyExtensionFactoryOutput; /** @public */ export type OverridableExtension< @@ -296,14 +304,19 @@ export type OverridableExtension< TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, > = { - override( + override< + TConfigSchemaOverrides extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + >( options: OverrideExtensionOptions< UOutput, TInputs, TConfig, TConfigInput, TConfigSchema, - UFactoryOutput + UFactoryOutput, + TConfigSchemaOverrides >, ): ExtensionDefinition; }; @@ -540,6 +553,9 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, + override() { + // todo(blam): implement. + }, } as InternalExtensionDefinition< TConfig & (string extends keyof TConfigSchema From 40ffc63b7016cdc35d3a2c9d3ecdebf75b152916 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 15:30:33 +0200 Subject: [PATCH 157/204] chore: added some more overrides part Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 52 ++++++++++++++++++- .../src/wiring/createExtension.ts | 23 +++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 8da3504434..7f27df4947 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { coreExtensionData } from './coreExtensionData'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; @@ -563,7 +564,7 @@ describe('createExtension', () => { }); describe('overrides', () => { - it('should allow overriding of the default factory', () => { + it('should allow overriding of config and merging', () => { const testExtension = createExtension({ namespace: 'test', attachTo: { id: 'root', input: 'blob' }, @@ -591,5 +592,54 @@ describe('createExtension', () => { expect(true).toBe(true); }); + + it('should allow overriding of outputs', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + inputs: { + test: createExtensionInput([stringDataRef], { singleton: true }), + }, + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory({ inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + const override = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [ + numberDataRef(inputs.test.get(stringDataRef).length), + stringDataRef('default'), + ]; + }, + }); + + // @ts-expect-error - should fail as the string output from previous is not provided + const override2 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [numberDataRef(inputs.test.get(stringDataRef).length)]; + }, + }); + + // @ts-expect-error - this should fail because string output should be merged? + const override3 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + unused(override, override2, override3); + + expect(true).toBe(true); + }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index f1db16b4d2..3ec674f75f 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -246,11 +246,12 @@ export type OverrideExtensionOptions< TConfig, TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - UFactoryOutput extends ExtensionDataValue, + UOriginalFactoryOutput extends ExtensionDataValue, TConfigSchemaOverrides extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, - UOutputOverrides extends AnyExtensionDataRef = UOutput, + UOutputOverrides extends AnyExtensionDataRef, + UFactoryOverrideOutput extends ExtensionDataValue, > = { kind?: string; namespace?: string; @@ -262,7 +263,10 @@ export type OverrideExtensionOptions< /** @deprecated - use `config.schema` instead */ configSchema?: PortableSchema; config?: { - schema: TConfigSchemaOverrides; + schema: TConfigSchemaOverrides & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; }; factory?( originalFactory: (context?: { @@ -270,7 +274,7 @@ export type OverrideExtensionOptions< [key in keyof TConfigSchema]: z.infer>; }; inputs?: Expand>; - }) => Iterable>, + }) => Iterable, context: { node: AppNode; config: TConfig & @@ -287,8 +291,9 @@ export type OverrideExtensionOptions< }); inputs: Expand>; }, - ): Iterable; -} & VerifyExtensionFactoryOutput; + ): Iterable; + // todo(blam): need to verify that the outputs are meged properly. +} & VerifyExtensionFactoryOutput; /** @public */ export type OverridableExtension< @@ -308,6 +313,8 @@ export type OverridableExtension< TConfigSchemaOverrides extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, + UOutputOverrides extends AnyExtensionDataRef, + UFactoryOverrideOutput extends ExtensionDataValue, >( options: OverrideExtensionOptions< UOutput, @@ -316,7 +323,9 @@ export type OverridableExtension< TConfigInput, TConfigSchema, UFactoryOutput, - TConfigSchemaOverrides + TConfigSchemaOverrides, + UOutputOverrides, + UFactoryOverrideOutput >, ): ExtensionDefinition; }; From 33786b586e08ef27cea7beb4a1c6062bacc4fb89 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 15:33:44 +0200 Subject: [PATCH 158/204] chore: reset some things Signed-off-by: blam --- packages/app-next/src/App.tsx | 52 +++---------- .../src/extensions/createPageExtension.tsx | 78 +------------------ plugins/home/src/alpha.tsx | 54 ++----------- 3 files changed, 18 insertions(+), 166 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index a33b2de8dc..b57c680474 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -20,7 +20,6 @@ import { pagesPlugin } from './examples/pagesPlugin'; import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin, { - extensions, titleExtensionDataRef, } from '@backstage/plugin-home/alpha'; @@ -75,48 +74,15 @@ TODO: /* app.tsx */ -// const homePageExtension = createExtension({ -// name: 'myhomepage', -// attachTo: { id: 'page:home', input: 'props' }, -// output: { -// children: coreExtensionData.reactElement, -// title: titleExtensionDataRef, -// }, -// factory() { -// return { children: homePage, title: 'just a title' }; -// }, -// }); - -// const homePage = createPageExtension({ -// defaultPath: '/home', -// routeRef: rootRouteRef, -// inputs: { -// props: createExtensionInput( -// { -// children: coreExtensionData.reactElement.optional(), -// title: titleExtensionDataRef.optional(), -// }, - -// { -// singleton: true, -// optional: true, -// }, -// ), -// }, -// loader: ({ inputs }) => -// import('./components/').then(m => -// compatWrapper( -// , -// ), -// ), -// }); - -const homePageExtension = extensions.homePage.override({ - factory(origi) { - return origi(); +const homePageExtension = createExtension({ + name: 'myhomepage', + attachTo: { id: 'page:home', input: 'props' }, + output: { + children: coreExtensionData.reactElement, + title: titleExtensionDataRef, + }, + factory() { + return { children: homePage, title: 'just a title' }; }, }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index efe6ab54d9..27b6b53b63 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -23,8 +23,6 @@ import { ResolvedExtensionInputs, AnyExtensionInputMap, createExtensionBlueprint, - ExtensionInput, - AnyExtensionDataRef, } from '../wiring'; import { RouteRef } from '../routing'; import { Expand } from '../types'; @@ -99,79 +97,6 @@ export function createPageExtension< }); } -export function createNewPageExtension< - TConfig extends { path: string }, - TInputs extends { - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, ->( - options: ( - | { - defaultPath: string; - } - | { - configSchema: PortableSchema; - } - ) & { - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - routeRef?: RouteRef; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; - }, -) { - const configSchema = - 'configSchema' in options - ? options.configSchema - : (createSchemaFromZod(z => - z.object({ path: z.string().default(options.defaultPath) }), - ) as PortableSchema); - - return createExtension({ - kind: 'page', - namespace: options.namespace, - name: options.name, - attachTo: options.attachTo ?? { id: 'app/routes', input: 'routes' }, - configSchema, - inputs: options.inputs, - disabled: options.disabled, - output: [ - coreExtensionData.routePath, - coreExtensionData.reactElement, - coreExtensionData.routeRef.optional(), - ], - factory({ config, inputs, node }) { - const ExtensionComponent = lazy(() => - options - .loader({ config, inputs }) - .then(element => ({ default: () => element })), - ); - - const outputs = [ - coreExtensionData.routePath(config.path), - coreExtensionData.reactElement( - - - , - ), - ]; - - if (options.routeRef) { - return [...outputs, coreExtensionData.routeRef(options.routeRef)]; - } - - return outputs; - }, - }); -} /** * A blueprint for creating extensions for routable React page components. * @public @@ -208,7 +133,8 @@ export const PageExtensionBlueprint = createExtensionBlueprint({ loader({ config, inputs }).then(element => ({ default: () => element })), ); - // TODO(blam): this is a little awkward for optional returns. I wonder if we should be using generators or yield instead + // TODO(blam): this is a little awkward for optional returns. + // I wonder if we should be using generators or yield instead // for a better API here. const outputs = [ coreExtensionData.routePath(config.path ?? defaultPath!), diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 31851726e5..b13c2a2e0e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -25,10 +25,6 @@ import { createRouteRef, } from '@backstage/frontend-plugin-api'; import { compatWrapper } from '@backstage/core-compat-api'; -import { - PageExtensionBlueprint, - createNewPageExtension, -} from '@backstage/frontend-plugin-api/src/extensions/createPageExtension'; const rootRouteRef = createRouteRef(); @@ -39,15 +35,15 @@ export const titleExtensionDataRef = createExtensionDataRef().with({ id: 'title', }); -const homePage = createNewPageExtension({ +const homePage = createPageExtension({ defaultPath: '/home', routeRef: rootRouteRef, inputs: { props: createExtensionInput( - [ - coreExtensionData.reactElement.optional(), - titleExtensionDataRef.optional(), - ], + { + children: coreExtensionData.reactElement.optional(), + title: titleExtensionDataRef.optional(), + }, { singleton: true, @@ -59,49 +55,13 @@ const homePage = createNewPageExtension({ import('./components/').then(m => compatWrapper( , ), ), }); -// const homePage2 = PageExtensionBlueprint.make({ -// inputs: { -// props: createExtensionInput( -// [ -// coreExtensionData.reactElement.optional(), -// titleExtensionDataRef.optional(), -// ], -// { -// singleton: true, -// optional: true, -// }, -// ), -// }, -// factory(origFactory, { config, inputs, node }) { -// return origFactory({ -// defaultPath: '/home', -// routeRef: rootRouteRef, -// loader: () => -// import('./components/').then(m => -// compatWrapper( -// , -// ), -// }, -// }); - -/** - * @alpha - * This will not be the way to export extensions eventually, - * will be something like `homePlugin.extensions.homePage` or `homePlugin.extensions.get('page')` - * Just haven't worked out a nice way to fix the types yet - */ -export const extensions = { homePage }; - /** * @alpha */ From 3ac12d718954a906c1d07f66d7d894cd3978059c Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 31 Jul 2024 17:02:53 +0200 Subject: [PATCH 159/204] chore: some more tests and comments Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 30 ++++++++++++++++++- .../src/wiring/createExtension.ts | 9 ++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 7f27df4947..e38a24cebe 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -599,7 +599,9 @@ describe('createExtension', () => { attachTo: { id: 'root', input: 'blob' }, output: [stringDataRef], inputs: { - test: createExtensionInput([stringDataRef], { singleton: true }), + test: createExtensionInput([stringDataRef], { + singleton: true, + }), }, config: { schema: { @@ -641,5 +643,31 @@ describe('createExtension', () => { expect(true).toBe(true); }); + + it('should allow overriding the factory function and calling the original factory', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + factory(originalFactory) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + return [stringDataRef(`foo-${foo}-override`)]; + }, + }); + }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 3ec674f75f..678ce4e042 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -274,6 +274,8 @@ export type OverrideExtensionOptions< [key in keyof TConfigSchema]: z.infer>; }; inputs?: Expand>; + // todo(blam): Think this is better as a DataContainer instead + // should probably update that everywhere. }) => Iterable, context: { node: AppNode; @@ -292,8 +294,11 @@ export type OverrideExtensionOptions< inputs: Expand>; }, ): Iterable; - // todo(blam): need to verify that the outputs are meged properly. -} & VerifyExtensionFactoryOutput; + // todo(blam): need to verify that the outputs are merged and verified properly. +} & VerifyExtensionFactoryOutput< + UOutput & UFactoryOverrideOutput, + UFactoryOverrideOutput +>; /** @public */ export type OverridableExtension< From 45eb5b437a690b6a0d33fea3174d53ed02f88380 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 09:41:40 +0200 Subject: [PATCH 160/204] chore: got the data container working nicely Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 3 ++ .../src/wiring/createExtension.ts | 28 +++---------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index e38a24cebe..c688f92004 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -665,6 +665,9 @@ describe('createExtension', () => { const foo: string = response.get(stringDataRef); + // @ts-expect-error - fails because original factory does not return number + const number: boolean = response.get(numberDataRef); + return [stringDataRef(`foo-${foo}-override`)]; }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 678ce4e042..87a4342df7 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -20,7 +20,6 @@ import { Expand } from '../types'; import { AnyExtensionDataRef, ExtensionDataRef, - ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput'; @@ -246,7 +245,6 @@ export type OverrideExtensionOptions< TConfig, TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - UOriginalFactoryOutput extends ExtensionDataValue, TConfigSchemaOverrides extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, @@ -274,9 +272,7 @@ export type OverrideExtensionOptions< [key in keyof TConfigSchema]: z.infer>; }; inputs?: Expand>; - // todo(blam): Think this is better as a DataContainer instead - // should probably update that everywhere. - }) => Iterable, + }) => ExtensionDataContainer, context: { node: AppNode; config: TConfig & @@ -296,7 +292,7 @@ export type OverrideExtensionOptions< ): Iterable; // todo(blam): need to verify that the outputs are merged and verified properly. } & VerifyExtensionFactoryOutput< - UOutput & UFactoryOverrideOutput, + UOutput & UOutputOverrides, UFactoryOverrideOutput >; @@ -312,7 +308,6 @@ export type OverridableExtension< TConfig, TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - UFactoryOutput extends ExtensionDataValue, > = { override< TConfigSchemaOverrides extends { @@ -327,7 +322,6 @@ export type OverridableExtension< TConfig, TConfigInput, TConfigSchema, - UFactoryOutput, TConfigSchemaOverrides, UOutputOverrides, UFactoryOverrideOutput @@ -431,14 +425,7 @@ export function createExtension< }> >) > & - OverridableExtension< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - UFactoryOutput - >; + OverridableExtension; /** * @public * @deprecated - use the array format of `output` instead, see TODO-doc-link @@ -519,14 +506,7 @@ export function createExtension< }> >) > & - OverridableExtension< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - UFactoryOutput - > { + OverridableExtension { const newConfigSchema = options.config?.schema; if (newConfigSchema && options.configSchema) { throw new Error(`Cannot provide both configSchema and config.schema`); From 8d631773468d3d852335d918d7f43599be1d2a56 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 11:07:38 +0200 Subject: [PATCH 161/204] chore: added some tests Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 37 ++++++++ .../src/wiring/createExtension.ts | 86 ++++++++++++++++++- .../wiring/createExtensionOverrides.test.ts | 3 + 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index c688f92004..432fb7f89e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { createExtensionTester } from '@backstage/frontend-test-utils'; import { coreExtensionData } from './coreExtensionData'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; @@ -671,6 +672,42 @@ describe('createExtension', () => { return [stringDataRef(`foo-${foo}-override`)]; }, }); + + expect(true).toBe(true); + }); + + it('should work functionally with overrides', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo)]; + }, + }); + + const overriden = testExtension.override({ + config: { + schema: { + bar: z => z.string().optional(), + }, + }, + factory(originalFactory, { config }) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + return [stringDataRef(`foo-${foo}-override-${config.bar}`)]; + }, + }); + + const tester = createExtensionTester(overriden).render(); }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 87a4342df7..200d3d1133 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { over } from 'lodash'; import { AppNode } from '../apis'; import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; @@ -547,8 +548,89 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - override() { - // todo(blam): implement. + override< + TConfigSchemaOverrides extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UOutputOverrides extends AnyExtensionDataRef, + UFactoryOverrideOutput extends ExtensionDataValue, + >( + overrideOptions: OverrideExtensionOptions< + UOutput, + TInputs, + TConfig, + TConfigInput, + TConfigSchema, + TConfigSchemaOverrides, + UOutputOverrides, + UFactoryOverrideOutput + >, + ) { + const overrideNewConfigSchema = overrideOptions.config?.schema; + if (overrideNewConfigSchema && overrideOptions.configSchema) { + throw new Error(`Cannot provide both configSchema and config.schema`); + } + + const mergedConfigSchema = { + ...options.config?.schema, + ...overrideOptions.config?.schema, + }; + + const overrideConfigSchema = mergedConfigSchema + ? createSchemaFromZod(innerZ => + innerZ.object( + Object.fromEntries( + Object.entries(mergedConfigSchema).map(([k, v]) => [ + k, + v(innerZ), + ]), + ), + ), + ) + : overrideOptions.configSchema; + + const buildDataContainer = (outputs): ExtensionDataContainer => { + const dataMap = new Map(); + if (Symbol.iterator in outputs) { + for (const output of outputs) { + dataMap.set(output.id, output.value); + } + } + + return { + get(ref) { + return dataMap.get(ref.id); + }, + } as ExtensionDataContainer; + }; + + return createExtension({ + attachTo: options.attachTo, + output: overrideOptions.output ?? options.output, + configSchema: overrideConfigSchema, + factory: ({ node, config, inputs }) => { + if (overrideOptions.factory) { + return overrideOptions.factory( + innerCtx => { + const originalFactoryResponse = options.factory({ + node, + config: innerCtx?.config ?? config, + inputs: innerCtx?.inputs ?? inputs, + }); + + return buildDataContainer(originalFactoryResponse); + }, + { node, config, inputs }, + ); + } + return options.factory(originalFactory, context); + }, + disabled: overrideOptions.disabled ?? options.disabled, + inputs: overrideOptions.inputs ?? options.inputs, + kind: overrideOptions.kind ?? options.kind, + name: overrideOptions.name ?? options.name, + namespace: overrideOptions.namespace ?? options.namespace, + }); }, } as InternalExtensionDefinition< TConfig & diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts index aa00247222..555cd82d4c 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts @@ -75,6 +75,7 @@ describe('createExtensionOverrides', () => { "id": "a", "inputs": {}, "output": {}, + "override": [Function], "toString": [Function], "version": "v1", }, @@ -90,6 +91,7 @@ describe('createExtensionOverrides', () => { "id": "b", "inputs": {}, "output": {}, + "override": [Function], "toString": [Function], "version": "v1", }, @@ -105,6 +107,7 @@ describe('createExtensionOverrides', () => { "id": "k:c/n", "inputs": {}, "output": {}, + "override": [Function], "toString": [Function], "version": "v1", }, From 4f446c44f627f2081bd373727a8defef8f2113fa Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 12:06:16 +0200 Subject: [PATCH 162/204] chore: test with the extension tester Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 432fb7f89e..6aa172cc5a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -695,7 +695,7 @@ describe('createExtension', () => { const overriden = testExtension.override({ config: { schema: { - bar: z => z.string().optional(), + bar: z => z.string().default('hello'), }, }, factory(originalFactory, { config }) { @@ -707,7 +707,15 @@ describe('createExtension', () => { }, }); - const tester = createExtensionTester(overriden).render(); + expect(createExtensionTester(overriden).data(stringDataRef)).toBe( + 'foo-boom-override-hello', + ); + + expect( + createExtensionTester(overriden, { + config: { foo: 'hello', bar: 'world' }, + }).data(stringDataRef), + ).toBe('foo-hello-override-world'); }); }); }); From 4a6d9dc85e2042f854d7999e1a08e98ddc43ad53 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 13:46:57 +0200 Subject: [PATCH 163/204] chore: reset page extension Signed-off-by: blam --- .../src/extensions/createPageExtension.tsx | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 27b6b53b63..cd434a3ffc 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -22,7 +22,6 @@ import { createExtension, ResolvedExtensionInputs, AnyExtensionInputMap, - createExtensionBlueprint, } from '../wiring'; import { RouteRef } from '../routing'; import { Expand } from '../types'; @@ -96,59 +95,3 @@ export function createPageExtension< }, }); } - -/** - * A blueprint for creating extensions for routable React page components. - * @public - */ -export const PageExtensionBlueprint = createExtensionBlueprint({ - kind: 'page', - attachTo: { id: 'app/routes', input: 'routes' }, - output: [ - coreExtensionData.routePath, - coreExtensionData.reactElement, - coreExtensionData.routeRef.optional(), - ], - config: { - schema: { - path: z => z.string().optional(), - }, - }, - factory( - { - defaultPath, - loader, - routeRef, - }: { - defaultPath?: string; - // TODO(blam) This type is impossible to type properly here as we don't have access - // to the input type generic. Maybe not a deal breaker though. - // It means we have to override the factory function in the `.make` method instead. - loader: (opts: unknown) => Promise; - routeRef?: RouteRef; - }, - { config, inputs, node }, - ) { - const ExtensionComponent = lazy(() => - loader({ config, inputs }).then(element => ({ default: () => element })), - ); - - // TODO(blam): this is a little awkward for optional returns. - // I wonder if we should be using generators or yield instead - // for a better API here. - const outputs = [ - coreExtensionData.routePath(config.path ?? defaultPath!), - coreExtensionData.reactElement( - - - , - ), - ]; - - if (routeRef) { - return [...outputs, coreExtensionData.routeRef(routeRef)]; - } - - return outputs; - }, -}); From 8e185e43ce5d4d99369d45358393f5d4d2ff1908 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Wed, 7 Aug 2024 15:04:20 +0200 Subject: [PATCH 164/204] Add Notifications to documentation sidebar Signed-off-by: Marek Libra --- docs/notifications/{getting-started.md => index.md} | 4 ++-- microsite/sidebars.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) rename docs/notifications/{getting-started.md => index.md} (98%) diff --git a/docs/notifications/getting-started.md b/docs/notifications/index.md similarity index 98% rename from docs/notifications/getting-started.md rename to docs/notifications/index.md index dc006e21b2..95d77a9b7f 100644 --- a/docs/notifications/getting-started.md +++ b/docs/notifications/index.md @@ -1,5 +1,5 @@ --- -id: getting-started +id: index title: Getting Started description: How to get started with the notifications and signals --- @@ -203,7 +203,7 @@ The use of signals with notifications is optional but generally enhances user ex When a notification is created, a new signal is emitted to a general-purpose message bus to announce it to subscribed listeners. -The frontend maintains a persistent connection (web socket) to receive these announcements from the notifications channel. +The frontend maintains a persistent connection (WebSocket) to receive these announcements from the notifications channel. The specific details of the updated or created notification should be retrieved via a request to the notifications API, except for new notifications, where the payload is included in the signal for performance reasons. In a frontend plugin, to subscribe for notifications' signals: diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 075025afbb..71d0257dc7 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -295,6 +295,7 @@ "conf/writing", "conf/defining" ], + "Notifications": ["notifications/index"], "Auth and identity": [ "auth/index", { From d0e441e1b88c22b04f92270d9ceb6235ef920085 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 7 Aug 2024 09:30:12 -0500 Subject: [PATCH 165/204] Added missing `@internal` scope to plugin modules doc Signed-off-by: Andre Wanlin --- docs/backend-system/architecture/06-modules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backend-system/architecture/06-modules.md b/docs/backend-system/architecture/06-modules.md index eb00e737e3..010f36109c 100644 --- a/docs/backend-system/architecture/06-modules.md +++ b/docs/backend-system/architecture/06-modules.md @@ -52,7 +52,7 @@ This allows you to install the module in your backend instance by just referenci ```ts backend.add( - import('backstage-plugin-catalog-backend-module-example-processor'), + import('@internal/backstage-plugin-catalog-backend-module-example-processor'), ); ``` From 9fdbbd0889ec6af08e21b4dc7092cedbe7fac3bf Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 17:11:24 +0200 Subject: [PATCH 166/204] chore: more work towards supporting extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- .../src/extensions/createApiExtension.test.ts | 2 + .../createAppRootElementExtension.test.tsx | 2 + .../createAppRootWrapperExtension.test.tsx | 2 + .../createNavLogoExtension.test.tsx | 1 + .../extensions/createPageExtension.test.tsx | 3 + .../extensions/createRouterExtension.test.tsx | 2 + .../createTranslationExtension.test.ts | 3 + .../src/wiring/createExtension.test.ts | 28 +- .../src/wiring/createExtension.ts | 467 +++++++++--------- .../wiring/createExtensionBlueprint.test.tsx | 3 + .../src/wiring/createExtensionBlueprint.ts | 17 +- 11 files changed, 250 insertions(+), 280 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts index c4d0239094..8f8a0cd9ac 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -48,6 +48,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); @@ -85,6 +86,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx index c8bf6ee355..6e80f6fe13 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx @@ -41,6 +41,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension).render(); @@ -88,6 +89,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension, { config: { name: 'Robin' } }) diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx index d21cfa7583..993c4fdee3 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx @@ -42,6 +42,7 @@ describe('createAppRootWrapperExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester( @@ -94,6 +95,7 @@ describe('createAppRootWrapperExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx index 8c54743f1d..17187a339d 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx @@ -41,6 +41,7 @@ describe('createNavLogoExtension', () => { logos: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 374400fce8..e5dd4027c0 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -56,6 +56,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); expect( @@ -78,6 +79,7 @@ describe('createPageExtension', () => { kind: 'page', attachTo: { id: 'other', input: 'place' }, configSchema: expect.anything(), + override: expect.any(Function), disabled: true, inputs: { first: createExtensionInput({ @@ -115,6 +117,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx index 7109e1a8aa..2c16076fcf 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx @@ -50,6 +50,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); @@ -119,6 +120,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts index 975901b8e5..019c565cbd 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts @@ -46,6 +46,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -84,6 +85,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -120,6 +122,7 @@ describe('createTranslationExtension', () => { version: 'v1', kind: 'translation', namespace: 'test', + override: expect.any(Function), name: 'sv', attachTo: { id: 'app', input: 'translations' }, disabled: false, diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 6aa172cc5a..30375049bf 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -15,7 +15,6 @@ */ import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { coreExtensionData } from './coreExtensionData'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; @@ -306,9 +305,7 @@ describe('createExtension', () => { baz: z => z.string().optional(), }, }, - output: { - foo: stringDataRef, - }, + output: [stringDataRef], factory({ config }) { const a1: string = config.foo; const a2: string = config.bar; @@ -322,12 +319,10 @@ describe('createExtension', () => { const c3: number = config.baz; unused(a1, a2, a3, c1, c2, c3); - return { - foo: 'bar', - }; + return [stringDataRef('bar')]; }, }); - expect(extension).toMatchObject({ version: 'v1', namespace: 'test' }); + expect(extension).toMatchObject({ version: 'v2', namespace: 'test' }); expect(String(extension)).toBe( 'ExtensionDefinition{namespace=test,attachTo=root@default}', ); @@ -614,18 +609,7 @@ describe('createExtension', () => { }, }); - const override = testExtension.override({ - output: [numberDataRef], - factory(_, { inputs }) { - return [ - numberDataRef(inputs.test.get(stringDataRef).length), - stringDataRef('default'), - ]; - }, - }); - - // @ts-expect-error - should fail as the string output from previous is not provided - const override2 = testExtension.override({ + const override1 = testExtension.override({ output: [numberDataRef], factory(_, { inputs }) { return [numberDataRef(inputs.test.get(stringDataRef).length)]; @@ -633,14 +617,14 @@ describe('createExtension', () => { }); // @ts-expect-error - this should fail because string output should be merged? - const override3 = testExtension.override({ + const override2 = testExtension.override({ output: [numberDataRef], factory(_, { inputs }) { return [stringDataRef(inputs.test.get(stringDataRef))]; }, }); - unused(override, override2, override3); + unused(override1, override2); expect(true).toBe(true); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 200d3d1133..6e75fc76e5 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { over } from 'lodash'; import { AppNode } from '../apis'; import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; +import { createDataContainer } from './createExtensionBlueprint'; import { AnyExtensionDataRef, ExtensionDataRef, @@ -128,7 +128,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, > { kind?: string; namespace?: string; @@ -137,21 +136,10 @@ export interface LegacyCreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: TOutput; - /** @deprecated - use `config.schema` instead */ configSchema?: PortableSchema; - config?: { - schema: TConfigSchema; - }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: TConfig; inputs: Expand>; }): Expand>; } @@ -193,8 +181,6 @@ export type CreateExtensionOptions< { optional: boolean; singleton: boolean } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, > = { @@ -205,27 +191,30 @@ export type CreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: Array; - /** @deprecated - use `config.schema` instead */ - configSchema?: PortableSchema; config?: { schema: TConfigSchema; }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: { + [key in keyof TConfigSchema]: z.infer>; + }; inputs: Expand>; }): Iterable; } & VerifyExtensionFactoryOutput; /** @public */ -export interface ExtensionDefinition { +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, +> { $$type: '@backstage/ExtensionDefinition'; readonly kind?: string; readonly namespace?: string; @@ -233,102 +222,71 @@ export interface ExtensionDefinition { readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema; -} -export type OverrideExtensionOptions< - UOutput extends AnyExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, - TConfig, - TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, - TConfigSchemaOverrides extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, - UOutputOverrides extends AnyExtensionDataRef, - UFactoryOverrideOutput extends ExtensionDataValue, -> = { - kind?: string; - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - output?: Array; - /** @deprecated - use `config.schema` instead */ - configSchema?: PortableSchema; - config?: { - schema: TConfigSchemaOverrides & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; - }; - }; - factory?( - originalFactory: (context?: { - config?: { - [key in keyof TConfigSchema]: z.infer>; - }; - inputs?: Expand>; - }) => ExtensionDataContainer, - context: { - node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchemaOverrides]: z.infer< - ReturnType - >; - } & { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); - inputs: Expand>; - }, - ): Iterable; - // todo(blam): need to verify that the outputs are merged and verified properly. -} & VerifyExtensionFactoryOutput< - UOutput & UOutputOverrides, - UFactoryOverrideOutput ->; - -/** @public */ -export type OverridableExtension< - UOutput extends AnyExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, - TConfig, - TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, -> = { override< - TConfigSchemaOverrides extends { + TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, - UOutputOverrides extends AnyExtensionDataRef, - UFactoryOverrideOutput extends ExtensionDataValue, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, >( - options: OverrideExtensionOptions< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - TConfigSchemaOverrides, - UOutputOverrides, - UFactoryOverrideOutput + args: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput >, - ): ExtensionDefinition; -}; + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs + >; +} /** @internal */ export type InternalExtensionDefinition = @@ -397,36 +355,27 @@ export function createExtension< { optional: boolean; singleton: boolean } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, >( options: CreateExtensionOptions< UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput >, ): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) -> & - OverridableExtension; + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs +>; /** * @public * @deprecated - use the array format of `output` instead, see TODO-doc-link @@ -436,31 +385,14 @@ export function createExtension< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, >( options: LegacyCreateExtensionOptions< TOutput, TInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, -): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) ->; +): ExtensionDefinition; export function createExtension< UOutput extends AnyExtensionDataRef, TInputs extends { @@ -476,20 +408,12 @@ export function createExtension< UFactoryOutput extends ExtensionDataValue, >( options: - | CreateExtensionOptions< - UOutput, - TInputs, - TConfig, - TConfigInput, - TConfigSchema, - UFactoryOutput - > + | CreateExtensionOptions | LegacyCreateExtensionOptions< AnyExtensionDataMap, TLegacyInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, ): ExtensionDefinition< TConfig & @@ -505,22 +429,29 @@ export function createExtension< z.ZodObject<{ [key in keyof TConfigSchema]: ReturnType; }> - >) -> & - OverridableExtension { - const newConfigSchema = options.config?.schema; - if (newConfigSchema && options.configSchema) { + >), + UOutput, + TInputs +> { + if ('configSchema' in options && 'config' in options) { throw new Error(`Cannot provide both configSchema and config.schema`); } - const configSchema = newConfigSchema - ? createSchemaFromZod(innerZ => + let configSchema: PortableSchema | undefined; + if ('configSchema' in options) { + configSchema = options.configSchema; + } + if ('config' in options) { + const newConfigSchema = options.config?.schema; + configSchema = + newConfigSchema && + createSchemaFromZod(innerZ => innerZ.object( Object.fromEntries( Object.entries(newConfigSchema).map(([k, v]) => [k, v(innerZ)]), ), ), - ) - : options.configSchema; + ); + } return { $$type: '@backstage/ExtensionDefinition', @@ -548,89 +479,133 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - override< - TConfigSchemaOverrides extends { + override: < + TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, - UOutputOverrides extends AnyExtensionDataRef, - UFactoryOverrideOutput extends ExtensionDataValue, - >( - overrideOptions: OverrideExtensionOptions< + UOverrideFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >(overrideOptions: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs; + output?: Array; + config?: { + schema: TExtensionConfigSchema; + }; + factory( + originalFactory: (context?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + }): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject< + { + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + } & { + [key in keyof TConfigSchema]: ReturnType; + } + > + >, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs + > => { + if (!Array.isArray(options.output)) { + throw new Error( + 'Cannot override an extension that is not declared using the new format with outputs as an array', + ); + } + const newOptions = options as CreateExtensionOptions< UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, - TConfigSchemaOverrides, - UOutputOverrides, - UFactoryOverrideOutput - >, - ) { + UFactoryOutput + >; const overrideNewConfigSchema = overrideOptions.config?.schema; - if (overrideNewConfigSchema && overrideOptions.configSchema) { - throw new Error(`Cannot provide both configSchema and config.schema`); - } - const mergedConfigSchema = { - ...options.config?.schema, - ...overrideOptions.config?.schema, - }; - - const overrideConfigSchema = mergedConfigSchema - ? createSchemaFromZod(innerZ => - innerZ.object( - Object.fromEntries( - Object.entries(mergedConfigSchema).map(([k, v]) => [ - k, - v(innerZ), - ]), - ), - ), - ) - : overrideOptions.configSchema; - - const buildDataContainer = (outputs): ExtensionDataContainer => { - const dataMap = new Map(); - if (Symbol.iterator in outputs) { - for (const output of outputs) { - dataMap.set(output.id, output.value); - } - } - - return { - get(ref) { - return dataMap.get(ref.id); - }, - } as ExtensionDataContainer; - }; + const schema = { + ...newOptions.config?.schema, + ...overrideNewConfigSchema, + } as TConfigSchema & TExtensionConfigSchema; return createExtension({ - attachTo: options.attachTo, - output: overrideOptions.output ?? options.output, - configSchema: overrideConfigSchema, + kind: newOptions.kind, + namespace: newOptions.namespace, + name: newOptions.name, + attachTo: overrideOptions.attachTo ?? newOptions.attachTo, + disabled: overrideOptions.disabled ?? newOptions.disabled, + inputs: { ...overrideOptions.inputs, ...newOptions.inputs }, + output: overrideOptions.output ?? newOptions.output, + config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { - if (overrideOptions.factory) { - return overrideOptions.factory( - innerCtx => { - const originalFactoryResponse = options.factory({ - node, - config: innerCtx?.config ?? config, - inputs: innerCtx?.inputs ?? inputs, - }); - - return buildDataContainer(originalFactoryResponse); - }, - { node, config, inputs }, - ); + if (!overrideOptions.factory) { + return newOptions.factory({ + node, + config, + inputs: inputs as unknown as Expand< + ResolvedExtensionInputs + >, + }); } - return options.factory(originalFactory, context); + return overrideOptions.factory( + (innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }): ExtensionDataContainer => { + return createDataContainer( + newOptions.factory({ + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }) as Iterable, + ); + }, + { + node, + config, + inputs, + }, + ); }, - disabled: overrideOptions.disabled ?? options.disabled, - inputs: overrideOptions.inputs ?? options.inputs, - kind: overrideOptions.kind ?? options.kind, - name: overrideOptions.name ?? options.name, - namespace: overrideOptions.namespace ?? options.namespace, - }); + } as CreateExtensionOptions); }, } as InternalExtensionDefinition< TConfig & diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 66c3939a96..924eba2cba 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -69,6 +69,7 @@ describe('createExtensionBlueprint', () => { output: [coreExtensionData.reactElement], factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), version: 'v2', }); @@ -108,6 +109,7 @@ describe('createExtensionBlueprint', () => { output: [coreExtensionData.reactElement], factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), version: 'v2', }); @@ -389,6 +391,7 @@ describe('createExtensionBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 88b2523111..a8bf42c7f8 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -171,7 +171,7 @@ export interface ExtensionBlueprint< } /** @internal */ -function createDataContainer( +export function createDataContainer( values: Iterable< UData extends ExtensionDataRef ? ExtensionDataValue @@ -241,10 +241,7 @@ class ExtensionBlueprintImpl< name?: string; attachTo?: { id: string; input: string }; disabled?: boolean; - inputs?: TExtraInputs & { - [KName in keyof TInputs]?: `Error: Input '${KName & - string}' is already defined in parent definition`; - }; + inputs?: TExtraInputs; output?: Array; params?: TParams; config?: { @@ -363,9 +360,7 @@ class ExtensionBlueprintImpl< } throw new Error('Either params or factory must be provided'); }, - } as CreateExtensionOptions< - UOutput, - TInputs & TExtraInputs, + } as CreateExtensionOptions) as ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -383,10 +378,8 @@ class ExtensionBlueprintImpl< [key in keyof TConfigSchema]: ReturnType; } > - >, - TConfigSchema, - UFactoryOutput - >); + > + >; } } From feb5fc73c386997e014714c22f6ceacbaa3d2b01 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 17:30:47 +0200 Subject: [PATCH 167/204] chore: fix tests and update api-reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- .../src/tree/instantiateAppNodeTree.test.ts | 14 +- .../src/tree/resolveAppNodeSpecs.test.ts | 1 + packages/frontend-plugin-api/api-report.md | 180 +++++++++++------- .../wiring/createExtensionOverrides.test.ts | 3 - .../wiring/resolveExtensionDefinition.test.ts | 2 + .../src/wiring/resolveExtensionDefinition.ts | 8 +- .../src/app/createExtensionTester.tsx | 2 +- plugins/catalog-react/api-report-alpha.md | 6 +- plugins/catalog/api-report-alpha.md | 2 +- plugins/search-react/api-report-alpha.md | 2 +- plugins/search/api-report-alpha.md | 11 +- plugins/techdocs/api-report-alpha.md | 4 +- plugins/user-settings/api-report-alpha.md | 4 +- 13 files changed, 146 insertions(+), 93 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index de8889daf0..aded5d7826 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -68,7 +68,7 @@ function makeNode( function makeInstanceWithId( extension: Extension, - config?: TConfig, + config?: TConfigInput, ): AppNode { const node = makeNode(extension, { config }); return { @@ -640,12 +640,12 @@ describe('instantiateAppNodeTree', () => { name: 'test', attachTo: { id: 'ignored', input: 'ignored' }, output: [testDataRef, otherDataRef.optional()], - configSchema: createSchemaFromZod(z => - z.object({ - output: z.string().default('test'), - other: z.number().optional(), - }), - ), + config: { + schema: { + output: z => z.string().default('test'), + other: z => z.number().optional(), + }, + }, factory({ config }) { return [ testDataRef(config.output), diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 8af2aef070..65f3f77640 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -48,6 +48,7 @@ function makeExtDef( name, attachTo: { id: attachId, input: 'default' }, disabled: status === 'disabled', + override: () => ({} as ExtensionDefinition), } as ExtensionDefinition; } diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 4dc9c99c78..325dbd193e 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -396,7 +396,7 @@ export function createApiExtension< configSchema?: PortableSchema; inputs?: TInputs; }, -): ExtensionDefinition; +): ExtensionDefinition; // @public (undocumented) export namespace createApiExtension { @@ -492,7 +492,7 @@ export function createComponentExtension< inputs: Expand>; }) => ComponentType; }; -}): ExtensionDefinition; +}): ExtensionDefinition; // @public (undocumented) export namespace createComponentExtension { @@ -524,8 +524,6 @@ export function createExtension< } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, @@ -534,26 +532,20 @@ export function createExtension< options: CreateExtensionOptions< UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput >, ): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs >; // @public @deprecated (undocumented) @@ -562,33 +554,14 @@ export function createExtension< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, >( options: LegacyCreateExtensionOptions< TOutput, TInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, -): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) ->; +): ExtensionDefinition; // @public export function createExtensionBlueprint< @@ -751,8 +724,6 @@ export type CreateExtensionOptions< } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, @@ -768,20 +739,14 @@ export type CreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: Array; - configSchema?: PortableSchema; config?: { schema: TConfigSchema; }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: { + [key in keyof TConfigSchema]: z.infer>; + }; inputs: Expand>; }): Iterable; } & VerifyExtensionFactoryOutput; @@ -827,7 +792,9 @@ export function createNavItemExtension(options: { }, { title?: string | undefined; - } + }, + never, + never >; // @public (undocumented) @@ -850,7 +817,7 @@ export function createNavLogoExtension(options: { namespace?: string; logoIcon: JSX.Element; logoFull: JSX.Element; -}): ExtensionDefinition<{}, {}>; +}): ExtensionDefinition; // @public (undocumented) export namespace createNavLogoExtension { @@ -1005,7 +972,7 @@ export function createSubRouteRef< // @public (undocumented) export function createThemeExtension( theme: AppTheme, -): ExtensionDefinition<{}, {}>; +): ExtensionDefinition; // @public (undocumented) export namespace createThemeExtension { @@ -1021,7 +988,7 @@ export namespace createThemeExtension { export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; -}): ExtensionDefinition<{}, {}>; +}): ExtensionDefinition; // @public (undocumented) export namespace createTranslationExtension { @@ -1259,7 +1226,20 @@ export type ExtensionDataValues = { }; // @public (undocumented) -export interface ExtensionDefinition { +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + } = {}, +> { // (undocumented) $$type: '@backstage/ExtensionDefinition'; // (undocumented) @@ -1277,6 +1257,76 @@ export interface ExtensionDefinition { readonly name?: string; // (undocumented) readonly namespace?: string; + // (undocumented) + override< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + >( + args: { + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >, + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs + >; } // @public (undocumented) @@ -1418,9 +1468,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, > { // (undocumented) attachTo: { @@ -1428,24 +1475,13 @@ export interface LegacyCreateExtensionOptions< input: string; }; // (undocumented) - config?: { - schema: TConfigSchema; - }; - // @deprecated (undocumented) configSchema?: PortableSchema; // (undocumented) disabled?: boolean; // (undocumented) factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: TConfig; inputs: Expand>; }): Expand>; // (undocumented) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts index 555cd82d4c..aa00247222 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts @@ -75,7 +75,6 @@ describe('createExtensionOverrides', () => { "id": "a", "inputs": {}, "output": {}, - "override": [Function], "toString": [Function], "version": "v1", }, @@ -91,7 +90,6 @@ describe('createExtensionOverrides', () => { "id": "b", "inputs": {}, "output": {}, - "override": [Function], "toString": [Function], "version": "v1", }, @@ -107,7 +105,6 @@ describe('createExtensionOverrides', () => { "id": "k:c/n", "inputs": {}, "output": {}, - "override": [Function], "toString": [Function], "version": "v1", }, diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index e1bfcbfc11..74d081e95d 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -23,6 +23,7 @@ describe('resolveExtensionDefinition', () => { version: 'v2', attachTo: { id: '', input: '' }, disabled: false, + override: () => ({} as ExtensionDefinition), }; it.each([ @@ -63,6 +64,7 @@ describe('old resolveExtensionDefinition', () => { version: 'v1', attachTo: { id: '', input: '' }, disabled: false, + override: () => ({} as ExtensionDefinition), }; it.each([ diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index 18b6f0a653..0c8a7959f7 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -100,7 +100,13 @@ export function resolveExtensionDefinition( context?: { namespace?: string }, ): Extension { const internalDefinition = toInternalExtensionDefinition(definition); - const { name, kind, namespace: _, ...rest } = internalDefinition; + const { + name, + kind, + namespace: _skip1, + override: _skip2, + ...rest + } = internalDefinition; const namespace = internalDefinition.namespace ?? context?.namespace; const namePart = diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index bbe52c3fbc..ce7da3701d 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -169,7 +169,7 @@ export class ExtensionTester { : [...internal.output, coreExtensionData.routePath], factory: params => { const parentOutput = Array.from( - internal.factory(params) as Iterable< + internal.factory(params as any) as Iterable< ExtensionDataValue >, ).filter(val => val.id !== coreExtensionData.routePath.id); diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index 206c0160e2..3ff0788b04 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -115,7 +115,7 @@ export function createEntityCardExtension< config: TConfig; inputs: Expand>; }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition; // @alpha (undocumented) export function createEntityContentExtension< @@ -148,7 +148,9 @@ export function createEntityContentExtension< filter?: string | undefined; title?: string | undefined; path?: string | undefined; - } + }, + never, + never >; // @alpha diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index 336bf4bc2b..726ee80573 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -108,7 +108,7 @@ export function createCatalogFilterExtension< inputs?: TInputs; configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition; // @alpha (undocumented) const _default: BackstagePlugin< diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index c6e011fe41..56c0b6eab9 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -25,7 +25,7 @@ export function createSearchResultListItemExtension< }, >( options: SearchResultItemExtensionOptions, -): ExtensionDefinition; +): ExtensionDefinition; // @alpha (undocumented) export namespace createSearchResultListItemExtension { diff --git a/plugins/search/api-report-alpha.md b/plugins/search/api-report-alpha.md index e9b86f1f87..bb670522e4 100644 --- a/plugins/search/api-report-alpha.md +++ b/plugins/search/api-report-alpha.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; @@ -17,7 +18,7 @@ const _default: BackstagePlugin< export default _default; // @alpha (undocumented) -export const searchApi: ExtensionDefinition<{}, {}>; +export const searchApi: ExtensionDefinition<{}, {}, never, never>; // @alpha (undocumented) export const searchNavItem: ExtensionDefinition< @@ -26,7 +27,9 @@ export const searchNavItem: ExtensionDefinition< }, { title?: string | undefined; - } + }, + never, + never >; // @alpha (undocumented) @@ -38,7 +41,9 @@ export const searchPage: ExtensionDefinition< { path: string; noTrack: boolean; - } + }, + AnyExtensionDataRef, + {} >; // (No @packageDocumentation comment for this package) diff --git a/plugins/techdocs/api-report-alpha.md b/plugins/techdocs/api-report-alpha.md index 04a0c5c512..e3c48ba214 100644 --- a/plugins/techdocs/api-report-alpha.md +++ b/plugins/techdocs/api-report-alpha.md @@ -37,7 +37,9 @@ export const techDocsSearchResultListItemExtension: ExtensionDefinition< asListItem: boolean; asLink: boolean; title?: string | undefined; - } + }, + never, + never >; // (No @packageDocumentation comment for this package) diff --git a/plugins/user-settings/api-report-alpha.md b/plugins/user-settings/api-report-alpha.md index fdb2c5f6bf..d0fad90e08 100644 --- a/plugins/user-settings/api-report-alpha.md +++ b/plugins/user-settings/api-report-alpha.md @@ -24,7 +24,9 @@ export const settingsNavItem: ExtensionDefinition< }, { title?: string | undefined; - } + }, + never, + never >; // @alpha (undocumented) From 467ec43f55ac9f4795bc95ba34dee1c2748b613d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 12:13:05 +0200 Subject: [PATCH 168/204] chore: wip Signed-off-by: blam --- .../src/wiring/createExtension.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 30375049bf..547ec2a160 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -660,6 +660,57 @@ describe('createExtension', () => { expect(true).toBe(true); }); + it('should allow overriding the returned values from the parent factory', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo), numberDataRef(42)]; + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output; + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output.omit(numberDataRef); + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output; + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const tester = createExtensionTester(overridden); + + expect(tester.data(numberDataRef)).toBe(43); + }); + it('should work functionally with overrides', () => { const testExtension = createExtension({ kind: 'thing', From 2d215993f14e9018a9fd031ed69c662d9b74783e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 13:22:59 +0200 Subject: [PATCH 169/204] chore: added changeset Signed-off-by: blam --- .changeset/thick-squids-drive.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .changeset/thick-squids-drive.md diff --git a/.changeset/thick-squids-drive.md b/.changeset/thick-squids-drive.md new file mode 100644 index 0000000000..07654da27d --- /dev/null +++ b/.changeset/thick-squids-drive.md @@ -0,0 +1,31 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +--- + +Added support for being able to override extension definitions. + +```tsx +const TestCard = EntityCardBlueprint.make({ + ... +}); + +TestCard.override({ + attachTo: { id: 'something-else', input: 'overriden' }, + config: { + schema: { + newConfig: z => z.string().optional(), + } + }, + *factory(originalFactory, { inputs, config }){ + const originalOutput = originalFactory(); + + yield coreExentsionData.reactElement( + + {originalOutput.get(coreExentsionData.reactElement)} + + ); + } +}); + +``` From 8fb60cb2f133a36684dd13028b02de54cb623a49 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 13:41:00 +0200 Subject: [PATCH 170/204] chore: some more work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- .changeset/thick-squids-drive.md | 5 ++++- .../src/wiring/createExtension.test.ts | 20 ------------------- .../src/wiring/createExtension.ts | 9 ++++++++- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.changeset/thick-squids-drive.md b/.changeset/thick-squids-drive.md index 07654da27d..0e798063c0 100644 --- a/.changeset/thick-squids-drive.md +++ b/.changeset/thick-squids-drive.md @@ -11,12 +11,15 @@ const TestCard = EntityCardBlueprint.make({ }); TestCard.override({ - attachTo: { id: 'something-else', input: 'overriden' }, + // override attachment points + attachTo: { id: 'something-else', input: 'overridden' }, + // extend the config schema config: { schema: { newConfig: z => z.string().optional(), } }, + // override factory *factory(originalFactory, { inputs, config }){ const originalOutput = originalFactory(); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 547ec2a160..f9e39e98de 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -686,26 +686,6 @@ describe('createExtension', () => { }, }); - const overridden = testExtension.override({ - output: [numberDataRef, stringDataRef], - *factory(originalFactory) { - const output = originalFactory(); - yield* output.omit(numberDataRef); - - yield numberDataRef(output.get(numberDataRef) + 1); - }, - }); - - const overridden = testExtension.override({ - output: [numberDataRef, stringDataRef], - *factory(originalFactory) { - const output = originalFactory(); - yield* output; - - yield numberDataRef(output.get(numberDataRef) + 1); - }, - }); - const tester = createExtensionTester(overridden); expect(tester.data(numberDataRef)).toBe(43); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 6e75fc76e5..600b96ebe0 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -581,7 +581,7 @@ export function createExtension< >, }); } - return overrideOptions.factory( + const parentResult = overrideOptions.factory( (innerContext?: { config?: { [key in keyof TConfigSchema]: z.infer< @@ -604,6 +604,13 @@ export function createExtension< inputs, }, ); + + const deduplicatedResult = new Map(); + for (const item of parentResult) { + deduplicatedResult.set(item.id, item); + } + + return deduplicatedResult.values() as Iterable; }, } as CreateExtensionOptions); }, From 201c98bb2c09a2a226c875d9343418eacae297c2 Mon Sep 17 00:00:00 2001 From: bioerrorlog Date: Thu, 8 Aug 2024 21:02:07 +0900 Subject: [PATCH 171/204] docs: fix broken link in techdocs configuration Signed-off-by: bioerrorlog --- docs/features/techdocs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index a861a50c87..8c2348972c 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -20,7 +20,7 @@ techdocs: # spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of). # You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running # into Docker in Docker situation. Read more here - # https://backstage.io/docs/features/techdocs/getting-started#disable-docker-in-docker-situation-optional + # https://backstage.io/docs/features/techdocs/getting-started/#disabling-docker-in-docker-situation-optional runIn: 'docker' From a65cfc814916f985d6a2b3a06a8b3fea3bd6557f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 8 Aug 2024 16:19:54 +0200 Subject: [PATCH 172/204] frontend-plugin-api: add support for accessing extensions from plugin instance Signed-off-by: Patrik Oldsberg --- .changeset/old-tools-smell.md | 5 + .../app-next-example-plugin/api-report.md | 2 +- packages/frontend-plugin-api/api-report.md | 172 ++++++++++++++---- .../src/wiring/createExtension.ts | 149 ++++++++++----- .../src/wiring/createExtensionBlueprint.ts | 70 ++++--- .../src/wiring/createPlugin.test.ts | 8 +- .../src/wiring/createPlugin.ts | 67 ++++--- .../wiring/resolveExtensionDefinition.test.ts | 105 ++++++++++- .../src/wiring/resolveExtensionDefinition.ts | 30 +++ .../frontend-plugin-api/src/wiring/types.ts | 18 +- plugins/api-docs/api-report-alpha.md | 3 +- plugins/app-visualizer/api-report.md | 2 +- plugins/catalog-graph/api-report-alpha.md | 3 +- plugins/catalog-import/api-report-alpha.md | 1 + plugins/catalog-react/api-report-alpha.md | 15 +- plugins/catalog/api-report-alpha.md | 13 +- plugins/devtools/api-report-alpha.md | 1 + plugins/home/api-report-alpha.md | 2 +- plugins/kubernetes/api-report-alpha.md | 1 + plugins/org/api-report-alpha.md | 3 +- plugins/scaffolder/api-report-alpha.md | 3 +- plugins/search-react/api-report-alpha.md | 10 +- plugins/search/api-report-alpha.md | 21 ++- plugins/techdocs/api-report-alpha.md | 6 +- plugins/user-settings/api-report-alpha.md | 6 +- 25 files changed, 564 insertions(+), 152 deletions(-) create mode 100644 .changeset/old-tools-smell.md diff --git a/.changeset/old-tools-smell.md b/.changeset/old-tools-smell.md new file mode 100644 index 0000000000..4dbd65e779 --- /dev/null +++ b/.changeset/old-tools-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Add support for accessing extensions definitions provided by a plugin via `plugin.getExtension(...)`. For this to work the extensions must be defined using the v2 format, typically using an extension blueprint. diff --git a/packages/app-next-example-plugin/api-report.md b/packages/app-next-example-plugin/api-report.md index c89a7b599b..962b1473a8 100644 --- a/packages/app-next-example-plugin/api-report.md +++ b/packages/app-next-example-plugin/api-report.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { default as React_2 } from 'react'; // @public (undocumented) -const examplePlugin: BackstagePlugin<{}, {}>; +const examplePlugin: BackstagePlugin<{}, {}, {}>; export default examplePlugin; // @public (undocumented) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 325dbd193e..a8f8ce591b 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -275,17 +275,22 @@ export { BackstageIdentityResponse }; // @public (undocumented) export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { + [id in string]: ExtensionDefinition; + } = {}, > { // (undocumented) readonly $$type: '@backstage/BackstagePlugin'; // (undocumented) - readonly externalRoutes: ExternalRoutes; + readonly externalRoutes: TExternalRoutes; + // (undocumented) + getExtension(id: TId): TExtensionMap[TId]; // (undocumented) readonly id: string; // (undocumented) - readonly routes: Routes; + readonly routes: TRoutes; } export { BackstageUserIdentity }; @@ -396,7 +401,15 @@ export function createApiExtension< configSchema?: PortableSchema; inputs?: TInputs; }, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createApiExtension { @@ -492,7 +505,15 @@ export function createComponentExtension< inputs: Expand>; }) => ComponentType; }; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createComponentExtension { @@ -528,8 +549,14 @@ export function createExtension< [key: string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, >( options: CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, TConfigSchema, @@ -545,7 +572,10 @@ export function createExtension< }> >, UOutput, - TInputs + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName >; // @public @deprecated (undocumented) @@ -580,11 +610,17 @@ export function createExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, TDataRefs extends { [name in string]: AnyExtensionDataRef; } = never, >( options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -593,6 +629,9 @@ export function createExtensionBlueprint< TDataRefs >, ): ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, string extends keyof TInputs ? {} : TInputs, @@ -613,6 +652,9 @@ export function createExtensionBlueprint< // @public (undocumented) export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -632,8 +674,8 @@ export type CreateExtensionBlueprintOptions< [name in string]: AnyExtensionDataRef; }, > = { - kind: string; - namespace?: string | ((params: TParams) => string); + kind: TKind; + namespace?: TNamespace | ((params: TParams) => TNamespace); attachTo: { id: string; input: string; @@ -641,7 +683,7 @@ export type CreateExtensionBlueprintOptions< disabled?: boolean; inputs?: TInputs; output: Array; - name?: string | ((params: TParams) => string); + name?: TName | ((params: TParams) => TName); config?: { schema: TConfigSchema | ((params: TParams) => TConfigSchema); }; @@ -714,6 +756,9 @@ export function createExtensionInput< // @public (undocumented) export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -729,9 +774,9 @@ export type CreateExtensionOptions< }, UFactoryOutput extends ExtensionDataValue, > = { - kind?: string; - namespace?: string; - name?: string; + kind?: TKind; + namespace?: TNamespace; + name?: TName; attachTo: { id: string; input: string; @@ -794,7 +839,10 @@ export function createNavItemExtension(options: { title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @public (undocumented) @@ -817,7 +865,15 @@ export function createNavLogoExtension(options: { namespace?: string; logoIcon: JSX.Element; logoFull: JSX.Element; -}): ExtensionDefinition; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createNavLogoExtension { @@ -865,11 +921,22 @@ export function createPageExtension< // @public (undocumented) export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin; + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +>; // @public export function createRouteRef< @@ -972,7 +1039,15 @@ export function createSubRouteRef< // @public (undocumented) export function createThemeExtension( theme: AppTheme, -): ExtensionDefinition; +): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createThemeExtension { @@ -988,7 +1063,15 @@ export namespace createThemeExtension { export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; -}): ExtensionDefinition; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createTranslationExtension { @@ -1044,6 +1127,9 @@ export interface Extension { // @public (undocumented) export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -1068,6 +1154,8 @@ export interface ExtensionBlueprint< // (undocumented) dataRefs: TDataRefs; make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -1084,8 +1172,8 @@ export interface ExtensionBlueprint< }, >( args: { - namespace?: string; - name?: string; + namespace?: TNewNamespace; + name?: TNewName; attachTo?: { id: string; input: string; @@ -1143,7 +1231,12 @@ export interface ExtensionBlueprint< >; }> > & - TConfigInput + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName >; } @@ -1239,6 +1332,9 @@ export interface ExtensionDefinition< } >; } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, > { // (undocumented) $$type: '@backstage/ExtensionDefinition'; @@ -1252,11 +1348,11 @@ export interface ExtensionDefinition< // (undocumented) readonly disabled: boolean; // (undocumented) - readonly kind?: string; + readonly kind?: TKind; // (undocumented) - readonly name?: string; + readonly name?: TName; // (undocumented) - readonly namespace?: string; + readonly namespace?: TNamespace; // (undocumented) override< TExtensionConfigSchema extends { @@ -1325,7 +1421,10 @@ export interface ExtensionDefinition< > & TConfigInput, AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - TInputs & TExtraInputs + TInputs & TExtraInputs, + TKind, + TNamespace, + TName >; } @@ -1405,6 +1504,9 @@ export { googleAuthApiRef }; // @public (undocumented) export const IconBundleBlueprint: ExtensionBlueprint< + 'icon-bundle', + 'app', + undefined, { icons: { [x: string]: IconComponent; @@ -1536,19 +1638,21 @@ export { PendingOAuthRequest }; // @public (undocumented) export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { // (undocumented) - extensions?: ExtensionDefinition[]; + extensions?: TExtensions; // (undocumented) - externalRoutes?: ExternalRoutes; + externalRoutes?: TExternalRoutes; // (undocumented) featureFlags?: FeatureFlagConfig[]; // (undocumented) - id: string; + id: TId; // (undocumented) - routes?: Routes; + routes?: TRoutes; } // @public (undocumented) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 600b96ebe0..f2d3d906ab 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -174,6 +174,9 @@ export type VerifyExtensionFactoryOutput< /** @public */ export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -184,9 +187,9 @@ export type CreateExtensionOptions< TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, > = { - kind?: string; - namespace?: string; - name?: string; + kind?: TKind; + namespace?: TNamespace; + name?: TName; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; @@ -214,11 +217,14 @@ export interface ExtensionDefinition< { optional: boolean; singleton: boolean } >; } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, > { $$type: '@backstage/ExtensionDefinition'; - readonly kind?: string; - readonly namespace?: string; - readonly name?: string; + readonly kind?: TKind; + readonly namespace?: TNamespace; + readonly name?: TName; readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema; @@ -284,45 +290,68 @@ export interface ExtensionDefinition< > & TConfigInput, AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - TInputs & TExtraInputs + TInputs & TExtraInputs, + TKind, + TNamespace, + TName >; } /** @internal */ -export type InternalExtensionDefinition = - ExtensionDefinition & - ( - | { - readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; - readonly output: AnyExtensionDataMap; - factory(context: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs; - }): ExtensionDataValues; - } - | { - readonly version: 'v2'; - readonly inputs: { +export type InternalExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> = ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + TNamespace, + TName +> & + ( + | { + readonly version: 'v1'; + readonly inputs: AnyExtensionInputMap; + readonly output: AnyExtensionDataMap; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs; + }): ExtensionDataValues; + } + | { + readonly version: 'v2'; + readonly inputs: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }; + readonly output: Array; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< AnyExtensionDataRef, { optional: boolean; singleton: boolean } >; - }; - readonly output: Array; - factory(context: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }>; - }): Iterable>; - } - ); + }>; + }): Iterable>; + } + ); /** @internal */ export function toInternalExtensionDefinition( @@ -357,8 +386,14 @@ export function createExtension< }, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, >( options: CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, TConfigSchema, @@ -374,7 +409,10 @@ export function createExtension< }> >, UOutput, - TInputs + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName >; /** * @public @@ -394,6 +432,9 @@ export function createExtension< >, ): ExtensionDefinition; export function createExtension< + const TKind extends string | undefined, + const TNamespace extends string | undefined, + const TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -408,7 +449,15 @@ export function createExtension< UFactoryOutput extends ExtensionDataValue, >( options: - | CreateExtensionOptions + | CreateExtensionOptions< + TKind, + TNamespace, + TName, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput + > | LegacyCreateExtensionOptions< AnyExtensionDataMap, TLegacyInputs, @@ -431,7 +480,10 @@ export function createExtension< }> >), UOutput, - TInputs + TInputs, + TKind, + TNamespace, + TName > { if ('configSchema' in options && 'config' in options) { throw new Error(`Cannot provide both configSchema and config.schema`); @@ -542,7 +594,10 @@ export function createExtension< > >, AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - TInputs & TExtraInputs + TInputs & TExtraInputs, + TKind, + TNamespace, + TName > => { if (!Array.isArray(options.output)) { throw new Error( @@ -550,6 +605,9 @@ export function createExtension< ); } const newOptions = options as CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, TConfigSchema, @@ -612,7 +670,7 @@ export function createExtension< return deduplicatedResult.values() as Iterable; }, - } as CreateExtensionOptions); + } as CreateExtensionOptions); }, } as InternalExtensionDefinition< TConfig & @@ -630,6 +688,11 @@ export function createExtension< z.ZodObject<{ [key in keyof TConfigSchema]: ReturnType; }> - >) + >), + UOutput, + TInputs, + TKind, + TNamespace, + TName >; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index a8bf42c7f8..81137f47e2 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -36,6 +36,9 @@ import { * @public */ export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -48,13 +51,13 @@ export type CreateExtensionBlueprintOptions< UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > = { - kind: string; - namespace?: string | ((params: TParams) => string); + kind: TKind; + namespace?: TNamespace | ((params: TParams) => TNamespace); attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; - name?: string | ((params: TParams) => string); + name?: TName | ((params: TParams) => TName); config?: { schema: TConfigSchema | ((params: TParams) => TConfigSchema); }; @@ -76,6 +79,9 @@ export type CreateExtensionBlueprintOptions< * @public */ export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -97,6 +103,8 @@ export interface ExtensionBlueprint< * optionally call the original factory with the same params. */ make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -110,8 +118,8 @@ export interface ExtensionBlueprint< }, >( args: { - namespace?: string; - name?: string; + namespace?: TNewNamespace; + name?: TNewName; attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TExtraInputs & { @@ -166,7 +174,12 @@ export interface ExtensionBlueprint< >; }> > & - TConfigInput + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName >; } @@ -198,6 +211,9 @@ export function createDataContainer( * @internal */ class ExtensionBlueprintImpl< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -211,6 +227,9 @@ class ExtensionBlueprintImpl< > { constructor( private readonly options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -236,9 +255,11 @@ class ExtensionBlueprintImpl< { optional: boolean; singleton: boolean } >; }, + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, >(args: { - namespace?: string; - name?: string; + namespace?: TNewNamespace; + name?: TNewName; attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TExtraInputs; @@ -360,26 +381,7 @@ class ExtensionBlueprintImpl< } throw new Error('Either params or factory must be provided'); }, - } as CreateExtensionOptions) as ExtensionDefinition< - { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - } & { - [key in keyof TConfigSchema]: z.infer>; - }, - z.input< - z.ZodObject< - { - [key in keyof TExtensionConfigSchema]: ReturnType< - TExtensionConfigSchema[key] - >; - } & { - [key in keyof TConfigSchema]: ReturnType; - } - > - > - >; + } as CreateExtensionOptions); } } @@ -400,9 +402,15 @@ export function createExtensionBlueprint< }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, TDataRefs extends { [name in string]: AnyExtensionDataRef } = never, >( options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -411,6 +419,9 @@ export function createExtensionBlueprint< TDataRefs >, ): ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, string extends keyof TInputs ? {} : TInputs, @@ -427,6 +438,9 @@ export function createExtensionBlueprint< TDataRefs > { return new ExtensionBlueprintImpl(options) as ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, string extends keyof TInputs ? {} : TInputs, diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index bb9bdc2107..0242e2961a 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -34,11 +34,9 @@ const nameExtensionDataRef = createExtensionDataRef().with({ const Extension1 = createExtension({ name: '1', attachTo: { id: 'test/output', input: 'names' }, - output: { - name: nameExtensionDataRef, - }, + output: [nameExtensionDataRef], factory() { - return { name: 'extension-1' }; + return [nameExtensionDataRef('extension-1')]; }, }); @@ -150,6 +148,8 @@ describe('createPlugin', () => { }); expect(plugin).toBeDefined(); + expect(plugin.getExtension('test/1')).toBe(Extension1); + await renderWithEffects( createTestAppRoot({ features: [plugin], diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index b671d95d2f..1d2a12ffe2 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -17,6 +17,7 @@ import { ExtensionDefinition } from './createExtension'; import { Extension, + ResolveExtensionId, resolveExtensionDefinition, } from './resolveExtensionDefinition'; import { @@ -28,21 +29,23 @@ import { /** @public */ export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { - id: string; - routes?: Routes; - externalRoutes?: ExternalRoutes; - extensions?: ExtensionDefinition[]; + id: TId; + routes?: TRoutes; + externalRoutes?: TExternalRoutes; + extensions?: TExtensions; featureFlags?: FeatureFlagConfig[]; } /** @public */ export interface InternalBackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, -> extends BackstagePlugin { + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, +> extends BackstagePlugin { readonly version: 'v1'; readonly extensions: Extension[]; readonly featureFlags: FeatureFlagConfig[]; @@ -50,17 +53,36 @@ export interface InternalBackstagePlugin< /** @public */ export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin { - const extensions = (options.extensions ?? []).map(def => - resolveExtensionDefinition(def, { namespace: options.id }), - ); + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +> { + const extensions = new Array>(); + const extensionDefinitionsById = new Map< + string, + ExtensionDefinition + >(); - const extensionIds = extensions.map(e => e.id); - if (extensionIds.length !== new Set(extensionIds).size) { + for (const def of options.extensions ?? []) { + const ext = resolveExtensionDefinition(def, { namespace: options.id }); + extensions.push(ext); + extensionDefinitionsById.set(ext.id, def); + } + + if (extensions.length !== extensionDefinitionsById.size) { + const extensionIds = extensions.map(e => e.id); const duplicates = Array.from( new Set( extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index), @@ -78,14 +100,17 @@ export function createPlugin< $$type: '@backstage/BackstagePlugin', version: 'v1', id: options.id, - routes: options.routes ?? ({} as Routes), - externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes), + routes: options.routes ?? ({} as TRoutes), + externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes), featureFlags: options.featureFlags ?? [], extensions, + getExtension(id) { + return extensionDefinitionsById.get(id); + }, toString() { return `Plugin{id=${options.id}}`; }, - } as InternalBackstagePlugin; + } as InternalBackstagePlugin; } /** @internal */ diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index 74d081e95d..5a42e9100e 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -15,7 +15,10 @@ */ import { ExtensionDefinition } from './createExtension'; -import { resolveExtensionDefinition } from './resolveExtensionDefinition'; +import { + ResolveExtensionId, + resolveExtensionDefinition, +} from './resolveExtensionDefinition'; describe('resolveExtensionDefinition', () => { const baseDef = { @@ -98,3 +101,103 @@ describe('old resolveExtensionDefinition', () => { ); }); }); + +describe('ResolveExtensionId', () => { + it('should resolve extension IDs correctly', () => { + type NamedExtension< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, + > = ExtensionDefinition; + + const id1: 'k:ns' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + undefined + >; + const id2: 'k:n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + undefined + >; + const id3: 'ns/n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id4: never = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + undefined + >; + const id5: 'ns' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id6: 'n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id7: 'k:ns/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + undefined + >; + const id8: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + 'ns2' + >; + const id9: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + 'ns2' + >; + const ida: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idb: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + 'ns2' + >; + const idc: 'ns2' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idd: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const ide: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + 'ns2' + >; + + const invalid1: never = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const invalid2: never = {} as ResolveExtensionId< + NamedExtension<'k', string | undefined, 'n'>, + undefined + >; + const invalid3: never = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', string | undefined>, + undefined + >; + + expect([ + id1, + id2, + id3, + id4, + id5, + id6, + id7, + id8, + id9, + ida, + idb, + idc, + idd, + ide, + invalid1, + invalid2, + invalid3, + ]).toBeDefined(); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index 0c8a7959f7..35bf9c6b05 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -94,6 +94,36 @@ export function toInternalExtension( return internal; } +/** @ignore */ +export type ResolveExtensionId< + TExtension extends ExtensionDefinition, + TDefaultNamespace extends string | undefined, +> = TExtension extends ExtensionDefinition< + any, + any, + any, + any, + infer IKind, + infer INamespace, + infer IName +> + ? [string | undefined] extends [IKind | INamespace | IName] + ? never + : ( + ( + undefined extends TDefaultNamespace ? INamespace : TDefaultNamespace + ) extends infer ISelectedNamespace extends string + ? undefined extends IName + ? ISelectedNamespace + : `${ISelectedNamespace}/${IName}` + : IName + ) extends infer INamePart extends string + ? IKind extends string + ? `${IKind}:${INamePart}` + : INamePart + : never + : never; + /** @internal */ export function resolveExtensionDefinition( definition: ExtensionDefinition, diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts index 7e0650e06c..b1afd4e7ea 100644 --- a/packages/frontend-plugin-api/src/wiring/types.ts +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -15,6 +15,7 @@ */ import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing'; +import { ExtensionDefinition } from './createExtension'; /** * Feature flag configuration. @@ -32,15 +33,24 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef }; /** @public */ export type AnyExternalRoutes = { [name in string]: ExternalRouteRef }; +/** @public */ +export type ExtensionMap< + TExtensionMap extends { [id in string]: ExtensionDefinition }, +> = { + get(id: TId): TExtensionMap[TId]; +}; + /** @public */ export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { [id in string]: ExtensionDefinition } = {}, > { readonly $$type: '@backstage/BackstagePlugin'; readonly id: string; - readonly routes: Routes; - readonly externalRoutes: ExternalRoutes; + readonly routes: TRoutes; + readonly externalRoutes: TExternalRoutes; + getExtension(id: TId): TExtensionMap[TId]; } /** @public */ diff --git a/plugins/api-docs/api-report-alpha.md b/plugins/api-docs/api-report-alpha.md index f17ba0fa3c..6003056cb3 100644 --- a/plugins/api-docs/api-report-alpha.md +++ b/plugins/api-docs/api-report-alpha.md @@ -14,7 +14,8 @@ const _default: BackstagePlugin< }, { registerApi: ExternalRouteRef; - } + }, + {} >; export default _default; diff --git a/plugins/app-visualizer/api-report.md b/plugins/app-visualizer/api-report.md index 764caa7190..fd3749b159 100644 --- a/plugins/app-visualizer/api-report.md +++ b/plugins/app-visualizer/api-report.md @@ -6,7 +6,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @public (undocumented) -const visualizerPlugin: BackstagePlugin<{}, {}>; +const visualizerPlugin: BackstagePlugin<{}, {}, {}>; export default visualizerPlugin; // (No @packageDocumentation comment for this package) diff --git a/plugins/catalog-graph/api-report-alpha.md b/plugins/catalog-graph/api-report-alpha.md index 88b3a38b76..e651946463 100644 --- a/plugins/catalog-graph/api-report-alpha.md +++ b/plugins/catalog-graph/api-report-alpha.md @@ -18,7 +18,8 @@ const _default: BackstagePlugin< kind: string; namespace: string; }>; - } + }, + {} >; export default _default; diff --git a/plugins/catalog-import/api-report-alpha.md b/plugins/catalog-import/api-report-alpha.md index 63e2cda339..51e3d99fd7 100644 --- a/plugins/catalog-import/api-report-alpha.md +++ b/plugins/catalog-import/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { importPage: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index 3ff0788b04..edfa183903 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -115,7 +115,15 @@ export function createEntityCardExtension< config: TConfig; inputs: Expand>; }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export function createEntityContentExtension< @@ -150,7 +158,10 @@ export function createEntityContentExtension< path?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index 726ee80573..5019fc25ea 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -108,7 +108,15 @@ export function createCatalogFilterExtension< inputs?: TInputs; configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) const _default: BackstagePlugin< @@ -132,7 +140,8 @@ const _default: BackstagePlugin< templateName: string; }>; unregisterRedirect: ExternalRouteRef; - } + }, + {} >; export default _default; diff --git a/plugins/devtools/api-report-alpha.md b/plugins/devtools/api-report-alpha.md index 59aefd3698..23ede8144c 100644 --- a/plugins/devtools/api-report-alpha.md +++ b/plugins/devtools/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/home/api-report-alpha.md b/plugins/home/api-report-alpha.md index eb693a8e6f..0f9b980a78 100644 --- a/plugins/home/api-report-alpha.md +++ b/plugins/home/api-report-alpha.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin<{}, {}>; +const _default: BackstagePlugin<{}, {}, {}>; export default _default; // @alpha (undocumented) diff --git a/plugins/kubernetes/api-report-alpha.md b/plugins/kubernetes/api-report-alpha.md index 8bd6e98baa..befbcf379a 100644 --- a/plugins/kubernetes/api-report-alpha.md +++ b/plugins/kubernetes/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { kubernetes: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/org/api-report-alpha.md b/plugins/org/api-report-alpha.md index e7c4db2c6d..be74cde141 100644 --- a/plugins/org/api-report-alpha.md +++ b/plugins/org/api-report-alpha.md @@ -11,7 +11,8 @@ const _default: BackstagePlugin< {}, { catalogIndex: ExternalRouteRef; - } + }, + {} >; export default _default; diff --git a/plugins/scaffolder/api-report-alpha.md b/plugins/scaffolder/api-report-alpha.md index 4628563be2..de0fc463a9 100644 --- a/plugins/scaffolder/api-report-alpha.md +++ b/plugins/scaffolder/api-report-alpha.md @@ -36,7 +36,8 @@ const _default: BackstagePlugin< kind: string; namespace: string; }>; - } + }, + {} >; export default _default; diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index 56c0b6eab9..4bcdbb3f78 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -25,7 +25,15 @@ export function createSearchResultListItemExtension< }, >( options: SearchResultItemExtensionOptions, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export namespace createSearchResultListItemExtension { diff --git a/plugins/search/api-report-alpha.md b/plugins/search/api-report-alpha.md index bb670522e4..e6f91d6d19 100644 --- a/plugins/search/api-report-alpha.md +++ b/plugins/search/api-report-alpha.md @@ -13,12 +13,21 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; // @alpha (undocumented) -export const searchApi: ExtensionDefinition<{}, {}, never, never>; +export const searchApi: ExtensionDefinition< + {}, + {}, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export const searchNavItem: ExtensionDefinition< @@ -29,7 +38,10 @@ export const searchNavItem: ExtensionDefinition< title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha (undocumented) @@ -43,7 +55,10 @@ export const searchPage: ExtensionDefinition< noTrack: boolean; }, AnyExtensionDataRef, - {} + {}, + string | undefined, + string | undefined, + string | undefined >; // (No @packageDocumentation comment for this package) diff --git a/plugins/techdocs/api-report-alpha.md b/plugins/techdocs/api-report-alpha.md index e3c48ba214..f7c6679139 100644 --- a/plugins/techdocs/api-report-alpha.md +++ b/plugins/techdocs/api-report-alpha.md @@ -18,6 +18,7 @@ const _default: BackstagePlugin< }>; entityContent: RouteRef; }, + {}, {} >; export default _default; @@ -39,7 +40,10 @@ export const techDocsSearchResultListItemExtension: ExtensionDefinition< title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // (No @packageDocumentation comment for this package) diff --git a/plugins/user-settings/api-report-alpha.md b/plugins/user-settings/api-report-alpha.md index d0fad90e08..700e06a7ad 100644 --- a/plugins/user-settings/api-report-alpha.md +++ b/plugins/user-settings/api-report-alpha.md @@ -13,6 +13,7 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; @@ -26,7 +27,10 @@ export const settingsNavItem: ExtensionDefinition< title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha (undocumented) From 7e8463743aa6853781733bcc01f528b9b31c9d9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Aug 2024 10:26:24 +0200 Subject: [PATCH 173/204] frontend-plugin-api: add test to make sure getExtension complains if ID is unknown Signed-off-by: Patrik Oldsberg --- packages/frontend-plugin-api/src/wiring/createPlugin.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index 0242e2961a..76cc030be3 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -149,6 +149,8 @@ describe('createPlugin', () => { expect(plugin).toBeDefined(); expect(plugin.getExtension('test/1')).toBe(Extension1); + // @ts-expect-error + expect(plugin.getExtension('nonexistent')).toBeUndefined(); await renderWithEffects( createTestAppRoot({ From d1191ee2d76a95a25bebe3860ca564032a6b3378 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 14:52:04 +0200 Subject: [PATCH 174/204] feat: added a new PageBlueprint Signed-off-by: blam Signed-off-by: blam --- .../src/extensions/PageBlueprint.test.tsx | 154 ++++++++++++++++++ .../src/extensions/PageBlueprint.tsx | 71 ++++++++ 2 files changed, 225 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx new file mode 100644 index 0000000000..abe2d96a85 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 { createRouteRef } from '../routing'; +import { PageBlueprint } from './PageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionInput, +} from '../wiring'; +import { waitFor } from '@testing-library/react'; + +describe('PageBlueprint', () => { + const mockRouteRef = createRouteRef(); + + it('should return an extension when calling make with sensible defaults', () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
    Test
    ), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + expect(myPage).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/routes", + "input": "routes", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "page", + "name": "test-page", + "namespace": undefined, + "output": [ + [Function], + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [Function], + }, + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a routeRef with the input routeRef', async () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
    Test
    ), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + const tester = createExtensionTester(myPage); + + // TODO(blam): test for the routePath output doesn't work. + // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); + + expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); + + const { getByTestId } = tester.render(); + + await waitFor(() => expect(getByTestId('test')).toBeInTheDocument()); + }); + + it('should allow defining additional inputs to the extension', async () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: async ({ inputs }) => { + return ( +
    + {/* todo(blam): need to fix the typescript here, as inputs is not the right type */} + {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} +
    + ); + }, + defaultPath: '/test', + routeRef: mockRouteRef, + }, + inputs: { + cards: createExtensionInput([coreExtensionData.reactElement], { + optional: false, + singleton: false, + }), + }, + }); + + const CardBlueprint = createExtensionBlueprint({ + kind: 'card', + attachTo: { id: 'page:test-page', input: 'cards' }, + output: [coreExtensionData.reactElement], + factory() { + return [ + coreExtensionData.reactElement( +
    I'm a lovely card
    , + ), + ]; + }, + }); + + const tester = createExtensionTester(myPage).add( + CardBlueprint.make({ name: 'card', params: {} }), + ); + + const { getByTestId, getByText } = tester.render(); + + await waitFor(() => expect(getByTestId('card')).toBeInTheDocument()); + await waitFor(() => + expect(getByText("I'm a lovely card")).toBeInTheDocument(), + ); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx new file mode 100644 index 0000000000..7ae26cf387 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx @@ -0,0 +1,71 @@ +/* + * 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, { lazy } from 'react'; +import { RouteRef } from '../routing'; +import { coreExtensionData, createExtensionBlueprint } from '../wiring'; +import { ExtensionBoundary } from '../components'; + +export const PageBlueprint = createExtensionBlueprint({ + kind: 'page', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + config: { + schema: { + path: z => z.string().optional(), + }, + }, + factory( + { + defaultPath, + loader, + routeRef, + }: { + defaultPath?: string; + loader: (opts: { + config: typeof config; + inputs: typeof inputs; + }) => Promise; + routeRef?: RouteRef; + }, + { config, inputs, node }, + ) { + const ExtensionComponent = lazy(() => + loader({ config, inputs }).then(element => ({ default: () => element })), + ); + + // TODO(blam): this is a little awkward for optional returns. + // I wonder if we should be using generators or yield instead + // for a better API here. + const outputs = [ + coreExtensionData.routePath(config.path ?? defaultPath!), + coreExtensionData.reactElement( + + + , + ), + ]; + + if (routeRef) { + return [...outputs, coreExtensionData.routeRef(routeRef)]; + } + + return outputs; + }, +}); From 8897c29ecc13ec2aa70dec12baeb6b944f33aad8 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 15:46:39 +0200 Subject: [PATCH 175/204] chore: migrate ThemeCreator Signed-off-by: blam --- .../src/extensions/ThemeBlueprint.test.ts | 64 +++++++++++++++++++ .../src/extensions/ThemeBlueprint.ts | 32 ++++++++++ 2 files changed, 96 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts create mode 100644 packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts new file mode 100644 index 0000000000..d3d05a5e68 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts @@ -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 { AppTheme } from '@backstage/core-plugin-api'; +import { ThemeBlueprint } from './ThemeBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('ThemeBlueprint', () => { + const theme = { + id: 'light', + colors: { primary: 'blue' }, + variant: 'dark', + title: 'lols', + Provider: (_: { children: React.ReactNode }) => null, + } as AppTheme; + + it('should create an extension with sensible defaults', () => { + expect( + // todo(blam): we can't inject theme.id as the name here like the old extension creator. + // Wonder if theres a better solution. + ThemeBlueprint.make({ name: 'blob', params: { theme } }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "themes", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "theme", + "name": "blob", + "namespace": "app", + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the theme as an themeDataRef', async () => { + const extension = ThemeBlueprint.make({ name: 'blob', params: { theme } }); + + expect( + createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme), + ).toEqual(theme); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts new file mode 100644 index 0000000000..4ddb5a298a --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts @@ -0,0 +1,32 @@ +/* + * 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 { AppTheme } from '@backstage/core-plugin-api'; +import { createExtensionBlueprint } from '../wiring'; +import { createThemeExtension } from './createThemeExtension'; + +export const ThemeBlueprint = createExtensionBlueprint({ + kind: 'theme', + namespace: 'app', + attachTo: { id: 'app', input: 'themes' }, + output: [createThemeExtension.themeDataRef], + dataRefs: { + theme: createThemeExtension.themeDataRef, + }, + factory: ({ theme }: { theme: AppTheme }) => [ + createThemeExtension.themeDataRef(theme), + ], +}); From da75ca4e4702b057b8c3bd23bd6e6e514b8a8e77 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 16:21:57 +0200 Subject: [PATCH 176/204] chore: migration translation extension creator Signed-off-by: blam --- .../extensions/TranslationBlueprint.test.ts | 81 +++++++++++++++++++ .../src/extensions/TranslationBlueprint.ts | 33 ++++++++ 2 files changed, 114 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts create mode 100644 packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts new file mode 100644 index 0000000000..fe7aeba59d --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + createTranslationMessages, + createTranslationRef, +} from '../translation'; +import { TranslationBlueprint } from './TranslationBlueprint'; + +describe('TranslationBlueprint', () => { + const translationRef = createTranslationRef({ + id: 'test', + messages: { + test: 'test', + }, + }); + + const messages = createTranslationMessages({ + ref: translationRef, + messages: { + test: 'test2', + }, + }); + + it('should return an extension instance with sane defaults', () => { + expect( + TranslationBlueprint.make({ + params: { + resource: messages, + }, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "translations", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "translation", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a translation data ref', () => { + const extension = TranslationBlueprint.make({ + params: { + resource: messages, + }, + }); + + expect( + createExtensionTester(extension).data( + TranslationBlueprint.dataRefs.translation, + ), + ).toBe(messages); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts new file mode 100644 index 0000000000..23cb5da6a6 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts @@ -0,0 +1,33 @@ +/* + * 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 { createExtensionBlueprint } from '../wiring'; +import { createTranslationExtension } from './createTranslationExtension'; +import { TranslationMessages, TranslationResource } from '../translation'; + +export const TranslationBlueprint = createExtensionBlueprint({ + kind: 'translation', + attachTo: { id: 'app', input: 'translations' }, + output: [createTranslationExtension.translationDataRef], + dataRefs: { + translation: createTranslationExtension.translationDataRef, + }, + factory: ({ + resource, + }: { + resource: TranslationResource | TranslationMessages; + }) => [createTranslationExtension.translationDataRef(resource)], +}); From 48b4304c46a62052ad2245b52def395ca0256aee Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 16:28:33 +0200 Subject: [PATCH 177/204] chore: fix Signed-off-by: blam Signed-off-by: blam --- .../src/extensions/TranslationBlueprint.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts index fe7aeba59d..658c0c9709 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -38,6 +38,10 @@ describe('TranslationBlueprint', () => { it('should return an extension instance with sane defaults', () => { expect( TranslationBlueprint.make({ + // todo(blam): we can't set the namespace dynamically based of the ResourceType. + // work out if we should wrap this up or another solution. + namespace: messages.id, + name: 'test', params: { resource: messages, }, @@ -54,8 +58,8 @@ describe('TranslationBlueprint', () => { "factory": [Function], "inputs": {}, "kind": "translation", - "name": undefined, - "namespace": undefined, + "name": "test", + "namespace": "test", "output": [ [Function], ], From 1f4029b9d877885db10ba63a57afee368bb906a5 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 08:50:47 +0200 Subject: [PATCH 178/204] chore: refactor a little bit and use some new features to remove todos Signed-off-by: blam --- .../src/extensions/PageBlueprint.test.tsx | 4 ++-- .../src/extensions/PageBlueprint.tsx | 23 +++++++------------ .../src/extensions/ThemeBlueprint.test.ts | 10 +++----- .../src/extensions/ThemeBlueprint.ts | 1 + .../extensions/TranslationBlueprint.test.ts | 12 ++++------ .../src/extensions/TranslationBlueprint.ts | 1 + 6 files changed, 20 insertions(+), 31 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx index abe2d96a85..e020167f43 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -94,7 +94,7 @@ describe('PageBlueprint', () => { const tester = createExtensionTester(myPage); - // TODO(blam): test for the routePath output doesn't work. + // TODO(blam): test for the routePath output doesn't work, due to the the way the test harness works // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); @@ -111,7 +111,6 @@ describe('PageBlueprint', () => { loader: async ({ inputs }) => { return (
    - {/* todo(blam): need to fix the typescript here, as inputs is not the right type */} {inputs.cards.map(c => c.get(coreExtensionData.reactElement))}
    ); @@ -119,6 +118,7 @@ describe('PageBlueprint', () => { defaultPath: '/test', routeRef: mockRouteRef, }, + /* todo(blam): need to fix the typescript here, as inputs is not the right type, wont let me merge without specifying parent opts */ inputs: { cards: createExtensionInput([coreExtensionData.reactElement], { optional: false, diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx index 7ae26cf387..4ee26321b7 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx @@ -31,7 +31,7 @@ export const PageBlueprint = createExtensionBlueprint({ path: z => z.string().optional(), }, }, - factory( + *factory( { defaultPath, loader, @@ -50,22 +50,15 @@ export const PageBlueprint = createExtensionBlueprint({ loader({ config, inputs }).then(element => ({ default: () => element })), ); - // TODO(blam): this is a little awkward for optional returns. - // I wonder if we should be using generators or yield instead - // for a better API here. - const outputs = [ - coreExtensionData.routePath(config.path ?? defaultPath!), - coreExtensionData.reactElement( - - - , - ), - ]; + yield coreExtensionData.routePath(config.path ?? defaultPath!); + yield coreExtensionData.reactElement( + + + , + ); if (routeRef) { - return [...outputs, coreExtensionData.routeRef(routeRef)]; + yield coreExtensionData.routeRef(routeRef); } - - return outputs; }, }); diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts index d3d05a5e68..5bc3a8f7c3 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts @@ -27,11 +27,7 @@ describe('ThemeBlueprint', () => { } as AppTheme; it('should create an extension with sensible defaults', () => { - expect( - // todo(blam): we can't inject theme.id as the name here like the old extension creator. - // Wonder if theres a better solution. - ThemeBlueprint.make({ name: 'blob', params: { theme } }), - ).toMatchInlineSnapshot(` + expect(ThemeBlueprint.make({ params: { theme } })).toMatchInlineSnapshot(` { "$$type": "@backstage/ExtensionDefinition", "attachTo": { @@ -43,7 +39,7 @@ describe('ThemeBlueprint', () => { "factory": [Function], "inputs": {}, "kind": "theme", - "name": "blob", + "name": "light", "namespace": "app", "output": [ [Function], @@ -55,7 +51,7 @@ describe('ThemeBlueprint', () => { }); it('should return the theme as an themeDataRef', async () => { - const extension = ThemeBlueprint.make({ name: 'blob', params: { theme } }); + const extension = ThemeBlueprint.make({ params: { theme } }); expect( createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme), diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts index 4ddb5a298a..500946713b 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts @@ -21,6 +21,7 @@ import { createThemeExtension } from './createThemeExtension'; export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', namespace: 'app', + name: ({ theme }) => theme.id, attachTo: { id: 'app', input: 'themes' }, output: [createThemeExtension.themeDataRef], dataRefs: { diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts index 658c0c9709..a059189044 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -22,7 +22,7 @@ import { TranslationBlueprint } from './TranslationBlueprint'; describe('TranslationBlueprint', () => { const translationRef = createTranslationRef({ - id: 'test', + id: 'translationRefId', messages: { test: 'test', }, @@ -38,10 +38,7 @@ describe('TranslationBlueprint', () => { it('should return an extension instance with sane defaults', () => { expect( TranslationBlueprint.make({ - // todo(blam): we can't set the namespace dynamically based of the ResourceType. - // work out if we should wrap this up or another solution. - namespace: messages.id, - name: 'test', + name: 'blob', params: { resource: messages, }, @@ -58,8 +55,8 @@ describe('TranslationBlueprint', () => { "factory": [Function], "inputs": {}, "kind": "translation", - "name": "test", - "namespace": "test", + "name": "blob", + "namespace": "translationRefId", "output": [ [Function], ], @@ -71,6 +68,7 @@ describe('TranslationBlueprint', () => { it('should output a translation data ref', () => { const extension = TranslationBlueprint.make({ + name: 'blob', params: { resource: messages, }, diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts index 23cb5da6a6..581f4d176a 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts @@ -20,6 +20,7 @@ import { TranslationMessages, TranslationResource } from '../translation'; export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', + namespace: ({ resource }) => resource.id, attachTo: { id: 'app', input: 'translations' }, output: [createTranslationExtension.translationDataRef], dataRefs: { From 593fdd4e007c072c37a7d3ba94734dad0ad115b0 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 09:14:05 +0200 Subject: [PATCH 179/204] chore: added SignInPageBlueprint Signed-off-by: blam --- .../extensions/SignInPageBlueprint.test.tsx | 67 +++++++++++++++++++ .../src/extensions/SignInPageBlueprint.tsx | 50 ++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx new file mode 100644 index 0000000000..7bbeb4e5d7 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { SignInPageBlueprint } from './SignInPageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { waitFor } from '@testing-library/react'; + +describe('SignInPageBlueprint', () => { + it('should create an extension with sensible defaults', () => { + expect( + SignInPageBlueprint.make({ + params: { loader: async () => () =>
    }, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "signInPage", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "sign-in-page", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the component as the componentRef', async () => { + const MockSignInPage = () =>
    MockSignInPage
    ; + + const extension = SignInPageBlueprint.make({ + params: { loader: async () => () => }, + }); + + const tester = createExtensionTester(extension); + expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); + + const { getByText } = tester.render(); + + // todo(blam): need a better way to test this, currently fails. + await waitFor(() => { + expect(getByText('MockSignInPage')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx new file mode 100644 index 0000000000..01ba174efe --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx @@ -0,0 +1,50 @@ +/* + * 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, { ComponentType, lazy } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createSignInPageExtension } from './createSignInPageExtension'; +import { SignInPageProps } from '@backstage/core-plugin-api'; +import { ExtensionBoundary } from '../components'; + +export const SignInPageBlueprint = createExtensionBlueprint({ + kind: 'sign-in-page', + attachTo: { id: 'app/root', input: 'signInPage' }, + output: [createSignInPageExtension.componentDataRef], + dataRefs: { + component: createSignInPageExtension.componentDataRef, + }, + *factory( + { + loader, + }: { + loader: (opts: { + config: typeof config; + inputs: typeof inputs; + }) => Promise>; + }, + { config, inputs, node }, + ) { + const ExtensionComponent = lazy(() => + loader({ config, inputs }).then(component => ({ default: component })), + ); + + yield createSignInPageExtension.componentDataRef(props => ( + + + + )); + }, +}); From ac136771b2ca4331b6f54f32cea3657d9b52f827 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 09:34:12 +0200 Subject: [PATCH 180/204] feat: added RouterBlueprint Signed-off-by: blam --- .../src/extensions/RouterBlueprint.test.tsx | 167 ++++++++++++++++++ .../src/extensions/RouterBlueprint.tsx | 45 +++++ 2 files changed, 212 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx new file mode 100644 index 0000000000..3abc967fd8 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx @@ -0,0 +1,167 @@ +/* + * 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 { RouterBlueprint } from './RouterBlueprint'; +import { MemoryRouter } from 'react-router-dom'; +import { render, waitFor } from '@testing-library/react'; +import { createSpecializedApp } from '@backstage/frontend-app-api'; +import { + coreExtensionData, + createExtension, + createExtensionInput, + createExtensionOverrides, +} from '../wiring'; +import { MockConfigApi } from '@backstage/test-utils'; +import { PageBlueprint } from './PageBlueprint'; + +describe('RouterBlueprint', () => { + it('should return an extension when calling make with sensible defaults', () => { + const extension = RouterBlueprint.make({ + params: { + Component: props =>
    {props.children}
    , + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "router", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-router-component", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should work with simple options', async () => { + const extension = RouterBlueprint.make({ + namespace: 'test', + params: { + Component: ({ children }) => ( + +
    {children}
    +
    + ), + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
    , + }, + }), + ], + }), + ], + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router')).toBeInTheDocument(); + }); + }); + + it('should work with complex options and props', async () => { + const extension = RouterBlueprint.make({ + namespace: 'test', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + params: { + Component: ({ inputs, children, config }) => ( + +
    + {children} +
    +
    + ), + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createExtension({ + namespace: 'test', + attachTo: { + id: 'app-router-component:test/test', + input: 'children', + }, + output: [coreExtensionData.reactElement], + *factory() { + yield coreExtensionData.reactElement(
    ); + }, + }), + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
    , + }, + }), + ], + }), + ], + config: new MockConfigApi({ + app: { + extensions: [ + { + 'app-router-component:test/test': { config: { name: 'Robin' } }, + }, + ], + }, + }), + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router-Robin-1')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx new file mode 100644 index 0000000000..9e9cddbdd1 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx @@ -0,0 +1,45 @@ +/* + * 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, { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createRouterExtension } from './createRouterExtension'; + +export const RouterBlueprint = createExtensionBlueprint({ + kind: 'app-router-component', + attachTo: { id: 'app/root', input: 'router' }, + output: [createRouterExtension.componentDataRef], + *factory( + { + Component, + }: { + Component: ComponentType< + PropsWithChildren<{ + inputs: typeof inputs; + config: typeof config; + }> + >; + }, + { config, inputs }, + ) { + const Wrapper = (props: PropsWithChildren<{}>) => ( + + {props.children} + + ); + + yield createRouterExtension.componentDataRef(Wrapper); + }, +}); From b7506f214ea83a5e833261de20e30ec8e92bbe7b Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 09:53:02 +0200 Subject: [PATCH 181/204] feat: added NavLogoBlueprint Signed-off-by: blam --- .../src/extensions/NavLogoBlueprint.test.tsx | 71 +++++++++++++++++++ .../src/extensions/NavLogoBlueprint.ts | 39 ++++++++++ .../src/extensions/RouterBlueprint.tsx | 3 + .../extensions/SignInPageBlueprint.test.tsx | 6 +- 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx new file mode 100644 index 0000000000..fa01862785 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx @@ -0,0 +1,71 @@ +/* + * 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 { NavLogoBlueprint } from './NavLogoBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('NavLogoBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = NavLogoBlueprint.make({ + params: { + logoFull:
    Logo Full
    , + logoIcon:
    Logo Icon
    , + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "logos", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-logo", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return a valid component ref', () => { + const logoFull =
    Logo Full
    ; + const logoIcon =
    Logo Icon
    ; + + const extension = NavLogoBlueprint.make({ + name: 'test', + params: { + logoFull, + logoIcon, + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavLogoBlueprint.dataRefs.logoElements)).toEqual({ + logoFull, + logoIcon, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts new file mode 100644 index 0000000000..d119287f29 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts @@ -0,0 +1,39 @@ +/* + * 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 { createExtensionBlueprint } from '../wiring'; +import { createNavLogoExtension } from './createNavLogoExtension'; + +export const NavLogoBlueprint = createExtensionBlueprint({ + kind: 'nav-logo', + attachTo: { id: 'app/nav', input: 'logos' }, + output: [createNavLogoExtension.logoElementsDataRef], + dataRefs: { + logoElements: createNavLogoExtension.logoElementsDataRef, + }, + *factory({ + logoIcon, + logoFull, + }: { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }) { + yield createNavLogoExtension.logoElementsDataRef({ + logoIcon, + logoFull, + }); + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx index 9e9cddbdd1..bf910eb944 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx @@ -21,6 +21,9 @@ export const RouterBlueprint = createExtensionBlueprint({ kind: 'app-router-component', attachTo: { id: 'app/root', input: 'router' }, output: [createRouterExtension.componentDataRef], + dataRefs: { + component: createRouterExtension.componentDataRef, + }, *factory( { Component, diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx index 7bbeb4e5d7..11fe1420f1 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -48,7 +48,7 @@ describe('SignInPageBlueprint', () => { }); it('should return the component as the componentRef', async () => { - const MockSignInPage = () =>
    MockSignInPage
    ; + const MockSignInPage = () =>
    ; const extension = SignInPageBlueprint.make({ params: { loader: async () => () => }, @@ -57,11 +57,11 @@ describe('SignInPageBlueprint', () => { const tester = createExtensionTester(extension); expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); - const { getByText } = tester.render(); + const { getByTestId } = tester.render(); // todo(blam): need a better way to test this, currently fails. await waitFor(() => { - expect(getByText('MockSignInPage')).toBeInTheDocument(); + expect(getByTestId('mock-sign-in')).toBeInTheDocument(); }); }); }); From b284ecb010272dd725ad043e36cf39e1136e3793 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 11:33:26 +0200 Subject: [PATCH 182/204] chore: reworking how to override the page component Signed-off-by: blam --- .../src/extensions/NavItemBlueprint.ts | 51 +++++++++++++++++++ .../src/extensions/PageBlueprint.test.tsx | 23 ++++----- 2 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts new file mode 100644 index 0000000000..568431c0f5 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts @@ -0,0 +1,51 @@ +/* + * 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 { IconComponent } from '@backstage/core-plugin-api'; +import { RouteRef } from '../routing'; +import { createExtensionBlueprint } from '../wiring'; +import { createNavItemExtension } from './createNavItemExtension'; + +export const NavItemBlueprint = createExtensionBlueprint({ + kind: 'nav-item', + attachTo: { id: 'app/nav', input: 'items' }, + output: [createNavItemExtension.targetDataRef], + dataRefs: { + target: createNavItemExtension.targetDataRef, + }, + factory: ( + { + icon, + routeRef, + }: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + { config }, + ) => [ + createNavItemExtension.targetDataRef({ + title: config.title, + icon, + routeRef, + }), + ], + config: { + schema: ({ title }) => ({ + title: z => z.string().default(title), + }), + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx index e020167f43..5e1d6b7601 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -107,24 +107,23 @@ describe('PageBlueprint', () => { it('should allow defining additional inputs to the extension', async () => { const myPage = PageBlueprint.make({ name: 'test-page', - params: { - loader: async ({ inputs }) => { - return ( -
    - {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} -
    - ); - }, - defaultPath: '/test', - routeRef: mockRouteRef, - }, - /* todo(blam): need to fix the typescript here, as inputs is not the right type, wont let me merge without specifying parent opts */ inputs: { cards: createExtensionInput([coreExtensionData.reactElement], { optional: false, singleton: false, }), }, + factory(originalFactory, { inputs }) { + return originalFactory({ + loader: async () => ( +
    + {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} +
    + ), + defaultPath: '/test', + routeRef: mockRouteRef, + }); + }, }); const CardBlueprint = createExtensionBlueprint({ From 60d1832ce3138cd0e2769f8feaeb33940596ae6d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 13:23:11 +0200 Subject: [PATCH 183/204] chore: added NavItemBlueprint Signed-off-by: blam --- .../src/extensions/NavItemBlueprint.test.tsx | 106 ++++++++++++++++++ .../extensions/SignInPageBlueprint.test.tsx | 2 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx new file mode 100644 index 0000000000..ba7643eee7 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createRouteRef } from '../routing'; +import { NavItemBlueprint } from './NavItemBlueprint'; + +describe('NavItemBlueprint', () => { + const mockRouteRef = createRouteRef(); + const MockIcon = () => null; + + it('should return an extension with sensible defaults', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "items", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "title": { + "default": "TEST", + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-item", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the correct extension data', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'TEST', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); + + it('should allow overriding of the title using config', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension, { + config: { title: 'OVERRIDDEN' }, + }); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'OVERRIDDEN', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx index 11fe1420f1..5ffdd3175b 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -48,7 +48,7 @@ describe('SignInPageBlueprint', () => { }); it('should return the component as the componentRef', async () => { - const MockSignInPage = () =>
    ; + const MockSignInPage = () =>
    ; const extension = SignInPageBlueprint.make({ params: { loader: async () => () => }, From 8d5049b5a5515ec4415fac6d7c4847fe6107f1b2 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 13:44:04 +0200 Subject: [PATCH 184/204] chore: wip Signed-off-by: blam --- .../src/extensions/ComponentBlueprint.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx new file mode 100644 index 0000000000..614196fc09 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx @@ -0,0 +1,69 @@ +/* + * 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 { ComponentRef } from '../components'; +import { createExtensionBlueprint } from '../wiring'; +import { createComponentExtension } from './createComponentExtension'; +import { lazy, ComponentType } from 'react'; + +// this is hard to do with blueprints... no TProps for the elements +export const ComponentBlueprint = createExtensionBlueprint({ + kind: 'component', + attachTo: { id: 'app', input: 'components' }, + output: [createComponentExtension.componentDataRef], + dataRefs: { + component: createComponentExtension.componentDataRef, + }, + factory( + { + ref, + loader, + }: { + ref: ComponentRef; + loader: + | { + lazy: (values: any) => Promise>; + } + | { + sync: (values: any) => ComponentType; + }; + }, + { config, inputs }, + ) { + if ('sync' in loader) { + return [ + createComponentExtension.componentDataRef({ + ref, + impl: loader.sync({ config, inputs }), + }), + ]; + } + + const lazyLoader = loader.lazy; + const ExtensionComponent = lazy(() => + lazyLoader({ config, inputs }).then(Component => ({ + default: Component, + })), + ); + + return [ + createComponentExtension.componentDataRef({ + ref, + impl: ExtensionComponent, + }), + ]; + }, +}); From 7a4eb9bcb5e213d1b5b018caf751ad14ef025387 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 11:30:24 +0200 Subject: [PATCH 185/204] chore: more work for migrating Signed-off-by: blam --- .../src/extensions/ApiBlueprint.test.ts | 66 +++++++++++++++++++ .../src/extensions/ApiBlueprint.ts | 50 ++++++++++++++ .../src/extensions/PageBlueprint.tsx | 9 +-- 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts create mode 100644 packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts new file mode 100644 index 0000000000..99369c6acd --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { createExtensionInput } from '../wiring'; +import { ApiBlueprint } from './ApiBlueprint'; +import { createApiFactory, createApiRef } from '@backstage/core-plugin-api'; + +describe('ApiBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = createApiFactory({ + api, + deps: {}, + factory: () => ({ foo: 'bar' }), + }); + + const extension = ApiBlueprint.make({ + params: { + factory, + }, + }); + + expect(extension).toMatchInlineSnapshot(); + }); + + it('should create an extension with custom factory', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = jest.fn(() => ({ foo: 'bar' })); + + const extension = ApiBlueprint.make({ + config: { + schema: { + test: z => z.string().default('test'), + }, + }, + inputs: { + test: createExtensionInput([ApiBlueprint.dataRefs.factory]), + }, + factory(originalFactory, { config: _config, inputs: _inputs }) { + return originalFactory({ + api, + factory: () => + createApiFactory({ + api, + deps: {}, + factory, + }), + }); + }, + }); + + expect(extension).toMatchInlineSnapshot(); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts new file mode 100644 index 0000000000..dd2130f2cb --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts @@ -0,0 +1,50 @@ +/* + * 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 { createExtensionBlueprint } from '../wiring'; +import { createApiExtension } from './createApiExtension'; +import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api'; + +export const ApiBlueprint = createExtensionBlueprint({ + kind: 'api', + attachTo: { id: 'app', input: 'apis' }, + output: [createApiExtension.factoryDataRef], + dataRefs: { + factory: createApiExtension.factoryDataRef, + }, + *factory( + params: // remove this form. + | { + api: AnyApiRef; + factory: (params: unknown) => AnyApiFactory; + } + | { + factory: AnyApiFactory; + }, + { config, inputs }, + ) { + yield createApiExtension.factoryDataRef( + typeof params.factory === 'function' + ? params.factory({ config, inputs }) + : params.factory, + ); + }, + namespace: params => { + const apiRef = + 'api' in params ? params.api : (params.factory as { api: AnyApiRef }).api; + + return apiRef.id; + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx index 4ee26321b7..506301ffdf 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx @@ -38,16 +38,13 @@ export const PageBlueprint = createExtensionBlueprint({ routeRef, }: { defaultPath?: string; - loader: (opts: { - config: typeof config; - inputs: typeof inputs; - }) => Promise; + loader: () => Promise; routeRef?: RouteRef; }, - { config, inputs, node }, + { config, node }, ) { const ExtensionComponent = lazy(() => - loader({ config, inputs }).then(element => ({ default: () => element })), + loader().then(element => ({ default: () => element })), ); yield coreExtensionData.routePath(config.path ?? defaultPath!); From d3cfdc6e4dcda5393d61c7db87a388068db50086 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 13:15:56 +0200 Subject: [PATCH 186/204] chore: updating API blueprint Signed-off-by: blam Signed-off-by: blam --- .../src/extensions/ApiBlueprint.test.ts | 81 ++++++++++++++++--- .../src/extensions/ApiBlueprint.ts | 27 +------ .../AppRootElementBlueprint.test.tsx | 48 +++++++++++ .../src/extensions/AppRootElementBlueprint.ts | 27 +++++++ .../src/extensions/ComponentBlueprint.tsx | 69 ---------------- .../src/extensions/RouterBlueprint.test.tsx | 22 ++--- .../src/extensions/RouterBlueprint.tsx | 24 +----- .../src/extensions/SignInPageBlueprint.tsx | 9 +-- .../src/wiring/createExtensionBlueprint.ts | 14 ++-- 9 files changed, 176 insertions(+), 145 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts delete mode 100644 packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts index 99369c6acd..c85c021c08 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts @@ -32,7 +32,27 @@ describe('ApiBlueprint', () => { }, }); - expect(extension).toMatchInlineSnapshot(); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); }); it('should create an extension with custom factory', () => { @@ -48,19 +68,62 @@ describe('ApiBlueprint', () => { inputs: { test: createExtensionInput([ApiBlueprint.dataRefs.factory]), }, + namespace: api.id, factory(originalFactory, { config: _config, inputs: _inputs }) { return originalFactory({ - api, - factory: () => - createApiFactory({ - api, - deps: {}, - factory, - }), + factory: createApiFactory({ + api, + deps: {}, + factory, + }), }); }, }); - expect(extension).toMatchInlineSnapshot(); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "test": { + "default": "test", + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": { + "test": { + "$$type": "@backstage/ExtensionInput", + "config": { + "optional": false, + "singleton": false, + }, + "extensionData": [ + [Function], + ], + }, + }, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts index dd2130f2cb..16a20b5130 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts @@ -15,7 +15,7 @@ */ import { createExtensionBlueprint } from '../wiring'; import { createApiExtension } from './createApiExtension'; -import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api'; +import { AnyApiFactory } from '@backstage/core-plugin-api'; export const ApiBlueprint = createExtensionBlueprint({ kind: 'api', @@ -24,27 +24,8 @@ export const ApiBlueprint = createExtensionBlueprint({ dataRefs: { factory: createApiExtension.factoryDataRef, }, - *factory( - params: // remove this form. - | { - api: AnyApiRef; - factory: (params: unknown) => AnyApiFactory; - } - | { - factory: AnyApiFactory; - }, - { config, inputs }, - ) { - yield createApiExtension.factoryDataRef( - typeof params.factory === 'function' - ? params.factory({ config, inputs }) - : params.factory, - ); - }, - namespace: params => { - const apiRef = - 'api' in params ? params.api : (params.factory as { api: AnyApiRef }).api; - - return apiRef.id; + *factory(params: { factory: AnyApiFactory }) { + yield createApiExtension.factoryDataRef(params.factory); }, + namespace: ({ factory }) => factory.api.id, }); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx new file mode 100644 index 0000000000..d9c7d296cc --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { AppRootElementBlueprint } from './AppRootElementBlueprint'; + +describe('AppRootElementBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = AppRootElementBlueprint.make({ + params: { + element:
    , + }, + }); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "elements", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-element", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts new file mode 100644 index 0000000000..1048d531ab --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts @@ -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. + */ +import { coreExtensionData, createExtensionBlueprint } from '../wiring'; + +export const AppRootElementBlueprint = createExtensionBlueprint({ + kind: 'app-root-element', + attachTo: { id: 'app/root', input: 'elements' }, + output: [coreExtensionData.reactElement], + *factory(params: { element: JSX.Element | (() => JSX.Element) }) { + yield coreExtensionData.reactElement( + typeof params.element === 'function' ? params.element() : params.element, + ); + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx deleted file mode 100644 index 614196fc09..0000000000 --- a/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx +++ /dev/null @@ -1,69 +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 { ComponentRef } from '../components'; -import { createExtensionBlueprint } from '../wiring'; -import { createComponentExtension } from './createComponentExtension'; -import { lazy, ComponentType } from 'react'; - -// this is hard to do with blueprints... no TProps for the elements -export const ComponentBlueprint = createExtensionBlueprint({ - kind: 'component', - attachTo: { id: 'app', input: 'components' }, - output: [createComponentExtension.componentDataRef], - dataRefs: { - component: createComponentExtension.componentDataRef, - }, - factory( - { - ref, - loader, - }: { - ref: ComponentRef; - loader: - | { - lazy: (values: any) => Promise>; - } - | { - sync: (values: any) => ComponentType; - }; - }, - { config, inputs }, - ) { - if ('sync' in loader) { - return [ - createComponentExtension.componentDataRef({ - ref, - impl: loader.sync({ config, inputs }), - }), - ]; - } - - const lazyLoader = loader.lazy; - const ExtensionComponent = lazy(() => - lazyLoader({ config, inputs }).then(Component => ({ - default: Component, - })), - ); - - return [ - createComponentExtension.componentDataRef({ - ref, - impl: ExtensionComponent, - }), - ]; - }, -}); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx index 3abc967fd8..ba4123138f 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx @@ -107,16 +107,18 @@ describe('RouterBlueprint', () => { inputs: { children: createExtensionInput([coreExtensionData.reactElement]), }, - params: { - Component: ({ inputs, children, config }) => ( - -
    - {children} -
    -
    - ), + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( + +
    + {children} +
    +
    + ), + }); }, }); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx index bf910eb944..e584a5654c 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ComponentType, PropsWithChildren } from 'react'; +import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; import { createRouterExtension } from './createRouterExtension'; @@ -24,25 +24,7 @@ export const RouterBlueprint = createExtensionBlueprint({ dataRefs: { component: createRouterExtension.componentDataRef, }, - *factory( - { - Component, - }: { - Component: ComponentType< - PropsWithChildren<{ - inputs: typeof inputs; - config: typeof config; - }> - >; - }, - { config, inputs }, - ) { - const Wrapper = (props: PropsWithChildren<{}>) => ( - - {props.children} - - ); - - yield createRouterExtension.componentDataRef(Wrapper); + *factory({ Component }: { Component: ComponentType> }) { + yield createRouterExtension.componentDataRef(Component); }, }); diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx index 01ba174efe..fc779f58ab 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx @@ -30,15 +30,12 @@ export const SignInPageBlueprint = createExtensionBlueprint({ { loader, }: { - loader: (opts: { - config: typeof config; - inputs: typeof inputs; - }) => Promise>; + loader: () => Promise>; }, - { config, inputs, node }, + { node }, ) { const ExtensionComponent = lazy(() => - loader({ config, inputs }).then(component => ({ default: component })), + loader().then(component => ({ default: component })), ); yield createSignInPageExtension.componentDataRef(props => ( diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 81137f47e2..f749f7d412 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -312,9 +312,9 @@ class ExtensionBlueprintImpl< > > > { - const optionsSchema = - typeof this.options.config?.schema === 'function' - ? this.options.config?.schema(args.params!) + const optionsSchema = // can remove this args.params check with the split apart of .make + typeof this.options.config?.schema === 'function' && args.params + ? this.options.config?.schema(args.params) : this.options.config?.schema; const schema = { @@ -323,13 +323,13 @@ class ExtensionBlueprintImpl< } as TConfigSchema & TExtensionConfigSchema; const namespace = - typeof this.options.namespace === 'function' - ? this.options.namespace(args.params!) + typeof this.options.namespace === 'function' && args.params + ? this.options.namespace(args.params) : this.options.namespace; const name = - typeof this.options.name === 'function' - ? this.options.name(args.params!) + typeof this.options.name === 'function' && args.params + ? this.options.name(args.params) : this.options.name; return createExtension({ From ab70dc33d7d313729c69f849c0bdae6c2d20abfe Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 08:11:00 +0200 Subject: [PATCH 187/204] chore: implementing the last of the blueprints Signed-off-by: blam --- .../AppRootWrapperBlueprint.test.tsx | 127 ++++++++++++++++++ .../extensions/AppRootWrapperBlueprint.tsx | 36 +++++ 2 files changed, 163 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx new file mode 100644 index 0000000000..b67fc3664d --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { PageBlueprint } from './PageBlueprint'; +import { waitFor } from '@testing-library/react'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '../wiring'; +import { each } from 'lodash'; + +describe('AppRootWrapperBlueprint', () => { + it('should return an extension with sensible defaults', () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
    Hello
    , + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "elements", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-wrapper", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should render the simple component wrapper', async () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
    Hello
    , + }, + }); + + const { getByText } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
    , + }, + }), + ) + .add(extension) + .render(); + + await waitFor(() => expect(getByText('Hello')).toBeInTheDocument()); + }); + + it('should render the complex component wrapper', async () => { + const extension = AppRootWrapperBlueprint.make({ + namespace: 'ns', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( +
    + {children} +
    + ), + }); + }, + }); + + const { getByText, getByTestId } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
    Hi
    , + }, + }), + ) + .add(extension, { config: { name: 'Robin' } }) + .add( + createExtension({ + attachTo: { id: 'app-root-wrapper:ns/test', input: 'children' }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
    Its Me
    )], + }), + ) + .render(); + + await waitFor(() => { + expect(getByText('Its Me')).toBeInTheDocument(); + expect(getByText('Hi')).toBeInTheDocument(); + expect(getByTestId('Robin-1')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx new file mode 100644 index 0000000000..5bcbaa49bd --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx @@ -0,0 +1,36 @@ +/* + * 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 { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; + +export const AppRootWrapperBlueprint = createExtensionBlueprint({ + kind: 'app-root-wrapper', + attachTo: { id: 'app/root', input: 'elements' }, + output: [createAppRootWrapperExtension.componentDataRef], + dataRefs: { + component: createAppRootWrapperExtension.componentDataRef, + }, + *factory(params: { Component: ComponentType> }) { + // todo(blam): not sure that this wrapping is even necessary anymore. + const Component = (props: PropsWithChildren<{}>) => { + return {props.children}; + }; + + yield createAppRootWrapperExtension.componentDataRef(Component); + }, +}); From 4bc224cc832e8bc9958b585d45b96b0e2bd4d376 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 11:21:16 +0200 Subject: [PATCH 188/204] chore: updating tests and making them work properly Signed-off-by: blam --- .../src/extensions/ApiBlueprint.test.ts | 2 ++ .../AppRootElementBlueprint.test.tsx | 1 + .../AppRootWrapperBlueprint.test.tsx | 4 ++-- .../extensions/AppRootWrapperBlueprint.tsx | 3 ++- .../src/extensions/NavItemBlueprint.test.tsx | 1 + .../src/extensions/NavLogoBlueprint.test.tsx | 1 + .../src/extensions/PageBlueprint.test.tsx | 1 + .../src/extensions/RouterBlueprint.test.tsx | 1 + .../extensions/SignInPageBlueprint.test.tsx | 23 +++++++++++++++---- .../src/extensions/ThemeBlueprint.test.ts | 1 + .../extensions/TranslationBlueprint.test.ts | 1 + 11 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts index c85c021c08..f070956e96 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts @@ -49,6 +49,7 @@ describe('ApiBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } @@ -121,6 +122,7 @@ describe('ApiBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx index d9c7d296cc..3fb8d7b37a 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx @@ -40,6 +40,7 @@ describe('AppRootElementBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx index b67fc3664d..2ff46f0785 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx @@ -23,7 +23,6 @@ import { createExtension, createExtensionInput, } from '../wiring'; -import { each } from 'lodash'; describe('AppRootWrapperBlueprint', () => { it('should return an extension with sensible defaults', () => { @@ -38,7 +37,7 @@ describe('AppRootWrapperBlueprint', () => { "$$type": "@backstage/ExtensionDefinition", "attachTo": { "id": "app/root", - "input": "elements", + "input": "wrappers", }, "configSchema": undefined, "disabled": false, @@ -50,6 +49,7 @@ describe('AppRootWrapperBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx index 5bcbaa49bd..4e8cab8f1b 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; @@ -20,7 +21,7 @@ import { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', - attachTo: { id: 'app/root', input: 'elements' }, + attachTo: { id: 'app/root', input: 'wrappers' }, output: [createAppRootWrapperExtension.componentDataRef], dataRefs: { component: createAppRootWrapperExtension.componentDataRef, diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx index ba7643eee7..90c9cdf9b4 100644 --- a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx @@ -60,6 +60,7 @@ describe('NavItemBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx index fa01862785..4a96475cf5 100644 --- a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx @@ -43,6 +43,7 @@ describe('NavLogoBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx index 5e1d6b7601..c1350b3ddb 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -76,6 +76,7 @@ describe('PageBlueprint', () => { "toString": [Function], }, ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx index ba4123138f..018c3fc570 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx @@ -52,6 +52,7 @@ describe('RouterBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx index 5ffdd3175b..f97798a64c 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { SignInPageBlueprint } from './SignInPageBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import { coreExtensionData, createExtension } from '../wiring'; describe('SignInPageBlueprint', () => { it('should create an extension with sensible defaults', () => { @@ -41,6 +43,7 @@ describe('SignInPageBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } @@ -51,17 +54,29 @@ describe('SignInPageBlueprint', () => { const MockSignInPage = () =>
    ; const extension = SignInPageBlueprint.make({ + name: 'test', params: { loader: async () => () => }, }); const tester = createExtensionTester(extension); + expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); - const { getByTestId } = tester.render(); + createExtensionTester( + createExtension({ + name: 'dummy', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + element: coreExtensionData.reactElement, + }, + factory: () => ({ element:
    }), + }), + ) + .add(extension) + .render(); - // todo(blam): need a better way to test this, currently fails. await waitFor(() => { - expect(getByTestId('mock-sign-in')).toBeInTheDocument(); + expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument(); }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts index 5bc3a8f7c3..92acd8eeba 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts @@ -44,6 +44,7 @@ describe('ThemeBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts index a059189044..e2a979c097 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -60,6 +60,7 @@ describe('TranslationBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } From 40808de76787f4564ee3fa47d72756b04c61ec8e Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Wed, 5 Jun 2024 17:30:20 +0900 Subject: [PATCH 189/204] Add spec.type to Domain model Signed-off-by: Joe Van Alstyne --- .../examples/domains/artists-domain.yaml | 1 + .../examples/domains/playback-domain.yaml | 1 + .../src/kinds/DomainEntityV1alpha1.test.ts | 11 +++++++++++ .../catalog-model/src/kinds/DomainEntityV1alpha1.ts | 1 + .../src/schema/kinds/Domain.v1alpha1.schema.json | 9 ++++++++- 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml index 598f8500aa..02370d9a14 100644 --- a/packages/catalog-model/examples/domains/artists-domain.yaml +++ b/packages/catalog-model/examples/domains/artists-domain.yaml @@ -13,3 +13,4 @@ metadata: spec: owner: team-a subdomainOf: audio + type: product-group diff --git a/packages/catalog-model/examples/domains/playback-domain.yaml b/packages/catalog-model/examples/domains/playback-domain.yaml index 58b5afe56e..e058ebf0a2 100644 --- a/packages/catalog-model/examples/domains/playback-domain.yaml +++ b/packages/catalog-model/examples/domains/playback-domain.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: user:frank.tiernan subdomainOf: audio + type: product-group diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts index 3c1e7f0a6d..181a696192 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts @@ -32,6 +32,7 @@ describe('DomainV1alpha1Validator', () => { spec: { owner: 'me', subdomainOf: 'parent-domain', + type: 'domain-type', }, }; }); @@ -84,4 +85,14 @@ describe('DomainV1alpha1Validator', () => { (entity as any).spec.subdomainOf = ''; await expect(validator.check(entity)).rejects.toThrow(/subdomainOf/); }); + + it('accepts missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index 1fe3dfb522..765329abb4 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -33,6 +33,7 @@ export interface DomainEntityV1alpha1 extends Entity { spec: { owner: string; subdomainOf?: string; + type?: string; }; } diff --git a/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json index 7d0d7e25ff..e4a6c29dd1 100644 --- a/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json @@ -12,7 +12,8 @@ }, "spec": { "owner": "artist-relations-team", - "subdomainOf": "audio" + "subdomainOf": "audio", + "type": "product-group" } } ], @@ -45,6 +46,12 @@ "description": "An entity reference to another domain of which the domain is a part.", "examples": ["audio"], "minLength": 1 + }, + "type": { + "type": "string", + "description": "The type of domain. There is currently no enforced set of values for this field, so it is left up to the adopting organization to choose a nomenclature that matches their catalog hierarchy.", + "examples": ["product-group", "bundle"], + "minLength": 1 } } } From 218abd5473183912064c5b86cccbd3c92147fe01 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Wed, 5 Jun 2024 17:34:36 +0900 Subject: [PATCH 190/204] Update api-report.md Signed-off-by: Joe Van Alstyne --- packages/catalog-model/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 2d91fa44a5..7b06e80fc5 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -123,6 +123,7 @@ interface DomainEntityV1alpha1 extends Entity { spec: { owner: string; subdomainOf?: string; + type?: string; }; } export { DomainEntityV1alpha1 as DomainEntity }; From 34fa803f2961136a6ab367c8e362f34f86404bc3 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Wed, 5 Jun 2024 17:36:17 +0900 Subject: [PATCH 191/204] Add changeset Signed-off-by: Joe Van Alstyne --- .changeset/young-birds-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/young-birds-push.md diff --git a/.changeset/young-birds-push.md b/.changeset/young-birds-push.md new file mode 100644 index 0000000000..5b8ab3a81f --- /dev/null +++ b/.changeset/young-birds-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Introduce an optional spec.type attribute on the Domain entity kind From 4e392ebdc49d3afbfa35bd767757bdcf839516d8 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Wed, 5 Jun 2024 17:42:20 +0900 Subject: [PATCH 192/204] Update documentation Signed-off-by: Joe Van Alstyne --- docs/features/software-catalog/descriptor-format.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 416eb27fbd..1287a0780e 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1303,6 +1303,18 @@ which the domain is a part, e.g. `audio`. This field is optional. | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | | [`Domain`](#kind-domain) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | +### `spec.type` [optional] + +The type of domain. There is currently no enforced set of values for this field, +so it is left up to the adopting organization to choose a nomenclature that +matches their catalog hierarchy. This field is optional. + +Some common values for this field could be: + +- `product-area` +- `product-group` +- `bundle` + ## Kind: Location Describes the following entity kind: From ccb514f24d0c0d0b7e0d5d14bc33c94583ff68e4 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Mon, 10 Jun 2024 18:54:32 +0900 Subject: [PATCH 193/204] Add spec.type to Sytem model Signed-off-by: Joe Van Alstyne --- .../systems/artist-engagement-portal-system.yaml | 1 + .../examples/systems/audio-playback-system.yaml | 1 + .../examples/systems/podcast-system.yaml | 1 + .../src/kinds/SystemEntityV1alpha1.test.ts | 11 +++++++++++ .../catalog-model/src/kinds/SystemEntityV1alpha1.ts | 1 + .../src/schema/kinds/System.v1alpha1.schema.json | 9 ++++++++- 6 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml b/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml index 8de3c00880..17fe21ba69 100644 --- a/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml +++ b/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml @@ -8,3 +8,4 @@ metadata: spec: owner: team-a domain: artists + type: service diff --git a/packages/catalog-model/examples/systems/audio-playback-system.yaml b/packages/catalog-model/examples/systems/audio-playback-system.yaml index 7430ae2ff5..f1edb7f17d 100644 --- a/packages/catalog-model/examples/systems/audio-playback-system.yaml +++ b/packages/catalog-model/examples/systems/audio-playback-system.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: team-c domain: playback + type: feature-set diff --git a/packages/catalog-model/examples/systems/podcast-system.yaml b/packages/catalog-model/examples/systems/podcast-system.yaml index 47a2f7ac9f..77f53925c1 100644 --- a/packages/catalog-model/examples/systems/podcast-system.yaml +++ b/packages/catalog-model/examples/systems/podcast-system.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: team-b domain: playback + type: feature-set diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts index fd78633a89..915f455e79 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts @@ -32,6 +32,7 @@ describe('SystemV1alpha1Validator', () => { spec: { owner: 'me', domain: 'domain', + type: 'system-type', }, }; }); @@ -84,4 +85,14 @@ describe('SystemV1alpha1Validator', () => { (entity as any).spec.domain = ''; await expect(validator.check(entity)).rejects.toThrow(/domain/); }); + + it('accepts missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 88d405ec5b..37ecee8f6c 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -33,6 +33,7 @@ export interface SystemEntityV1alpha1 extends Entity { spec: { owner: string; domain?: string; + type?: string; }; } diff --git a/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json index 2cdbc37076..f91fe751dc 100644 --- a/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json @@ -12,7 +12,8 @@ }, "spec": { "owner": "artist-relations-team", - "domain": "artists" + "domain": "artists", + "type": "service" } } ], @@ -45,6 +46,12 @@ "description": "An entity reference to the domain that the system belongs to.", "examples": ["artists"], "minLength": 1 + }, + "type": { + "type": "string", + "description": "The type of system. There is currently no enforced set of values for this field, so it is left up to the adopting organization to choose a nomenclature that matches their catalog hierarchy.", + "examples": ["product", "service", "feature-set"], + "minLength": 1 } } } From 2d6003457426d4a9caf8d12df421e9865bb6f482 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Mon, 10 Jun 2024 18:58:00 +0900 Subject: [PATCH 194/204] Update documentation Signed-off-by: Joe Van Alstyne --- docs/features/software-catalog/descriptor-format.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 1287a0780e..57c2283586 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1243,6 +1243,18 @@ system belongs to, e.g. `artists`. This field is optional. | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | | [`Domain`](#kind-domain) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | +### `spec.type` [optional] + +The type of system. There is currently no enforced set of values for this field, +so it is left up to the adopting organization to choose a nomenclature that +matches their catalog hierarchy. This field is optional. + +Some common values for this field could be: + +- `product` +- `service` +- `feature-set` + ## Kind: Domain Describes the following entity kind: From 55a5ef2c3212c2aca2221cc94716f1b18a9d18c2 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Mon, 10 Jun 2024 19:03:17 +0900 Subject: [PATCH 195/204] Update changeset Signed-off-by: Joe Van Alstyne --- .changeset/young-birds-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/young-birds-push.md b/.changeset/young-birds-push.md index 5b8ab3a81f..54afa48aaf 100644 --- a/.changeset/young-birds-push.md +++ b/.changeset/young-birds-push.md @@ -2,4 +2,4 @@ '@backstage/catalog-model': minor --- -Introduce an optional spec.type attribute on the Domain entity kind +Introduce an optional spec.type attribute on the Domain and System entity kinds From ef71b00bbe0b2878379c73d67747751961e7cba2 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Mon, 10 Jun 2024 19:09:37 +0900 Subject: [PATCH 196/204] Update api-report.md Signed-off-by: Joe Van Alstyne --- packages/catalog-model/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 7b06e80fc5..de941e2502 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -455,6 +455,7 @@ interface SystemEntityV1alpha1 extends Entity { spec: { owner: string; domain?: string; + type?: string; }; } export { SystemEntityV1alpha1 as SystemEntity }; From 1a0d955bb526e47ef8d635e267adb56868207e25 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Fri, 9 Aug 2024 18:17:19 +0900 Subject: [PATCH 197/204] Add test to DomainEntityV1alpha1.test.ts Signed-off-by: Joe Van Alstyne --- .../catalog-model/src/kinds/DomainEntityV1alpha1.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts index 181a696192..ce35441fc8 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts @@ -95,4 +95,9 @@ describe('DomainV1alpha1Validator', () => { (entity as any).spec.type = ''; await expect(validator.check(entity)).rejects.toThrow(/type/); }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); From 33489f73abe9032304c37c5777fbb5e91bba6b53 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Fri, 9 Aug 2024 18:17:33 +0900 Subject: [PATCH 198/204] Add test to SystemEntityV1alpha1.test.ts Signed-off-by: Joe Van Alstyne --- .../catalog-model/src/kinds/SystemEntityV1alpha1.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts index 915f455e79..6ea295f190 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts @@ -95,4 +95,9 @@ describe('SystemV1alpha1Validator', () => { (entity as any).spec.type = ''; await expect(validator.check(entity)).rejects.toThrow(/type/); }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); From 30e13a6b822d0c9db7bec55411b0e7cb43d9b9c6 Mon Sep 17 00:00:00 2001 From: Joe Van Alstyne Date: Fri, 9 Aug 2024 18:25:16 +0900 Subject: [PATCH 199/204] Fix test order in DomainEntityV1alpha1.test.ts, SystemEntityV1alpha1.test.ts Signed-off-by: Joe Van Alstyne --- .../src/kinds/DomainEntityV1alpha1.test.ts | 10 +++++----- .../src/kinds/SystemEntityV1alpha1.test.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts index ce35441fc8..61408b7e80 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts @@ -91,13 +91,13 @@ describe('DomainV1alpha1Validator', () => { await expect(validator.check(entity)).resolves.toBe(true); }); - it('rejects empty type', async () => { - (entity as any).spec.type = ''; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - it('rejects wrong type', async () => { (entity as any).spec.type = 7; await expect(validator.check(entity)).rejects.toThrow(/type/); }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts index 6ea295f190..828fe7a648 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts @@ -91,13 +91,13 @@ describe('SystemV1alpha1Validator', () => { await expect(validator.check(entity)).resolves.toBe(true); }); - it('rejects empty type', async () => { - (entity as any).spec.type = ''; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - it('rejects wrong type', async () => { (entity as any).spec.type = 7; await expect(validator.check(entity)).rejects.toThrow(/type/); }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); From e6c763f76b6d19865a320c86a361fbb3ef5035f9 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 15:12:47 +0200 Subject: [PATCH 200/204] chore: getting the tests working MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 410 +++++++++++++++--- .../ApiBlueprint.test.ts | 3 +- .../ApiBlueprint.ts | 4 +- .../AppRootElementBlueprint.test.tsx | 0 .../AppRootElementBlueprint.ts | 1 + .../AppRootWrapperBlueprint.test.tsx | 7 +- .../AppRootWrapperBlueprint.tsx | 3 +- .../IconBundleBlueprint.ts | 0 .../NavItemBlueprint.test.tsx | 1 - .../NavItemBlueprint.ts | 12 +- .../NavLogoBlueprint.test.tsx | 0 .../NavLogoBlueprint.ts | 3 +- .../PageBlueprint.test.tsx | 2 +- .../PageBlueprint.tsx | 5 +- .../RouterBlueprint.test.tsx | 2 +- .../RouterBlueprint.tsx | 3 +- .../SignInPageBlueprint.test.tsx | 0 .../SignInPageBlueprint.tsx | 3 +- .../ThemeBlueprint.test.ts | 3 +- .../ThemeBlueprint.ts | 4 +- .../TranslationBlueprint.test.ts | 2 +- .../TranslationBlueprint.ts | 4 +- .../src/blueprints/index.ts | 27 ++ .../src/extensions/createApiExtension.ts | 5 +- .../createAppRootElementExtension.ts | 1 + .../createAppRootWrapperExtension.tsx | 1 + .../src/extensions/createNavItemExtension.tsx | 2 + .../src/extensions/createNavLogoExtension.tsx | 2 + .../src/extensions/createPageExtension.tsx | 1 + .../src/extensions/createRouterExtension.tsx | 1 + .../extensions/createSignInPageExtension.tsx | 1 + .../src/extensions/createThemeExtension.ts | 10 +- .../extensions/createTranslationExtension.ts | 10 +- .../src/extensions/index.ts | 1 - packages/frontend-plugin-api/src/index.ts | 1 + .../wiring/createExtensionBlueprint.test.tsx | 95 +--- .../src/wiring/createExtensionBlueprint.ts | 224 +++++----- .../src/app/createExtensionTester.tsx | 5 +- 38 files changed, 578 insertions(+), 281 deletions(-) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ApiBlueprint.test.ts (97%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ApiBlueprint.ts (91%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootElementBlueprint.test.tsx (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootElementBlueprint.ts (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootWrapperBlueprint.test.tsx (95%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootWrapperBlueprint.tsx (92%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/IconBundleBlueprint.ts (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavItemBlueprint.test.tsx (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavItemBlueprint.ts (86%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavLogoBlueprint.test.tsx (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavLogoBlueprint.ts (92%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/PageBlueprint.test.tsx (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/PageBlueprint.tsx (97%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/RouterBlueprint.test.tsx (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/RouterBlueprint.tsx (92%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/SignInPageBlueprint.test.tsx (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/SignInPageBlueprint.tsx (93%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ThemeBlueprint.test.ts (94%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ThemeBlueprint.ts (91%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/TranslationBlueprint.test.ts (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/TranslationBlueprint.ts (90%) create mode 100644 packages/frontend-plugin-api/src/blueprints/index.ts diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index a8f8ce591b..2d3f1d3278 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -189,6 +189,27 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef; }; +// @public (undocumented) +export const ApiBlueprint: ExtensionBlueprint< + 'api', + undefined, + undefined, + { + factory: AnyApiFactory; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + factory: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + } +>; + export { ApiFactory }; export { ApiHolder }; @@ -240,6 +261,50 @@ export interface AppNodeSpec { readonly source?: BackstagePlugin; } +// @public (undocumented) +export const AppRootElementBlueprint: ExtensionBlueprint< + 'app-root-element', + undefined, + undefined, + { + element: JSX.Element | (() => JSX.Element); + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + never +>; + +// @public (undocumented) +export const AppRootWrapperBlueprint: ExtensionBlueprint< + 'app-root-wrapper', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >; + } +>; + export { AppTheme }; export { AppThemeApi }; @@ -381,7 +446,7 @@ export type CoreNotFoundErrorPageProps = { // @public (undocumented) export type CoreProgressProps = {}; -// @public (undocumented) +// @public @deprecated (undocumented) export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -425,7 +490,7 @@ export { createApiFactory }; export { createApiRef }; -// @public +// @public @deprecated export function createAppRootElementExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -447,7 +512,7 @@ export function createAppRootElementExtension< }) => JSX_2.Element); }): ExtensionDefinition; -// @public +// @public @deprecated export function createAppRootWrapperExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -675,7 +740,7 @@ export type CreateExtensionBlueprintOptions< }, > = { kind: TKind; - namespace?: TNamespace | ((params: TParams) => TNamespace); + namespace?: TNamespace; attachTo: { id: string; input: string; @@ -683,9 +748,9 @@ export type CreateExtensionBlueprintOptions< disabled?: boolean; inputs?: TInputs; output: Array; - name?: TName | ((params: TParams) => TName); + name?: TName; config?: { - schema: TConfigSchema | ((params: TParams) => TConfigSchema); + schema: TConfigSchema; }; factory( params: TParams, @@ -824,7 +889,7 @@ export function createExternalRouteRef< } >; -// @public +// @public @deprecated export function createNavItemExtension(options: { namespace?: string; name?: string; @@ -859,7 +924,7 @@ export namespace createNavItemExtension { >; } -// @public +// @public @deprecated export function createNavLogoExtension(options: { name?: string; namespace?: string; @@ -888,7 +953,7 @@ export namespace createNavLogoExtension { >; } -// @public +// @public @deprecated export function createPageExtension< TConfig extends { path: string; @@ -958,7 +1023,7 @@ export function createRouteRef< } >; -// @public +// @public @deprecated export function createRouterExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -997,7 +1062,7 @@ export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, ): PortableSchema; -// @public (undocumented) +// @public @deprecated (undocumented) export function createSignInPageExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -1036,7 +1101,7 @@ export function createSubRouteRef< parent: RouteRef; }): MakeSubRouteRef, ParentParams>; -// @public (undocumented) +// @public @deprecated (undocumented) export function createThemeExtension( theme: AppTheme, ): ExtensionDefinition< @@ -1049,7 +1114,7 @@ export function createThemeExtension( string | undefined >; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createThemeExtension { const // (undocumented) themeDataRef: ConfigurableExtensionDataRef< @@ -1059,7 +1124,7 @@ export namespace createThemeExtension { >; } -// @public (undocumented) +// @public @deprecated (undocumented) export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; @@ -1073,7 +1138,7 @@ export function createTranslationExtension(options: { string | undefined >; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createTranslationExtension { const // (undocumented) translationDataRef: ConfigurableExtensionDataRef< @@ -1153,9 +1218,31 @@ export interface ExtensionBlueprint< > { // (undocumented) dataRefs: TDataRefs; + // (undocumented) make< TNewNamespace extends string | undefined, TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + makeWithOverrides< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -1170,55 +1257,48 @@ export interface ExtensionBlueprint< } >; }, - >( - args: { - namespace?: TNewNamespace; - name?: TNewName; - attachTo?: { - id: string; - input: string; + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; }; - disabled?: boolean; - inputs?: TExtraInputs & { - [KName in keyof TInputs]?: `Error: Input '${KName & - string}' is already defined in parent definition`; - }; - output?: Array; - config?: { - schema: TExtensionConfigSchema & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; }; - }; - } & ( - | ({ - factory( - originalFactory: ( - params: TParams, - context?: { - config?: TConfig; - inputs?: Expand>; - }, - ) => ExtensionDataContainer, - context: { - node: AppNode; - config: TConfig & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - inputs: Expand>; - }, - ): Iterable; - } & VerifyExtensionFactoryOutput< - AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - UFactoryOutput - >) - | { - params: TParams; - } - ), - ): ExtensionDefinition< + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -1616,6 +1696,73 @@ export interface LegacyExtensionInput< export { microsoftAuthApiRef }; +// @public (undocumented) +export const NavItemBlueprint: ExtensionBlueprint< + 'nav-item', + undefined, + undefined, + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + {}, + {}, + { + target: ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + } +>; + +// @public (undocumented) +export const NavLogoBlueprint: ExtensionBlueprint< + 'nav-logo', + undefined, + undefined, + { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }, + ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >, + {}, + {}, + {}, + { + logoElements: ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >; + } +>; + export { OAuthApi }; export { OAuthRequestApi }; @@ -1634,6 +1781,35 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; +// @public (undocumented) +export const PageBlueprint: ExtensionBlueprint< + 'page', + undefined, + undefined, + { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + RouteRef & { + optional: true; + } + >, + {}, + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + never +>; + export { PendingOAuthRequest }; // @public (undocumented) @@ -1699,6 +1875,35 @@ export type RouteFunc = ( : readonly [params: TParams] ) => string; +// @public (undocumented) +export const RouterBlueprint: ExtensionBlueprint< + 'app-router-component', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >; + } +>; + // @public export interface RouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, @@ -1733,6 +1938,31 @@ export { SessionApi }; export { SessionState }; +// @public (undocumented) +export const SignInPageBlueprint: ExtensionBlueprint< + 'sign-in-page', + undefined, + undefined, + { + loader: () => Promise>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >; + } +>; + export { StorageApi }; export { storageApiRef }; @@ -1751,6 +1981,62 @@ export interface SubRouteRef< readonly T: TParams; } +// @public (undocumented) +export const ThemeBlueprint: ExtensionBlueprint< + 'theme', + 'app', + undefined, + { + theme: AppTheme; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + theme: ConfigurableExtensionDataRef; + } +>; + +// @public (undocumented) +export const TranslationBlueprint: ExtensionBlueprint< + 'translation', + undefined, + undefined, + { + resource: TranslationResource | TranslationMessages; + }, + ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >, + {}, + {}, + {}, + { + translation: ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >; + } +>; + export { TranslationMessages }; export { TranslationMessagesOptions }; diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts similarity index 97% rename from packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts rename to packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts index f070956e96..3b865a78bb 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts @@ -30,6 +30,7 @@ describe('ApiBlueprint', () => { params: { factory, }, + namespace: 'test', }); expect(extension).toMatchInlineSnapshot(` @@ -60,7 +61,7 @@ describe('ApiBlueprint', () => { const api = createApiRef<{ foo: string }>({ id: 'test' }); const factory = jest.fn(() => ({ foo: 'bar' })); - const extension = ApiBlueprint.make({ + const extension = ApiBlueprint.makeWithOverrides({ config: { schema: { test: z => z.string().default('test'), diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts similarity index 91% rename from packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts index 16a20b5130..da12faef1a 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createExtensionBlueprint } from '../wiring'; -import { createApiExtension } from './createApiExtension'; +import { createApiExtension } from '../extensions/createApiExtension'; import { AnyApiFactory } from '@backstage/core-plugin-api'; +/** @public */ export const ApiBlueprint = createExtensionBlueprint({ kind: 'api', attachTo: { id: 'app', input: 'apis' }, @@ -27,5 +28,4 @@ export const ApiBlueprint = createExtensionBlueprint({ *factory(params: { factory: AnyApiFactory }) { yield createApiExtension.factoryDataRef(params.factory); }, - namespace: ({ factory }) => factory.api.id, }); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts similarity index 98% rename from packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts index 1048d531ab..e44942979d 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts @@ -15,6 +15,7 @@ */ import { coreExtensionData, createExtensionBlueprint } from '../wiring'; +/** @public */ export const AppRootElementBlueprint = createExtensionBlueprint({ kind: 'app-root-element', attachTo: { id: 'app/root', input: 'elements' }, diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx similarity index 95% rename from packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx index 2ff46f0785..f7feab1d46 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx @@ -78,7 +78,7 @@ describe('AppRootWrapperBlueprint', () => { }); it('should render the complex component wrapper', async () => { - const extension = AppRootWrapperBlueprint.make({ + const extension = AppRootWrapperBlueprint.makeWithOverrides({ namespace: 'ns', name: 'test', config: { @@ -94,6 +94,9 @@ describe('AppRootWrapperBlueprint', () => { Component: ({ children }) => (
    {children} + {inputs.children.flatMap(c => + c.get(coreExtensionData.reactElement), + )}
    ), }); @@ -119,9 +122,9 @@ describe('AppRootWrapperBlueprint', () => { .render(); await waitFor(() => { - expect(getByText('Its Me')).toBeInTheDocument(); expect(getByText('Hi')).toBeInTheDocument(); expect(getByTestId('Robin-1')).toBeInTheDocument(); + expect(getByText('Its Me')).toBeInTheDocument(); }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx similarity index 92% rename from packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx index 4e8cab8f1b..db63550880 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -17,8 +17,9 @@ import React from 'react'; import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; -import { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; +import { createAppRootWrapperExtension } from '../extensions/createAppRootWrapperExtension'; +/** @public */ export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', attachTo: { id: 'app/root', input: 'wrappers' }, diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts similarity index 100% rename from packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx similarity index 98% rename from packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx index 90c9cdf9b4..94b9e7b8e6 100644 --- a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx @@ -44,7 +44,6 @@ describe('NavItemBlueprint', () => { "additionalProperties": false, "properties": { "title": { - "default": "TEST", "type": "string", }, }, diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts similarity index 86% rename from packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts index 568431c0f5..5391d0dec2 100644 --- a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -17,8 +17,9 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { RouteRef } from '../routing'; import { createExtensionBlueprint } from '../wiring'; -import { createNavItemExtension } from './createNavItemExtension'; +import { createNavItemExtension } from '../extensions/createNavItemExtension'; +/** @public */ export const NavItemBlueprint = createExtensionBlueprint({ kind: 'nav-item', attachTo: { id: 'app/nav', input: 'items' }, @@ -30,6 +31,7 @@ export const NavItemBlueprint = createExtensionBlueprint({ { icon, routeRef, + title, }: { title: string; icon: IconComponent; @@ -38,14 +40,14 @@ export const NavItemBlueprint = createExtensionBlueprint({ { config }, ) => [ createNavItemExtension.targetDataRef({ - title: config.title, + title: config.title ?? title, icon, routeRef, }), ], config: { - schema: ({ title }) => ({ - title: z => z.string().default(title), - }), + schema: { + title: z => z.string().optional(), + }, }, }); diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts similarity index 92% rename from packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts index d119287f29..f31b3cec9b 100644 --- a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts @@ -15,8 +15,9 @@ */ import { createExtensionBlueprint } from '../wiring'; -import { createNavLogoExtension } from './createNavLogoExtension'; +import { createNavLogoExtension } from '../extensions/createNavLogoExtension'; +/** @public */ export const NavLogoBlueprint = createExtensionBlueprint({ kind: 'nav-logo', attachTo: { id: 'app/nav', input: 'logos' }, diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx similarity index 98% rename from packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx index c1350b3ddb..c4bcb4b467 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -106,7 +106,7 @@ describe('PageBlueprint', () => { }); it('should allow defining additional inputs to the extension', async () => { - const myPage = PageBlueprint.make({ + const myPage = PageBlueprint.makeWithOverrides({ name: 'test-page', inputs: { cards: createExtensionInput([coreExtensionData.reactElement], { diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx similarity index 97% rename from packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index 506301ffdf..bfcf4a7934 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -18,6 +18,7 @@ import { RouteRef } from '../routing'; import { coreExtensionData, createExtensionBlueprint } from '../wiring'; import { ExtensionBoundary } from '../components'; +/** @public */ export const PageBlueprint = createExtensionBlueprint({ kind: 'page', attachTo: { id: 'app/routes', input: 'routes' }, @@ -37,7 +38,7 @@ export const PageBlueprint = createExtensionBlueprint({ loader, routeRef, }: { - defaultPath?: string; + defaultPath: string; loader: () => Promise; routeRef?: RouteRef; }, @@ -47,7 +48,7 @@ export const PageBlueprint = createExtensionBlueprint({ loader().then(element => ({ default: () => element })), ); - yield coreExtensionData.routePath(config.path ?? defaultPath!); + yield coreExtensionData.routePath(config.path ?? defaultPath); yield coreExtensionData.reactElement( diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx similarity index 98% rename from packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx index 018c3fc570..fe40aaa8d7 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx @@ -97,7 +97,7 @@ describe('RouterBlueprint', () => { }); it('should work with complex options and props', async () => { - const extension = RouterBlueprint.make({ + const extension = RouterBlueprint.makeWithOverrides({ namespace: 'test', name: 'test', config: { diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx similarity index 92% rename from packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx index e584a5654c..24ee805659 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx @@ -15,8 +15,9 @@ */ import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; -import { createRouterExtension } from './createRouterExtension'; +import { createRouterExtension } from '../extensions/createRouterExtension'; +/** @public */ export const RouterBlueprint = createExtensionBlueprint({ kind: 'app-router-component', attachTo: { id: 'app/root', input: 'router' }, diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx similarity index 93% rename from packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx index fc779f58ab..2727335d8a 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -15,10 +15,11 @@ */ import React, { ComponentType, lazy } from 'react'; import { createExtensionBlueprint } from '../wiring'; -import { createSignInPageExtension } from './createSignInPageExtension'; +import { createSignInPageExtension } from '../extensions/createSignInPageExtension'; import { SignInPageProps } from '@backstage/core-plugin-api'; import { ExtensionBoundary } from '../components'; +/** @public */ export const SignInPageBlueprint = createExtensionBlueprint({ kind: 'sign-in-page', attachTo: { id: 'app/root', input: 'signInPage' }, diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts similarity index 94% rename from packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts rename to packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts index 92acd8eeba..40323b6a31 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts @@ -27,7 +27,8 @@ describe('ThemeBlueprint', () => { } as AppTheme; it('should create an extension with sensible defaults', () => { - expect(ThemeBlueprint.make({ params: { theme } })).toMatchInlineSnapshot(` + expect(ThemeBlueprint.make({ name: 'light', params: { theme } })) + .toMatchInlineSnapshot(` { "$$type": "@backstage/ExtensionDefinition", "attachTo": { diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts similarity index 91% rename from packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index 500946713b..cd97e0437f 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -16,12 +16,12 @@ import { AppTheme } from '@backstage/core-plugin-api'; import { createExtensionBlueprint } from '../wiring'; -import { createThemeExtension } from './createThemeExtension'; +import { createThemeExtension } from '../extensions/createThemeExtension'; +/** @public */ export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', namespace: 'app', - name: ({ theme }) => theme.id, attachTo: { id: 'app', input: 'themes' }, output: [createThemeExtension.themeDataRef], dataRefs: { diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts similarity index 98% rename from packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts rename to packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts index e2a979c097..dfb918053a 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts @@ -56,7 +56,7 @@ describe('TranslationBlueprint', () => { "inputs": {}, "kind": "translation", "name": "blob", - "namespace": "translationRefId", + "namespace": undefined, "output": [ [Function], ], diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts similarity index 90% rename from packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts index 581f4d176a..c70ca56f6b 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -15,12 +15,12 @@ */ import { createExtensionBlueprint } from '../wiring'; -import { createTranslationExtension } from './createTranslationExtension'; +import { createTranslationExtension } from '../extensions/createTranslationExtension'; import { TranslationMessages, TranslationResource } from '../translation'; +/** @public */ export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', - namespace: ({ resource }) => resource.id, attachTo: { id: 'app', input: 'translations' }, output: [createTranslationExtension.translationDataRef], dataRefs: { diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts new file mode 100644 index 0000000000..85f7a99a2c --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -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. + */ + +export { ApiBlueprint } from './ApiBlueprint'; +export { AppRootElementBlueprint } from './AppRootElementBlueprint'; +export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +export { IconBundleBlueprint } from './IconBundleBlueprint'; +export { NavItemBlueprint } from './NavItemBlueprint'; +export { NavLogoBlueprint } from './NavLogoBlueprint'; +export { PageBlueprint } from './PageBlueprint'; +export { RouterBlueprint } from './RouterBlueprint'; +export { SignInPageBlueprint } from './SignInPageBlueprint'; +export { ThemeBlueprint } from './ThemeBlueprint'; +export { TranslationBlueprint } from './TranslationBlueprint'; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index 93346f1ec9..b1db2a0106 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -24,7 +24,10 @@ import { import { AnyExtensionInputMap } from '../wiring/createExtension'; import { Expand } from '../types'; -/** @public */ +/** + * @public + * @deprecated Use {@link ApiBlueprint} instead. + */ export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts index 8be82d5540..c8f7bdc366 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts @@ -30,6 +30,7 @@ import { * the app layout. This is useful for example for shared popups and similar. * * @public + * @deprecated Use {@link AppRootElementBlueprint} instead. */ export function createAppRootElementExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx index ce2eb89f25..4f70f29470 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * and similar. * * @public + * @deprecated Use {@link AppRootWrapperBlueprint} instead. */ export function createAppRootWrapperExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx index 2a2c47835a..d694440427 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -21,7 +21,9 @@ import { RouteRef } from '../routing'; /** * Helper for creating extensions for a nav item. + * * @public + * @deprecated Use {@link NavItemBlueprint} instead. */ export function createNavItemExtension(options: { namespace?: string; diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx index 1f2419bd02..02c32e7f34 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx @@ -18,7 +18,9 @@ import { createExtension, createExtensionDataRef } from '../wiring'; /** * Helper for creating extensions for a nav logos. + * * @public + * @deprecated Use {@link NavLogoBlueprint} instead. */ export function createNavLogoExtension(options: { name?: string; diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index cd434a3ffc..bef642e465 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -31,6 +31,7 @@ import { ExtensionDefinition } from '../wiring/createExtension'; * Helper for creating extensions for a routable React page component. * * @public + * @deprecated Use {@link PageBlueprint} instead. */ export function createPageExtension< TConfig extends { path: string }, diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx index 12f946e8a7..943711f5e0 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * MemoryRouter in tests, or to add additional props to a BrowserRouter. * * @public + * @deprecated Use {@link RouterBlueprint} instead. */ export function createRouterExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx index 312d30c78c..3f41202f11 100644 --- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx @@ -30,6 +30,7 @@ import { SignInPageProps } from '@backstage/core-plugin-api'; /** * * @public + * @deprecated Use {@link SignInPageBlueprint} instead. */ export function createSignInPageExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts index 1740f05a5a..e523fd9c05 100644 --- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -17,7 +17,10 @@ import { createExtension, createExtensionDataRef } from '../wiring'; import { AppTheme } from '@backstage/core-plugin-api'; -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export function createThemeExtension(theme: AppTheme) { return createExtension({ kind: 'theme', @@ -31,7 +34,10 @@ export function createThemeExtension(theme: AppTheme) { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export namespace createThemeExtension { export const themeDataRef = createExtensionDataRef().with({ id: 'core.theme.theme', diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts index 9445e32e24..c4d2f6037d 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts @@ -17,7 +17,10 @@ import { TranslationMessages, TranslationResource } from '../translation'; import { createExtension, createExtensionDataRef } from '../wiring'; -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; @@ -34,7 +37,10 @@ export function createTranslationExtension(options: { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export namespace createTranslationExtension { export const translationDataRef = createExtensionDataRef< TranslationResource | TranslationMessages diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 267201db81..562cb728cb 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -25,4 +25,3 @@ export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; export { createComponentExtension } from './createComponentExtension'; export { createTranslationExtension } from './createTranslationExtension'; -export { IconBundleBlueprint } from './IconBundleBlueprint'; diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 263380b1f8..957ca50bba 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -22,6 +22,7 @@ export * from './analytics'; export * from './apis'; +export * from './blueprints'; export * from './components'; export * from './extensions'; export * from './icons'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 924eba2cba..b9d1475074 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -127,7 +127,7 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', factory(origFactory) { return origFactory({ @@ -185,11 +185,8 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', - params: { - text: 'Hello, world!', - }, config: { schema: { something: z => z.string(), @@ -236,7 +233,7 @@ describe('createExtensionBlueprint', () => { }, }); - TestExtensionBlueprint.make({ + TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', params: { text: 'Hello, world!', @@ -266,11 +263,8 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', - params: { - text: 'Hello, world!', - }, config: { schema: { something: z => z.string(), @@ -341,65 +335,6 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); - it('should allow providing callback for properties to set with params', () => { - type TestParams = { test: string }; - - const Blueprint = createExtensionBlueprint({ - kind: 'test-extension', - attachTo: { id: 'test', input: 'default' }, - name: (params: TestParams) => `${params.test}-name`, - output: [coreExtensionData.reactElement], - namespace: (props: TestParams) => props.test, - config: { - schema: (props: TestParams) => ({ - test: z => z.string().default(props.test), - }), - }, - factory(params: TestParams) { - return [coreExtensionData.reactElement(
    {params.test}
    )]; - }, - }); - - expect(Blueprint.make({ params: { test: 'hello' } })) - .toMatchInlineSnapshot(` - { - "$$type": "@backstage/ExtensionDefinition", - "attachTo": { - "id": "test", - "input": "default", - }, - "configSchema": { - "parse": [Function], - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "test": { - "default": "hello", - "type": "string", - }, - }, - "type": "object", - }, - }, - "disabled": false, - "factory": [Function], - "inputs": {}, - "kind": "test-extension", - "name": "hello-name", - "namespace": "hello", - "output": [ - [Function], - ], - "override": [Function], - "toString": [Function], - "version": "v2", - } - `); - - expect(true).toBe(true); - }); - it('should allow merging of inputs', () => { const blueprint = createExtensionBlueprint({ kind: 'test-extension', @@ -418,7 +353,7 @@ describe('createExtensionBlueprint', () => { }, }); - blueprint.make({ + blueprint.makeWithOverrides({ inputs: { test2: createExtensionInput([coreExtensionData.reactElement], { singleton: true, @@ -453,7 +388,7 @@ describe('createExtensionBlueprint', () => { }, }); - blueprint.make({ + blueprint.makeWithOverrides({ inputs: { // @ts-expect-error test: createExtensionInput([]), // Overrides are not allowed @@ -480,7 +415,7 @@ describe('createExtensionBlueprint', () => { }); const ext = toInternalExtensionDefinition( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef2], factory(origFactory) { const parent = origFactory({}); @@ -509,7 +444,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], *factory(origFactory) { yield* origFactory({}); @@ -521,7 +456,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], factory(origFactory) { return [...origFactory({}), testDataRef2('bar')]; @@ -544,9 +479,9 @@ describe('createExtensionBlueprint', () => { }, }); - // @ts-expect-error - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef2.optional()], + // @ts-expect-error *factory() { yield testDataRef1('foo'); yield testDataRef2('bar'); @@ -555,7 +490,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef2.optional()], *factory() { yield testDataRef2('bar'); @@ -578,9 +513,9 @@ describe('createExtensionBlueprint', () => { }, }); - // @ts-expect-error - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], + // @ts-expect-error *factory(origFactory) { yield* origFactory({}); }, @@ -588,7 +523,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], *factory(origFactory) { yield* origFactory({}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index f749f7d412..656e681a2b 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -52,14 +52,14 @@ export type CreateExtensionBlueprintOptions< TDataRefs extends { [name in string]: AnyExtensionDataRef }, > = { kind: TKind; - namespace?: TNamespace | ((params: TParams) => TNamespace); + namespace?: TNamespace; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; - name?: TName | ((params: TParams) => TName); + name?: TName; config?: { - schema: TConfigSchema | ((params: TParams) => TConfigSchema); + schema: TConfigSchema; }; factory( params: TParams, @@ -96,13 +96,32 @@ export interface ExtensionBlueprint< > { dataRefs: TDataRefs; + make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + /** * Creates a new extension from the blueprint. * * You must either pass `params` directly, or define a `factory` that can * optionally call the original factory with the same params. */ - make< + makeWithOverrides< TNewNamespace extends string | undefined, TNewName extends string | undefined, TExtensionConfigSchema extends { @@ -116,52 +135,45 @@ export interface ExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - >( - args: { - namespace?: TNewNamespace; - name?: TNewName; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TExtraInputs & { - [KName in keyof TInputs]?: `Error: Input '${KName & - string}' is already defined in parent definition`; + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; }; - output?: Array; - config?: { - schema: TExtensionConfigSchema & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; }; - }; - } & ( - | ({ - factory( - originalFactory: ( - params: TParams, - context?: { - config?: TConfig; - inputs?: Expand>; - }, - ) => ExtensionDataContainer, - context: { - node: AppNode; - config: TConfig & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - inputs: Expand>; - }, - ): Iterable; - } & VerifyExtensionFactoryOutput< - AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - UFactoryOutput - >) - | { - params: TParams; - } - ), - ): ExtensionDefinition< + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -243,7 +255,7 @@ class ExtensionBlueprintImpl< dataRefs: TDataRefs; - public make< + public makeWithOverrides< TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -264,11 +276,10 @@ class ExtensionBlueprintImpl< disabled?: boolean; inputs?: TExtraInputs; output?: Array; - params?: TParams; config?: { schema: TExtensionConfigSchema; }; - factory?( + factory( originalFactory: ( params: TParams, context?: { @@ -312,77 +323,82 @@ class ExtensionBlueprintImpl< > > > { - const optionsSchema = // can remove this args.params check with the split apart of .make - typeof this.options.config?.schema === 'function' && args.params - ? this.options.config?.schema(args.params) - : this.options.config?.schema; - const schema = { - ...optionsSchema, + ...this.options.config?.schema, ...args.config?.schema, } as TConfigSchema & TExtensionConfigSchema; - const namespace = - typeof this.options.namespace === 'function' && args.params - ? this.options.namespace(args.params) - : this.options.namespace; - - const name = - typeof this.options.name === 'function' && args.params - ? this.options.name(args.params) - : this.options.name; - return createExtension({ kind: this.options.kind, - namespace: args.namespace ?? namespace, - name: args.name ?? name, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, inputs: { ...args.inputs, ...this.options.inputs }, output: args.output ?? this.options.output, config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { - if (args.factory) { - return args.factory( - ( - innerParams: TParams, - innerContext?: { - config?: { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }; - inputs?: Expand>; - }, - ): ExtensionDataContainer => { - return createDataContainer( - this.options.factory(innerParams, { - node, - config: innerContext?.config ?? config, - inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden - }), - ); + return args.factory( + ( + innerParams: TParams, + innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; }, - { - node, - config, - inputs, - }, - ); - } else if (args.params) { - return this.options.factory(args.params, { + ): ExtensionDataContainer => { + return createDataContainer( + this.options.factory(innerParams, { + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }), + ); + }, + { node, config, - // TODO: Figure out types once legacy data map input type is gone - inputs: inputs as unknown as Expand< - ResolvedExtensionInputs - >, - }); - } - throw new Error('Either params or factory must be provided'); + inputs, + }, + ); }, } as CreateExtensionOptions); } + + public make< + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + > + > { + return createExtension({ + kind: this.options.kind, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, + attachTo: args.attachTo ?? this.options.attachTo, + disabled: args.disabled ?? this.options.disabled, + inputs: this.options.inputs, + output: this.options.output, + config: this.options.config, + factory: ctx => this.options.factory(args.params, ctx), + } as CreateExtensionOptions); + } } /** diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index ce7da3701d..4bfc8392cb 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -19,7 +19,6 @@ import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { - ExtensionDataValue, AppNode, AppTree, Extension, @@ -169,9 +168,7 @@ export class ExtensionTester { : [...internal.output, coreExtensionData.routePath], factory: params => { const parentOutput = Array.from( - internal.factory(params as any) as Iterable< - ExtensionDataValue - >, + internal.factory(params as any), ).filter(val => val.id !== coreExtensionData.routePath.id); return [...parentOutput, coreExtensionData.routePath('/')]; From 264e10f9cd1ae5cf0666f5e5f207d17e91a5b476 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 15:43:33 +0200 Subject: [PATCH 201/204] chore: added changeset Signed-off-by: blam --- .changeset/tiny-dodos-prove-2.md | 5 +++++ .changeset/tiny-dodos-prove.md | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/tiny-dodos-prove-2.md create mode 100644 .changeset/tiny-dodos-prove.md diff --git a/.changeset/tiny-dodos-prove-2.md b/.changeset/tiny-dodos-prove-2.md new file mode 100644 index 0000000000..91ac021c67 --- /dev/null +++ b/.changeset/tiny-dodos-prove-2.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Deprecate existing `ExtensionCreators` in favour of their new Blueprint counterparts. diff --git a/.changeset/tiny-dodos-prove.md b/.changeset/tiny-dodos-prove.md new file mode 100644 index 0000000000..6392e5050e --- /dev/null +++ b/.changeset/tiny-dodos-prove.md @@ -0,0 +1,9 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Refactor `.make` method on Blueprints into two different methods, `.make` and `.makeWithOverrides`. + +When using `createExtensionBlueprint` you can define parameters for the factory function, if you wish to take advantage of these parameters you should use `.make` when creating an extension instance of a Blueprint. If you wish to override more things other than the standard `attachTo`, `name`, `namespace` then you should use `.makeWithOverrides` instead. + +`.make` is reserved for simple creation of extension instances from Blueprints using higher level parameters, whereas `.makeWithOverrides` is lower level and you have more control over the final extension. From 0e615076857067573e03b8d13ea0e250083ae54e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 15:45:57 +0200 Subject: [PATCH 202/204] chore: updating api-reports and documentation for exported blueprints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 18 +++++++++--------- .../src/blueprints/ApiBlueprint.ts | 7 ++++++- .../src/blueprints/AppRootElementBlueprint.ts | 7 ++++++- .../src/blueprints/AppRootWrapperBlueprint.tsx | 8 +++++++- .../src/blueprints/NavItemBlueprint.ts | 6 +++++- .../src/blueprints/NavLogoBlueprint.ts | 6 +++++- .../src/blueprints/PageBlueprint.tsx | 6 +++++- .../src/blueprints/SignInPageBlueprint.tsx | 6 +++++- .../src/blueprints/ThemeBlueprint.ts | 6 +++++- .../src/blueprints/TranslationBlueprint.ts | 6 +++++- 10 files changed, 58 insertions(+), 18 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 2d3f1d3278..f39add924a 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -189,7 +189,7 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef; }; -// @public (undocumented) +// @public export const ApiBlueprint: ExtensionBlueprint< 'api', undefined, @@ -261,7 +261,7 @@ export interface AppNodeSpec { readonly source?: BackstagePlugin; } -// @public (undocumented) +// @public export const AppRootElementBlueprint: ExtensionBlueprint< 'app-root-element', undefined, @@ -276,7 +276,7 @@ export const AppRootElementBlueprint: ExtensionBlueprint< never >; -// @public (undocumented) +// @public export const AppRootWrapperBlueprint: ExtensionBlueprint< 'app-root-wrapper', undefined, @@ -1696,7 +1696,7 @@ export interface LegacyExtensionInput< export { microsoftAuthApiRef }; -// @public (undocumented) +// @public export const NavItemBlueprint: ExtensionBlueprint< 'nav-item', undefined, @@ -1731,7 +1731,7 @@ export const NavItemBlueprint: ExtensionBlueprint< } >; -// @public (undocumented) +// @public export const NavLogoBlueprint: ExtensionBlueprint< 'nav-logo', undefined, @@ -1781,7 +1781,7 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; -// @public (undocumented) +// @public export const PageBlueprint: ExtensionBlueprint< 'page', undefined, @@ -1938,7 +1938,7 @@ export { SessionApi }; export { SessionState }; -// @public (undocumented) +// @public export const SignInPageBlueprint: ExtensionBlueprint< 'sign-in-page', undefined, @@ -1981,7 +1981,7 @@ export interface SubRouteRef< readonly T: TParams; } -// @public (undocumented) +// @public export const ThemeBlueprint: ExtensionBlueprint< 'theme', 'app', @@ -1998,7 +1998,7 @@ export const ThemeBlueprint: ExtensionBlueprint< } >; -// @public (undocumented) +// @public export const TranslationBlueprint: ExtensionBlueprint< 'translation', undefined, diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts index da12faef1a..b5ba9e05f6 100644 --- a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts @@ -13,11 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createExtensionBlueprint } from '../wiring'; import { createApiExtension } from '../extensions/createApiExtension'; import { AnyApiFactory } from '@backstage/core-plugin-api'; -/** @public */ +/** + * Creates utility API extensions. + * + * @public + */ export const ApiBlueprint = createExtensionBlueprint({ kind: 'api', attachTo: { id: 'app', input: 'apis' }, diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts index e44942979d..bc6304923a 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts @@ -15,7 +15,12 @@ */ import { coreExtensionData, createExtensionBlueprint } from '../wiring'; -/** @public */ +/** + * Creates extensions that render a React element at the app root, outside of + * the app layout. This is useful for example for shared popups and similar. + * + * @public + */ export const AppRootElementBlueprint = createExtensionBlueprint({ kind: 'app-root-element', attachTo: { id: 'app/root', input: 'elements' }, diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx index db63550880..c54686f54f 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -19,7 +19,13 @@ import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; import { createAppRootWrapperExtension } from '../extensions/createAppRootWrapperExtension'; -/** @public */ +/** + * Creates a extensions that render a React wrapper at the app root, enclosing + * the app layout. This is useful for example for adding global React contexts + * and similar. + * + * @public + */ export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', attachTo: { id: 'app/root', input: 'wrappers' }, diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts index 5391d0dec2..20f57421c7 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -19,7 +19,11 @@ import { RouteRef } from '../routing'; import { createExtensionBlueprint } from '../wiring'; import { createNavItemExtension } from '../extensions/createNavItemExtension'; -/** @public */ +/** + * Creates extensions that make up the items of the nav bar. + * + * @public + */ export const NavItemBlueprint = createExtensionBlueprint({ kind: 'nav-item', attachTo: { id: 'app/nav', input: 'items' }, diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts index f31b3cec9b..8066646ca3 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts @@ -17,7 +17,11 @@ import { createExtensionBlueprint } from '../wiring'; import { createNavLogoExtension } from '../extensions/createNavLogoExtension'; -/** @public */ +/** + * Creates an extension that replaces the logo in the nav bar with your own. + * + * @public + */ export const NavLogoBlueprint = createExtensionBlueprint({ kind: 'nav-logo', attachTo: { id: 'app/nav', input: 'logos' }, diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index bfcf4a7934..b224ea491e 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -18,7 +18,11 @@ import { RouteRef } from '../routing'; import { coreExtensionData, createExtensionBlueprint } from '../wiring'; import { ExtensionBoundary } from '../components'; -/** @public */ +/** + * Createx extensions that are routable React page components. + * + * @public + */ export const PageBlueprint = createExtensionBlueprint({ kind: 'page', attachTo: { id: 'app/routes', input: 'routes' }, diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx index 2727335d8a..f87ae91452 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -19,7 +19,11 @@ import { createSignInPageExtension } from '../extensions/createSignInPageExtensi import { SignInPageProps } from '@backstage/core-plugin-api'; import { ExtensionBoundary } from '../components'; -/** @public */ +/** + * Creates an extension that replaces the sign in page. + * + * @public + */ export const SignInPageBlueprint = createExtensionBlueprint({ kind: 'sign-in-page', attachTo: { id: 'app/root', input: 'signInPage' }, diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index cd97e0437f..686c498c06 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -18,7 +18,11 @@ import { AppTheme } from '@backstage/core-plugin-api'; import { createExtensionBlueprint } from '../wiring'; import { createThemeExtension } from '../extensions/createThemeExtension'; -/** @public */ +/** + * Creates an extension that adds/replaces an app theme. + * + * @public + */ export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', namespace: 'app', diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts index c70ca56f6b..bdb36e2191 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -18,7 +18,11 @@ import { createExtensionBlueprint } from '../wiring'; import { createTranslationExtension } from '../extensions/createTranslationExtension'; import { TranslationMessages, TranslationResource } from '../translation'; -/** @public */ +/** + * Creates an extension that adds translations to your app. + * + * @public + */ export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', attachTo: { id: 'app', input: 'translations' }, From 6d898d847533eb8eb5c065f20f409d8c501e386d Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 9 Aug 2024 12:07:10 -0600 Subject: [PATCH 203/204] Switch process polyfill to require.resolve Signed-off-by: Tim Hansen --- .changeset/funny-bears-sort.md | 5 +++++ packages/cli/src/lib/bundler/config.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/funny-bears-sort.md diff --git a/.changeset/funny-bears-sort.md b/.changeset/funny-bears-sort.md new file mode 100644 index 0000000000..a3dad0f489 --- /dev/null +++ b/.changeset/funny-bears-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Switched the `process` polyfill to use `require.resolve` for greater compatability. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 42cdae24e3..da7b378fbd 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -180,7 +180,7 @@ export async function createConfig( // to remove this eventually! plugins.push( new ProvidePlugin({ - process: 'process/browser', + process: require.resolve('process/browser'), Buffer: ['buffer', 'Buffer'], }), ); From 27794d1089ba154b3bd7625ca55429d75d20a58b Mon Sep 17 00:00:00 2001 From: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:33:47 -0400 Subject: [PATCH 204/204] Add ability to override theme options for the TechDocsReaderPage. (#25956) * Add ability to override theme options for the TechDocsReaderPage. --------- Signed-off-by: Sydney Achinger --- .changeset/healthy-fireants-jump.md | 5 ++ plugins/techdocs/api-report.md | 2 + .../TechDocsReaderPage.test.tsx | 26 ++++++++ .../TechDocsReaderPage/TechDocsReaderPage.tsx | 59 +++++++++++-------- 4 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 .changeset/healthy-fireants-jump.md diff --git a/.changeset/healthy-fireants-jump.md b/.changeset/healthy-fireants-jump.md new file mode 100644 index 0000000000..7a11015b80 --- /dev/null +++ b/.changeset/healthy-fireants-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Allow for more granular control of TechDocsReaderPage styling. Theme overrides can now be provided to TechDocs without affecting the theme in other areas of Backstage. diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 90834c6fcb..781d8db422 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -30,6 +30,7 @@ import { TechDocsApi as TechDocsApi_2 } from '@backstage/plugin-techdocs-react'; import { TechDocsEntityMetadata as TechDocsEntityMetadata_2 } from '@backstage/plugin-techdocs-react'; import { TechDocsMetadata as TechDocsMetadata_2 } from '@backstage/plugin-techdocs-react'; import { TechDocsStorageApi as TechDocsStorageApi_2 } from '@backstage/plugin-techdocs-react'; +import { ThemeOptions } from '@material-ui/core/styles'; import { ToolbarProps } from '@material-ui/core/Toolbar'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; @@ -389,6 +390,7 @@ export type TechDocsReaderPageHeaderProps = PropsWithChildren<{ export type TechDocsReaderPageProps = { entityRef?: CompoundEntityRef; children?: TechDocsReaderPageRenderFunction | ReactNode; + overrideThemeOptions?: Partial; }; // @public diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index ed8b05c661..5e8cd7af27 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -252,4 +252,30 @@ describe('', () => { expect(rendered.getByText('the page')).toBeInTheDocument(); }); + + it('should apply overrideThemeOptions', async () => { + const overrideThemeOptions = { + typography: { fontFamily: 'Comic Sans MS' }, + }; + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + const text = rendered.getAllByText(mockTechDocsMetadata.site_name)[0]; + + expect(text).toHaveStyle('fontFamily: Comic Sans MS'); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index b1b9ecb13b..0f48df7f39 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -37,6 +37,8 @@ import { } from '@backstage/core-plugin-api'; import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; +import { ThemeOptions } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider, useTheme } from '@material-ui/core/styles'; /* An explanation for the multiple ways of customizing the TechDocs reader page @@ -151,6 +153,7 @@ export const TechDocsReaderLayout = (props: TechDocsReaderLayoutProps) => { export type TechDocsReaderPageProps = { entityRef?: CompoundEntityRef; children?: TechDocsReaderPageRenderFunction | ReactNode; + overrideThemeOptions?: Partial; }; /** @@ -159,6 +162,12 @@ export type TechDocsReaderPageProps = { * @public */ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { + const currentTheme = useTheme(); + + const readerPageTheme = createTheme({ + ...currentTheme, + ...(props.overrideThemeOptions || {}), + }); const { kind, name, namespace } = useRouteRefParams(rootDocsRouteRef); const { children, entityRef = { kind, name, namespace } } = props; @@ -179,33 +188,37 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { // As explained above, "page" is configuration 4 and is 1 return ( - - - {(page as JSX.Element) || } - - + + + + {(page as JSX.Element) || } + + + ); } // As explained above, a render function is configuration 3 and React element is 2 return ( - - - {({ metadata, entityMetadata, onReady }) => ( -
    - - {children instanceof Function - ? children({ - entityRef, - techdocsMetadataValue: metadata.value, - entityMetadataValue: entityMetadata.value, - onReady, - }) - : children} - -
    - )} -
    -
    + + + + {({ metadata, entityMetadata, onReady }) => ( +
    + + {children instanceof Function + ? children({ + entityRef, + techdocsMetadataValue: metadata.value, + entityMetadataValue: entityMetadata.value, + onReady, + }) + : children} + +
    + )} +
    +
    +
    ); };