From 05a0506afaa3681a3626a1e3ac58c5a90d5c3d15 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 6 Nov 2023 15:21:43 +0100 Subject: [PATCH 01/13] frontend-plugin-api: move over apis Co-authored-by: Philipp Hugenroth Co-authored-by: Camila Belo Signed-off-by: Vincenzo Scamporlino --- .../frontend-app-api/src/wiring/createApp.tsx | 6 +- packages/frontend-plugin-api/package.json | 1 + .../src/definitions/AlertApi.ts | 56 +++ .../src/definitions/AppLanguageApi.ts | 36 ++ .../src/definitions/AppThemeApi.ts | 87 ++++ .../src/definitions/ConfigApi.ts | 34 ++ .../src/definitions/DiscoveryApi.ts | 55 +++ .../src/definitions/ErrorApi.ts | 91 ++++ .../src/definitions/FeatureFlagsApi.ts | 110 +++++ .../src/definitions/FetchApi.ts | 51 ++ .../src/definitions/IdentityApi.ts | 56 +++ .../src/definitions/OAuthRequestApi.ts | 131 +++++ .../src/definitions/StorageApi.ts | 110 +++++ .../src/definitions/alpha.ts | 17 + .../src/definitions/auth.ts | 452 ++++++++++++++++++ .../src/definitions/index.ts | 34 ++ .../frontend-plugin-api/src/icons/index.ts | 17 + .../frontend-plugin-api/src/icons/types.ts | 45 ++ packages/frontend-plugin-api/src/index.ts | 3 + .../src/system/ApiRef.test.ts | 50 ++ .../frontend-plugin-api/src/system/ApiRef.ts | 64 +++ .../frontend-plugin-api/src/system/index.ts | 19 + .../frontend-plugin-api/src/system/types.ts | 74 +++ 23 files changed, 1597 insertions(+), 2 deletions(-) create mode 100644 packages/frontend-plugin-api/src/definitions/AlertApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/AppLanguageApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/AppThemeApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/ConfigApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/DiscoveryApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/ErrorApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/FeatureFlagsApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/FetchApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/IdentityApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/OAuthRequestApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/StorageApi.ts create mode 100644 packages/frontend-plugin-api/src/definitions/alpha.ts create mode 100644 packages/frontend-plugin-api/src/definitions/auth.ts create mode 100644 packages/frontend-plugin-api/src/definitions/index.ts create mode 100644 packages/frontend-plugin-api/src/icons/index.ts create mode 100644 packages/frontend-plugin-api/src/icons/types.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiRef.test.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiRef.ts create mode 100644 packages/frontend-plugin-api/src/system/index.ts create mode 100644 packages/frontend-plugin-api/src/system/types.ts diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 8495f961db..a857eff27d 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -35,16 +35,18 @@ import { ApiHolder, AppComponents, AppContext, + attachComponentData, +} from '@backstage/core-plugin-api'; +import { appThemeApiRef, ConfigApi, configApiRef, IconComponent, BackstagePlugin as LegacyBackstagePlugin, featureFlagsApiRef, - attachComponentData, identityApiRef, AppTheme, -} from '@backstage/core-plugin-api'; +} from '@backstage/frontend-plugin-api'; import { getAvailableFeatures } from './discovery'; import { ApiFactoryRegistry, diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 2a1d741d3d..d7d19e3d74 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -39,6 +39,7 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "dependencies": { + "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/types": "workspace:^", diff --git a/packages/frontend-plugin-api/src/definitions/AlertApi.ts b/packages/frontend-plugin-api/src/definitions/AlertApi.ts new file mode 100644 index 0000000000..afdcb6a617 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/AlertApi.ts @@ -0,0 +1,56 @@ +/* + * 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 { createApiRef, ApiRef } from '../system'; +import { Observable } from '@backstage/types'; + +/** + * Message handled by the {@link AlertApi}. + * + * @public + */ +export type AlertMessage = { + message: string; + // Severity will default to success since that is what material ui defaults the value to. + severity?: 'success' | 'info' | 'warning' | 'error'; + display?: 'permanent' | 'transient'; +}; + +/** + * The alert API is used to report alerts to the app, and display them to the user. + * + * @public + */ +export type AlertApi = { + /** + * Post an alert for handling by the application. + */ + post(alert: AlertMessage): void; + + /** + * Observe alerts posted by other parts of the application. + */ + alert$(): Observable; +}; + +/** + * The {@link ApiRef} of {@link AlertApi}. + * + * @public + */ +export const alertApiRef: ApiRef = createApiRef({ + id: 'core.alert', +}); diff --git a/packages/frontend-plugin-api/src/definitions/AppLanguageApi.ts b/packages/frontend-plugin-api/src/definitions/AppLanguageApi.ts new file mode 100644 index 0000000000..1207e84755 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/AppLanguageApi.ts @@ -0,0 +1,36 @@ +/* + * 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 { ApiRef, createApiRef } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; + +/** @alpha */ +export type AppLanguageApi = { + getAvailableLanguages(): { languages: string[] }; + + setLanguage(language?: string): void; + + getLanguage(): { language: string }; + + language$(): Observable<{ language: string }>; +}; + +/** + * @alpha + */ +export const appLanguageApiRef: ApiRef = createApiRef({ + id: 'core.applanguage', +}); diff --git a/packages/frontend-plugin-api/src/definitions/AppThemeApi.ts b/packages/frontend-plugin-api/src/definitions/AppThemeApi.ts new file mode 100644 index 0000000000..e771fad597 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/AppThemeApi.ts @@ -0,0 +1,87 @@ +/* + * 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 { ReactNode } from 'react'; +import { ApiRef, createApiRef } from '../system'; +import { Observable } from '@backstage/types'; + +/** + * Describes a theme provided by the app. + * + * @public + */ +export type AppTheme = { + /** + * ID used to remember theme selections. + */ + id: string; + + /** + * Title of the theme + */ + title: string; + + /** + * Theme variant + */ + variant: 'light' | 'dark'; + + /** + * An Icon for the theme mode setting. + */ + icon?: React.ReactElement; + + Provider(props: { children: ReactNode }): JSX.Element | null; +}; + +/** + * The AppThemeApi gives access to the current app theme, and allows switching + * to other options that have been registered as a part of the App. + * + * @public + */ +export type AppThemeApi = { + /** + * Get a list of available themes. + */ + getInstalledThemes(): AppTheme[]; + + /** + * Observe the currently selected theme. A value of undefined means no specific theme has been selected. + */ + activeThemeId$(): Observable; + + /** + * Get the current theme ID. Returns undefined if no specific theme is selected. + */ + getActiveThemeId(): string | undefined; + + /** + * Set a specific theme to use in the app, overriding the default theme selection. + * + * Clear the selection by passing in undefined. + */ + setActiveThemeId(themeId?: string): void; +}; + +/** + * The {@link ApiRef} of {@link AppThemeApi}. + * + * @public + */ +export const appThemeApiRef: ApiRef = createApiRef({ + id: 'core.apptheme', +}); diff --git a/packages/frontend-plugin-api/src/definitions/ConfigApi.ts b/packages/frontend-plugin-api/src/definitions/ConfigApi.ts new file mode 100644 index 0000000000..d3bada9e46 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/ConfigApi.ts @@ -0,0 +1,34 @@ +/* + * 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 { ApiRef, createApiRef } from '../system'; +import { Config } from '@backstage/config'; + +/** + * The Config API is used to provide a mechanism to access the + * runtime configuration of the system. + * + * @public + */ +export type ConfigApi = Config; + +/** + * The {@link ApiRef} of {@link ConfigApi}. + * + * @public + */ +export const configApiRef: ApiRef = createApiRef({ + id: 'core.config', +}); diff --git a/packages/frontend-plugin-api/src/definitions/DiscoveryApi.ts b/packages/frontend-plugin-api/src/definitions/DiscoveryApi.ts new file mode 100644 index 0000000000..d23fe3db6c --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/DiscoveryApi.ts @@ -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 { ApiRef, createApiRef } from '../system'; + +/** + * The discovery API is used to provide a mechanism for plugins to + * discover the endpoint to use to talk to their backend counterpart. + * + * @remarks + * + * The purpose of the discovery API is to allow for many different deployment + * setups and routing methods through a central configuration, instead + * of letting each individual plugin manage that configuration. + * + * Implementations of the discovery API can be a simple as a URL pattern + * using the pluginId, but could also have overrides for individual plugins, + * or query a separate discovery service. + * + * @public + */ +export type DiscoveryApi = { + /** + * Returns the HTTP base backend URL for a given plugin, without a trailing slash. + * + * This method must always be called just before making a request, as opposed to + * fetching the URL when constructing an API client. That is to ensure that more + * flexible routing patterns can be supported. + * + * For example, asking for the URL for `auth` may return something + * like `https://backstage.example.com/api/auth` + */ + getBaseUrl(pluginId: string): Promise; +}; + +/** + * The {@link ApiRef} of {@link DiscoveryApi}. + * + * @public + */ +export const discoveryApiRef: ApiRef = createApiRef({ + id: 'core.discovery', +}); diff --git a/packages/frontend-plugin-api/src/definitions/ErrorApi.ts b/packages/frontend-plugin-api/src/definitions/ErrorApi.ts new file mode 100644 index 0000000000..9c73d94cac --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/ErrorApi.ts @@ -0,0 +1,91 @@ +/* + * 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 { ApiRef, createApiRef } from '../system'; +import { Observable } from '@backstage/types'; + +/** + * Mirrors the JavaScript Error class, for the purpose of + * providing documentation and optional fields. + * + * @public + */ +export type ErrorApiError = { + name: string; + message: string; + stack?: string; +}; + +/** + * Provides additional information about an error that was posted to the application. + * + * @public + */ +export type ErrorApiErrorContext = { + /** + * If set to true, this error should not be displayed to the user. + * + * Hidden errors are typically not displayed in the UI, but the ErrorApi + * implementation may still report them to error tracking services + * or other utilities that care about all errors. + * + * @defaultValue false + */ + hidden?: boolean; +}; + +/** + * The error API is used to report errors to the app, and display them to the user. + * + * @remarks + * + * Plugins can use this API as a method of displaying errors to the user, but also + * to report errors for collection by error reporting services. + * + * If an error can be displayed inline, e.g. as feedback in a form, that should be + * preferred over relying on this API to display the error. The main use of this API + * for displaying errors should be for asynchronous errors, such as a failing background process. + * + * Even if an error is displayed inline, it should still be reported through this API + * if it would be useful to collect or log it for debugging purposes, but with + * the hidden flag set. For example, an error arising from form field validation + * should probably not be reported, while a failed REST call would be useful to report. + * + * @public + */ +export type ErrorApi = { + /** + * Post an error for handling by the application. + */ + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; + + /** + * Observe errors posted by other parts of the application. + */ + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>; +}; + +/** + * The {@link ApiRef} of {@link ErrorApi}. + * + * @public + */ +export const errorApiRef: ApiRef = createApiRef({ + id: 'core.error', +}); diff --git a/packages/frontend-plugin-api/src/definitions/FeatureFlagsApi.ts b/packages/frontend-plugin-api/src/definitions/FeatureFlagsApi.ts new file mode 100644 index 0000000000..66260afa32 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/FeatureFlagsApi.ts @@ -0,0 +1,110 @@ +/* + * 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 { ApiRef, createApiRef } from '../system'; + +/** + * Feature flag descriptor. + * + * @public + */ +export type FeatureFlag = { + name: string; + pluginId: string; + description?: string; +}; + +/** + * Enum representing the state of a feature flag (inactive/active). + * + * @public + */ +export enum FeatureFlagState { + /** + * Feature flag inactive (disabled). + */ + None = 0, + /** + * Feature flag active (enabled). + */ + Active = 1, +} + +/** + * Options to use when saving feature flags. + * + * @public + */ +export type FeatureFlagsSaveOptions = { + /** + * The new feature flag states to save. + */ + states: Record; + + /** + * Whether the saves states should be merged into the existing ones, or replace them. + * + * Defaults to false. + */ + merge?: boolean; +}; + +/** + * The feature flags API is used to toggle functionality to users across plugins and Backstage. + * + * @remarks + * + * Plugins can use this API to register feature flags that they have available + * for users to enable/disable, and this API will centralize the current user's + * state of which feature flags they would like to enable. + * + * This is ideal for Backstage plugins, as well as your own App, to trial incomplete + * or unstable upcoming features. Although there will be a common interface for users + * to enable and disable feature flags, this API acts as another way to enable/disable. + * + * @public + */ +export interface FeatureFlagsApi { + /** + * Registers a new feature flag. Once a feature flag has been registered it + * can be toggled by users, and read back to enable or disable features. + */ + registerFlag(flag: FeatureFlag): void; + + /** + * Get a list of all registered flags. + */ + getRegisteredFlags(): FeatureFlag[]; + + /** + * Whether the feature flag with the given name is currently activated for the user. + */ + isActive(name: string): boolean; + + /** + * Save the user's choice of feature flag states. + */ + save(options: FeatureFlagsSaveOptions): void; +} + +/** + * The {@link ApiRef} of {@link FeatureFlagsApi}. + * + * @public + */ +export const featureFlagsApiRef: ApiRef = createApiRef({ + id: 'core.featureflags', +}); diff --git a/packages/frontend-plugin-api/src/definitions/FetchApi.ts b/packages/frontend-plugin-api/src/definitions/FetchApi.ts new file mode 100644 index 0000000000..aba0e53bb7 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/FetchApi.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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 { ApiRef, createApiRef } from '../system'; + +/** + * A wrapper for the fetch API, that has additional behaviors such as the + * ability to automatically inject auth information where necessary. + * + * @public + */ +export type FetchApi = { + /** + * The `fetch` implementation. + */ + fetch: typeof fetch; +}; + +/** + * The {@link ApiRef} of {@link FetchApi}. + * + * @remarks + * + * This is a wrapper for the fetch API, that has additional behaviors such as + * the ability to automatically inject auth information where necessary. + * + * Note that the default behavior of this API (unless overridden by your org), + * is to require that the user is already signed in so that it has auth + * information to inject. Therefore, using the default implementation of this + * utility API e.g. on the `SignInPage` or similar, would cause issues. In + * special circumstances like those, you can use the regular system `fetch` + * instead. + * + * @public + */ +export const fetchApiRef: ApiRef = createApiRef({ + id: 'core.fetch', +}); diff --git a/packages/frontend-plugin-api/src/definitions/IdentityApi.ts b/packages/frontend-plugin-api/src/definitions/IdentityApi.ts new file mode 100644 index 0000000000..1b127a1971 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/IdentityApi.ts @@ -0,0 +1,56 @@ +/* + * 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 { ApiRef, createApiRef } from '../system'; +import { BackstageUserIdentity, ProfileInfo } from './auth'; + +/** + * The Identity API used to identify and get information about the signed in user. + * + * @public + */ +export type IdentityApi = { + /** + * The profile of the signed in user. + */ + getProfileInfo(): Promise; + + /** + * User identity information within Backstage. + */ + getBackstageIdentity(): Promise; + + /** + * Provides credentials in the form of a token which proves the identity of the signed in user. + * + * The token will be undefined if the signed in user does not have a verified + * identity, such as a demo user or mocked user for e2e tests. + */ + getCredentials(): Promise<{ token?: string }>; + + /** + * Sign out the current user + */ + signOut(): Promise; +}; + +/** + * The {@link ApiRef} of {@link IdentityApi}. + * + * @public + */ +export const identityApiRef: ApiRef = createApiRef({ + id: 'core.identity', +}); diff --git a/packages/frontend-plugin-api/src/definitions/OAuthRequestApi.ts b/packages/frontend-plugin-api/src/definitions/OAuthRequestApi.ts new file mode 100644 index 0000000000..75bf3a3864 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/OAuthRequestApi.ts @@ -0,0 +1,131 @@ +/* + * 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 { Observable } from '@backstage/types'; +import { ApiRef, createApiRef } from '../system'; +import { AuthProviderInfo } from './auth'; + +/** + * Describes how to handle auth requests. Both how to show them to the user, and what to do when + * the user accesses the auth request. + * + * @public + */ +export type OAuthRequesterOptions = { + /** + * Information about the auth provider, which will be forwarded to auth requests. + */ + provider: AuthProviderInfo; + + /** + * Implementation of the auth flow, which will be called synchronously when + * trigger() is called on an auth requests. + */ + onAuthRequest(scopes: Set): Promise; +}; + +/** + * Function used to trigger new auth requests for a set of scopes. + * + * @remarks + * + * The returned promise will resolve to the same value returned by the onAuthRequest in the + * {@link OAuthRequesterOptions}. Or rejected, if the request is rejected. + * + * This function can be called multiple times before the promise resolves. All calls + * will be merged into one request, and the scopes forwarded to the onAuthRequest will be the + * union of all requested scopes. + * + * @public + */ +export type OAuthRequester = ( + scopes: Set, +) => Promise; + +/** + * An pending auth request for a single auth provider. The request will remain in this pending + * state until either reject() or trigger() is called. + * + * @remarks + * + * Any new requests for the same provider are merged into the existing pending request, meaning + * there will only ever be a single pending request for a given provider. + * + * @public + */ +export type PendingOAuthRequest = { + /** + * Information about the auth provider, as given in the AuthRequesterOptions + */ + provider: AuthProviderInfo; + + /** + * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError". + */ + reject(): void; + + /** + * Trigger the auth request to continue the auth flow, by for example showing a popup. + * + * Synchronously calls onAuthRequest with all scope currently in the request. + */ + trigger(): Promise; +}; + +/** + * Provides helpers for implemented OAuth login flows within Backstage. + * + * @public + */ +export type OAuthRequestApi = { + /** + * A utility for showing login popups or similar things, and merging together multiple requests for + * different scopes into one request that includes all scopes. + * + * The passed in options provide information about the login provider, and how to handle auth requests. + * + * The returned AuthRequester function is used to request login with new scopes. These requests + * are merged together and forwarded to the auth handler, as soon as a consumer of auth requests + * triggers an auth flow. + * + * See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. + */ + createAuthRequester( + options: OAuthRequesterOptions, + ): OAuthRequester; + + /** + * Observers pending auth requests. The returned observable will emit all + * current active auth request, at most one for each created auth requester. + * + * Each request has its own info about the login provider, forwarded from the auth requester options. + * + * Depending on user interaction, the request should either be rejected, or used to trigger the auth handler. + * If the request is rejected, all pending AuthRequester calls will fail with a "RejectedError". + * If a auth is triggered, and the auth handler resolves successfully, then all currently pending + * AuthRequester calls will resolve to the value returned by the onAuthRequest call. + */ + authRequest$(): Observable; +}; + +/** + * The {@link ApiRef} of {@link OAuthRequestApi}. + * + * @public + */ +export const oauthRequestApiRef: ApiRef = createApiRef({ + id: 'core.oauthrequest', +}); diff --git a/packages/frontend-plugin-api/src/definitions/StorageApi.ts b/packages/frontend-plugin-api/src/definitions/StorageApi.ts new file mode 100644 index 0000000000..1506e5f143 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/StorageApi.ts @@ -0,0 +1,110 @@ +/* + * 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 { ApiRef, createApiRef } from '../system'; +import { JsonValue, Observable } from '@backstage/types'; + +/** + * A snapshot in time of the current known value of a storage key. + * + * @public + */ +export type StorageValueSnapshot = + | { + key: string; + presence: 'unknown' | 'absent'; + value?: undefined; + } + | { + key: string; + presence: 'present'; + value: TValue; + }; + +/** + * Provides a key-value persistence API. + * + * @public + */ +export interface StorageApi { + /** + * Create a bucket to store data in. + * + * @param name - Namespace for the storage to be stored under, + * will inherit previous namespaces too + */ + forBucket(name: string): StorageApi; + + /** + * Remove persistent data. + * + * @param key - Unique key associated with the data. + */ + remove(key: string): Promise; + + /** + * Save persistent data, and emit messages to anyone that is using + * {@link StorageApi.observe$} for this key. + * + * @param key - Unique key associated with the data. + * @param data - The data to be stored under the key. + */ + set(key: string, data: T): Promise; + + /** + * Observe the value over time for a particular key in the current bucket. + * + * @remarks + * + * The observable will only emit values when the value changes in the underlying + * storage, although multiple values with the same shape may be emitted in a row. + * + * If a {@link StorageApi.snapshot} of a key is retrieved and the presence is + * `'unknown'`, then you are guaranteed to receive a snapshot with a known + * presence, as long as you observe the key within the same tick. + * + * Since the emitted values are shared across all subscribers, it is important + * not to mutate the returned values. The values may be frozen as a precaution. + * + * @param key - Unique key associated with the data + */ + observe$( + key: string, + ): Observable>; + + /** + * Returns an immediate snapshot value for the given key, if possible. + * + * @remarks + * + * Combine with {@link StorageApi.observe$} to get notified of value changes. + * + * Note that this method is synchronous, and some underlying storages may be + * unable to retrieve a value using this method - the result may or may not + * consistently have a presence of 'unknown'. Use {@link StorageApi.observe$} + * to be sure to receive an actual value eventually. + */ + snapshot(key: string): StorageValueSnapshot; +} + +/** + * The {@link ApiRef} of {@link StorageApi}. + * + * @public + */ +export const storageApiRef: ApiRef = createApiRef({ + id: 'core.storage', +}); diff --git a/packages/frontend-plugin-api/src/definitions/alpha.ts b/packages/frontend-plugin-api/src/definitions/alpha.ts new file mode 100644 index 0000000000..a4ccde1170 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/alpha.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { appLanguageApiRef, type AppLanguageApi } from './AppLanguageApi'; diff --git a/packages/frontend-plugin-api/src/definitions/auth.ts b/packages/frontend-plugin-api/src/definitions/auth.ts new file mode 100644 index 0000000000..5ed873a566 --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/auth.ts @@ -0,0 +1,452 @@ +/* + * 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 { ApiRef, createApiRef } from '../system'; +import { IconComponent } from '../icons/types'; +import { Observable } from '@backstage/types'; + +/** + * This file contains declarations for common interfaces of auth-related APIs. + * The declarations should be used to signal which type of authentication and + * authorization methods each separate auth provider supports. + * + * For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, + * would be declared as follows: + * + * const googleAuthApiRef = createApiRef({ ... }) + */ + +/** + * Information about the auth provider. + * + * @remarks + * + * This information is used both to connect the correct auth provider in the backend, as + * well as displaying the provider to the user. + * + * @public + */ +export type AuthProviderInfo = { + /** + * The ID of the auth provider. This should match with ID of the provider in the `@backstage/auth-backend`. + */ + id: string; + + /** + * Title for the auth provider, for example "GitHub" + */ + title: string; + + /** + * Icon for the auth provider. + */ + icon: IconComponent; +}; + +/** + * An array of scopes, or a scope string formatted according to the + * auth provider, which is typically a space separated list. + * + * @remarks + * + * See the documentation for each auth provider for the list of scopes + * supported by each provider. + * + * @public + */ +export type OAuthScope = string | string[]; + +/** + * Configuration of an authentication request. + * + * @public + */ +export type AuthRequestOptions = { + /** + * If this is set to true, the user will not be prompted to log in, + * and an empty response will be returned if there is no existing session. + * + * This can be used to perform a check whether the user is logged in, or if you don't + * want to force a user to be logged in, but provide functionality if they already are. + * + * @defaultValue false + */ + optional?: boolean; + + /** + * If this is set to true, the request will bypass the regular oauth login modal + * and open the login popup directly. + * + * The method must be called synchronously from a user action for this to work in all browsers. + * + * @defaultValue false + */ + instantPopup?: boolean; +}; + +/** + * This API provides access to OAuth 2 credentials. It lets you request access tokens, + * which can be used to act on behalf of the user when talking to APIs. + * + * @public + */ +export type OAuthApi = { + /** + * Requests an OAuth 2 Access Token, optionally with a set of scopes. The access token allows + * you to make requests on behalf of the user, and the copes may grant you broader access, depending + * on the auth provider. + * + * Each auth provider has separate handling of scope, so you need to look at the documentation + * for each one to know what scope you need to request. + * + * This method is cheap and should be called each time an access token is used. Do not for example + * store the access token in React component state, as that could cause the token to expire. Instead + * fetch a new access token for each request. + * + * Be sure to include all required scopes when requesting an access token. When testing your implementation + * it is best to log out the Backstage session and then visit your plugin page directly, as + * you might already have some required scopes in your existing session. Not requesting the correct + * scopes can lead to 403 or other authorization errors, which can be tricky to debug. + * + * If the user has not yet granted access to the provider and the set of requested scopes, the user + * will be prompted to log in. The returned promise will not resolve until the user has + * successfully logged in. The returned promise can be rejected, but only if the user rejects the login request. + */ + getAccessToken( + scope?: OAuthScope, + options?: AuthRequestOptions, + ): Promise; +}; + +/** + * This API provides access to OpenID Connect credentials. It lets you request ID tokens, + * which can be passed to backend services to prove the user's identity. + * + * @public + */ +export type OpenIdConnectApi = { + /** + * Requests an OpenID Connect ID Token. + * + * This method is cheap and should be called each time an ID token is used. Do not for example + * store the id token in React component state, as that could cause the token to expire. Instead + * fetch a new id token for each request. + * + * If the user has not yet logged in to Google inside Backstage, the user will be prompted + * to log in. The returned promise will not resolve until the user has successfully logged in. + * The returned promise can be rejected, but only if the user rejects the login request. + */ + getIdToken(options?: AuthRequestOptions): Promise; +}; + +/** + * This API provides access to profile information of the user from an auth provider. + * + * @public + */ +export type ProfileInfoApi = { + /** + * Get profile information for the user as supplied by this auth provider. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. + */ + getProfile(options?: AuthRequestOptions): Promise; +}; + +/** + * This API provides access to the user's identity within Backstage. + * + * @remarks + * + * An auth provider that implements this interface can be used to sign-in to backstage. It is + * not intended to be used directly from a plugin, but instead serves as a connection between + * this authentication method and the app's {@link IdentityApi} + * + * @public + */ +export type BackstageIdentityApi = { + /** + * Get the user's identity within Backstage. This should normally not be called directly, + * use the {@link IdentityApi} instead. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. + */ + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; +}; + +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + +/** + * Token and Identity response, with the users claims in the Identity. + * + * @public + */ +export type BackstageIdentityResponse = { + /** + * The token used to authenticate the user within Backstage. + */ + token: string; + + /** + * The time at which the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresAt?: Date; + + /** + * Identity information derived from the token. + */ + identity: BackstageUserIdentity; +}; + +/** + * Profile information of the user. + * + * @public + */ +export type ProfileInfo = { + /** + * Email ID. + */ + email?: string; + + /** + * Display name that can be presented to the user. + */ + displayName?: string; + + /** + * URL to an avatar image of the user. + */ + picture?: string; +}; + +/** + * Session state values passed to subscribers of the SessionApi. + * + * @public + */ +export enum SessionState { + /** + * User signed in. + */ + SignedIn = 'SignedIn', + /** + * User not signed in. + */ + SignedOut = 'SignedOut', +} + +/** + * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. + * + * @public + */ +export type SessionApi = { + /** + * Sign in with a minimum set of permissions. + */ + signIn(): Promise; + + /** + * Sign out from the current session. This will reload the page. + */ + signOut(): Promise; + + /** + * Observe the current state of the auth session. Emits the current state on subscription. + */ + sessionState$(): Observable; +}; + +/** + * Provides authentication towards Google APIs and identities. + * + * @public + * @remarks + * + * See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes. + * + * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, + * email and expiration information. Do not rely on any other fields, as they might not be present. + */ +export const googleAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.google', +}); + +/** + * Provides authentication towards GitHub APIs. + * + * @public + * @remarks + * + * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} + * for a full list of supported scopes. + */ +export const githubAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.github', +}); + +/** + * Provides authentication towards Okta APIs. + * + * @public + * @remarks + * + * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} + * for a full list of supported scopes. + */ +export const oktaAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.okta', +}); + +/** + * Provides authentication towards GitLab APIs. + * + * @public + * @remarks + * + * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} + * for a full list of supported scopes. + */ +export const gitlabAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.gitlab', +}); + +/** + * Provides authentication towards Microsoft APIs and identities. + * + * @public + * @remarks + * + * For more info and a full list of supported scopes, see: + * - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent} + * - {@link https://docs.microsoft.com/en-us/graph/permissions-reference} + */ +export const microsoftAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.microsoft', +}); + +/** + * Provides authentication towards OneLogin APIs. + * + * @public + */ +export const oneloginAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.onelogin', +}); + +/** + * Provides authentication towards Bitbucket APIs. + * + * @public + * @remarks + * + * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} + * for a full list of supported scopes. + */ +export const bitbucketAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.bitbucket', +}); + +/** + * Provides authentication towards Bitbucket Server APIs. + * + * @public + * @remarks + * + * See {@link https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html#BitbucketOAuth2.0providerAPI-scopes} + * for a full list of supported scopes. + */ +export const bitbucketServerAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.bitbucket-server', +}); + +/** + * Provides authentication towards Atlassian APIs. + * + * @public + * @remarks + * + * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} + * for a full list of supported scopes. + */ +export const atlassianAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.atlassian', +}); diff --git a/packages/frontend-plugin-api/src/definitions/index.ts b/packages/frontend-plugin-api/src/definitions/index.ts new file mode 100644 index 0000000000..cc02442f3d --- /dev/null +++ b/packages/frontend-plugin-api/src/definitions/index.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +// This folder contains definitions for all core APIs. +// +// Plugins should rely on these APIs for functionality as much as possible. +// +// If you think some API definition is missing, please open an Issue or send a PR! + +export * from './auth'; + +export * from './AlertApi'; +export * from './AppThemeApi'; +export * from './ConfigApi'; +export * from './DiscoveryApi'; +export * from './ErrorApi'; +export * from './FeatureFlagsApi'; +export * from './FetchApi'; +export * from './IdentityApi'; +export * from './OAuthRequestApi'; +export * from './StorageApi'; diff --git a/packages/frontend-plugin-api/src/icons/index.ts b/packages/frontend-plugin-api/src/icons/index.ts new file mode 100644 index 0000000000..9c9e45e54c --- /dev/null +++ b/packages/frontend-plugin-api/src/icons/index.ts @@ -0,0 +1,17 @@ +/* + * 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 type { IconComponent } from './types'; diff --git a/packages/frontend-plugin-api/src/icons/types.ts b/packages/frontend-plugin-api/src/icons/types.ts new file mode 100644 index 0000000000..4d54629de2 --- /dev/null +++ b/packages/frontend-plugin-api/src/icons/types.ts @@ -0,0 +1,45 @@ +/* + * 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 { ComponentType } from 'react'; + +/** + * IconComponent is the common icon type used throughout Backstage when + * working with and rendering generic icons, including the app system icons. + * + * @remarks + * + * The type is based on SvgIcon from Material UI, but both do not what the plugin-api + * package to have a dependency on Material UI, nor do we want the props to be as broad + * as the SvgIconProps interface. + * + * If you have the need to forward additional props from SvgIconProps, you can + * open an issue or submit a PR to the main Backstage repo. When doing so please + * also describe your use-case and reasoning of the addition. + * + * @public + */ + +export type IconComponent = ComponentType< + /* Material UI v4 */ + | { + fontSize?: 'large' | 'small' | 'default' | 'inherit'; + } + /* Material UI v5: https://mui.com/material-ui/migration/v5-component-changes/#icon */ + | { + fontSize?: 'medium' | 'large' | 'small' | 'inherit'; + } +>; diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 498bc96a77..f3656a0dc3 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -22,7 +22,10 @@ export * from './apis'; export * from './components'; +export * from './definitions'; export * from './extensions'; +export * from './icons'; export * from './routing'; export * from './schema'; +export * from './system'; export * from './wiring'; diff --git a/packages/frontend-plugin-api/src/system/ApiRef.test.ts b/packages/frontend-plugin-api/src/system/ApiRef.test.ts new file mode 100644 index 0000000000..dab872236f --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiRef.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { createApiRef } from './ApiRef'; + +describe('ApiRef', () => { + it('should be created', () => { + const ref = createApiRef({ id: 'abc' }); + expect(ref.id).toBe('abc'); + expect(String(ref)).toBe('apiRef{abc}'); + expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); + }); + + it('should reject invalid ids', () => { + for (const id of ['a', 'abc', 'ab-c', 'a.b.c', 'a-b.c', 'abc.a-b-c.abc3']) { + expect(createApiRef({ id }).id).toBe(id); + } + + for (const id of [ + '123', + 'ab-3', + 'ab_c', + '.', + '2ac', + 'ab.3a', + '.abc', + 'abc.', + 'ab..s', + '', + '_', + ]) { + expect(() => createApiRef({ id }).id).toThrow( + `API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`, + ); + } + }); +}); diff --git a/packages/frontend-plugin-api/src/system/ApiRef.ts b/packages/frontend-plugin-api/src/system/ApiRef.ts new file mode 100644 index 0000000000..adedff9f73 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiRef.ts @@ -0,0 +1,64 @@ +/* + * 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 type { ApiRef } from './types'; + +/** + * API reference configuration - holds an ID of the referenced API. + * + * @public + */ +export type ApiRefConfig = { + id: string; +}; + +class ApiRefImpl implements ApiRef { + constructor(private readonly config: ApiRefConfig) { + const valid = config.id + .split('.') + .flatMap(part => part.split('-')) + .every(part => part.match(/^[a-z][a-z0-9]*$/)); + if (!valid) { + throw new Error( + `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`, + ); + } + } + + get id(): string { + return this.config.id; + } + + // Utility for getting type of an api, using `typeof apiRef.T` + get T(): T { + throw new Error(`tried to read ApiRef.T of ${this}`); + } + + toString() { + return `apiRef{${this.config.id}}`; + } +} + +/** + * Creates a reference to an API. + * + * @param config - The descriptor of the API to reference. + * @returns An API reference. + * @public + */ +export function createApiRef(config: ApiRefConfig): ApiRef { + return new ApiRefImpl(config); +} diff --git a/packages/frontend-plugin-api/src/system/index.ts b/packages/frontend-plugin-api/src/system/index.ts new file mode 100644 index 0000000000..6aad24daa9 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export { createApiRef } from './ApiRef'; +export type { ApiRefConfig } from './ApiRef'; +export * from './types'; diff --git a/packages/frontend-plugin-api/src/system/types.ts b/packages/frontend-plugin-api/src/system/types.ts new file mode 100644 index 0000000000..96614c320c --- /dev/null +++ b/packages/frontend-plugin-api/src/system/types.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ + +/** + * API reference. + * + * @public + */ +export type ApiRef = { + id: string; + T: T; +}; + +/** + * Catch-all {@link ApiRef} type. + * + * @public + */ +export type AnyApiRef = ApiRef; + +/** + * Wraps a type with API properties into a type holding their respective {@link ApiRef}s. + * + * @public + */ +export type TypesToApiRefs = { [key in keyof T]: ApiRef }; + +/** + * Provides lookup of APIs through their {@link ApiRef}s. + * + * @public + */ +export type ApiHolder = { + get(api: ApiRef): T | undefined; +}; + +/** + * Describes type returning API implementations. + * + * @public + */ +export type ApiFactory< + Api, + Impl extends Api, + Deps extends { [name in string]: unknown }, +> = { + api: ApiRef; + deps: TypesToApiRefs; + factory(deps: Deps): Impl; +}; + +/** + * Catch-all {@link ApiFactory} type. + * + * @public + */ +export type AnyApiFactory = ApiFactory< + unknown, + unknown, + { [key in string]: unknown } +>; From a70e6937076096d529b6a33c7b7da09f8f0e1ab9 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 6 Nov 2023 15:55:31 +0100 Subject: [PATCH 02/13] Copy over missing APIs Co-authored-by: Camila Belo Signed-off-by: Philipp Hugenroth --- .../frontend-app-api/src/wiring/createApp.tsx | 94 +++---- .../src/system/ApiAggregator.test.ts | 46 +++ .../src/system/ApiAggregator.ts | 39 +++ .../src/system/ApiFactoryRegistry.test.ts | 91 ++++++ .../src/system/ApiFactoryRegistry.ts | 95 +++++++ .../src/system/ApiProvider.test.tsx | 234 ++++++++++++++++ .../src/system/ApiProvider.tsx | 53 ++++ .../src/system/ApiRegistry.test.ts | 75 +++++ .../src/system/ApiRegistry.ts | 80 ++++++ .../src/system/ApiResolver.test.ts | 263 ++++++++++++++++++ .../src/system/ApiResolver.ts | 116 ++++++++ .../frontend-plugin-api/src/system/index.ts | 3 + .../frontend-plugin-api/src/system/types.ts | 9 + 13 files changed, 1150 insertions(+), 48 deletions(-) create mode 100644 packages/frontend-plugin-api/src/system/ApiAggregator.test.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiAggregator.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiFactoryRegistry.test.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiFactoryRegistry.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiProvider.test.tsx create mode 100644 packages/frontend-plugin-api/src/system/ApiProvider.tsx create mode 100644 packages/frontend-plugin-api/src/system/ApiRegistry.test.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiRegistry.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiResolver.test.ts create mode 100644 packages/frontend-plugin-api/src/system/ApiResolver.ts diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index a857eff27d..59b62f3051 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -14,49 +14,67 @@ * limitations under the License. */ -import React, { JSX } from 'react'; -import { ConfigReader, Config } from '@backstage/config'; -import { - AppTree, - appTreeApiRef, - BackstagePlugin, - coreExtensionData, - ExtensionDataRef, - ExtensionOverrides, - RouteRef, - useRouteRef, -} from '@backstage/frontend-plugin-api'; -import { Core } from '../extensions/Core'; -import { CoreRoutes } from '../extensions/CoreRoutes'; -import { CoreLayout } from '../extensions/CoreLayout'; -import { CoreNav } from '../extensions/CoreNav'; +import { Config, ConfigReader } from '@backstage/config'; +import { SidebarItem } from '@backstage/core-components'; import { AnyApiFactory, ApiHolder, AppComponents, AppContext, + BackstagePlugin as LegacyBackstagePlugin, attachComponentData, } from '@backstage/core-plugin-api'; import { - appThemeApiRef, - ConfigApi, - configApiRef, - IconComponent, - BackstagePlugin as LegacyBackstagePlugin, - featureFlagsApiRef, - identityApiRef, - AppTheme, -} from '@backstage/frontend-plugin-api'; -import { getAvailableFeatures } from './discovery'; + appLanguageApiRef, + translationApiRef, +} from '@backstage/core-plugin-api/alpha'; import { ApiFactoryRegistry, ApiProvider, ApiResolver, - AppThemeSelector, -} from '@backstage/core-app-api'; + AppNode, + AppTheme, + AppTree, + BackstagePlugin, + ConfigApi, + ExtensionDataRef, + ExtensionOverrides, + IconComponent, + RouteRef, + appThemeApiRef, + appTreeApiRef, + configApiRef, + coreExtensionData, + featureFlagsApiRef, + identityApiRef, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; +import { JSX, default as React } from 'react'; +import { BrowserRouter, Route } from 'react-router-dom'; +import { Core } from '../extensions/Core'; +import { CoreLayout } from '../extensions/CoreLayout'; +import { CoreNav } from '../extensions/CoreNav'; +import { CoreRoutes } from '../extensions/CoreRoutes'; +import { DarkTheme, LightTheme } from '../extensions/themes'; +import { AppRouteBinder } from '../routing'; +import { RoutingProvider } from '../routing/RoutingProvider'; +import { collectRouteIds } from '../routing/collectRouteIds'; +import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; +import { resolveRouteBindings } from '../routing/resolveRouteBindings'; +import { createAppTree } from '../tree'; +import { getAvailableFeatures } from './discovery'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { + apis as defaultApis, + components as defaultComponents, + icons as defaultIcons, +} from '../../../app-defaults/src/defaults'; // TODO: Get rid of all of these // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppThemeSelector } from '../../../core-app-api/src/apis/implementations/AppThemeApi'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppThemeProvider } from '../../../core-app-api/src/app/AppThemeProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy'; @@ -73,26 +91,6 @@ import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementati // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - apis as defaultApis, - components as defaultComponents, - icons as defaultIcons, -} from '../../../app-defaults/src/defaults'; -import { BrowserRouter, Route } from 'react-router-dom'; -import { SidebarItem } from '@backstage/core-components'; -import { DarkTheme, LightTheme } from '../extensions/themes'; -import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; -import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; -import { - appLanguageApiRef, - translationApiRef, -} from '@backstage/core-plugin-api/alpha'; -import { AppRouteBinder } from '../routing'; -import { RoutingProvider } from '../routing/RoutingProvider'; -import { resolveRouteBindings } from '../routing/resolveRouteBindings'; -import { collectRouteIds } from '../routing/collectRouteIds'; -import { createAppTree } from '../tree'; -import { AppNode } from '@backstage/frontend-plugin-api'; const builtinExtensions = [ Core, diff --git a/packages/frontend-plugin-api/src/system/ApiAggregator.test.ts b/packages/frontend-plugin-api/src/system/ApiAggregator.test.ts new file mode 100644 index 0000000000..c2754878f2 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiAggregator.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { ApiAggregator } from './ApiAggregator'; +import { ApiRegistry } from './ApiRegistry'; + +describe('ApiAggregator', () => { + const apiARef = createApiRef({ id: 'a' }); + const apiBRef = createApiRef({ id: 'b' }); + + it('should forward implementations', () => { + const agg = new ApiAggregator( + ApiRegistry.from([ + [apiARef, 5], + [apiBRef, 10], + ]), + ); + expect(agg.get(apiARef)).toBe(5); + expect(agg.get(apiBRef)).toBe(10); + }); + + it('should return the first implementation', () => { + const agg = new ApiAggregator( + ApiRegistry.from([ + [apiARef, 1], + [apiARef, 2], + ]), + ); + expect(agg.get(apiARef)).toBe(2); + expect(agg.get(apiBRef)).toBe(undefined); + }); +}); diff --git a/packages/frontend-plugin-api/src/system/ApiAggregator.ts b/packages/frontend-plugin-api/src/system/ApiAggregator.ts new file mode 100644 index 0000000000..a6b4471f8c --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiAggregator.ts @@ -0,0 +1,39 @@ +/* + * 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 { ApiRef, ApiHolder } from '@backstage/core-plugin-api'; + +/** + * An ApiHolder that queries multiple other holders from for + * an Api implementation, returning the first one encountered.. + */ +export class ApiAggregator implements ApiHolder { + private readonly holders: ApiHolder[]; + + constructor(...holders: ApiHolder[]) { + this.holders = holders; + } + + get(apiRef: ApiRef): T | undefined { + for (const holder of this.holders) { + const api = holder.get(apiRef); + if (api) { + return api; + } + } + return undefined; + } +} diff --git a/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.test.ts b/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.test.ts new file mode 100644 index 0000000000..e859e8a491 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { ApiFactoryRegistry } from './ApiFactoryRegistry'; + +const aRef = createApiRef({ id: 'a' }); +const aFactory1 = { api: aRef, deps: {}, factory: () => 1 }; +const aFactory2 = { api: aRef, deps: {}, factory: () => 2 }; +const bRef = createApiRef({ id: 'b' }); +const bFactory = { api: bRef, deps: {}, factory: () => 'x' }; +const cRef = createApiRef({ id: 'c' }); +const cFactory = { api: cRef, deps: {}, factory: () => 'y' }; + +describe('ApiFactoryRegistry', () => { + it('should be empty when created', () => { + const registry = new ApiFactoryRegistry(); + expect(registry.getAllApis()).toEqual(new Set()); + }); + + it('should register a factory', () => { + const registry = new ApiFactoryRegistry(); + expect(registry.register('default', aFactory1)).toBe(true); + expect(registry.get(aRef)).toBe(aFactory1); + expect(registry.getAllApis()).toEqual(new Set([aRef])); + }); + + it('should prioritize factories based on scope', () => { + const registry = new ApiFactoryRegistry(); + expect(registry.register('default', aFactory1)).toBe(true); + expect(registry.get(aRef)).toBe(aFactory1); + expect(registry.register('default', aFactory2)).toBe(false); + expect(registry.get(aRef)).toBe(aFactory1); + expect(registry.register('app', aFactory2)).toBe(true); + expect(registry.get(aRef)).toBe(aFactory2); + expect(registry.register('default', aFactory1)).toBe(false); + expect(registry.get(aRef)).toBe(aFactory2); + expect(registry.register('static', aFactory1)).toBe(true); + expect(registry.get(aRef)).toBe(aFactory1); + expect(registry.register('static', aFactory2)).toBe(false); + expect(registry.get(aRef)).toBe(aFactory1); + expect(registry.register('app', aFactory2)).toBe(false); + expect(registry.get(aRef)).toBe(aFactory1); + expect(registry.getAllApis()).toEqual(new Set([aRef])); + }); + + it('should register multiple factories without conflict', () => { + const registry = new ApiFactoryRegistry(); + expect(registry.register('static', aFactory1)).toBe(true); + expect(registry.register('default', bFactory)).toBe(true); + expect(registry.register('app', cFactory)).toBe(true); + expect(registry.get(aRef)).toBe(aFactory1); + expect(registry.get(bRef)).toBe(bFactory); + expect(registry.get(cRef)).toBe(cFactory); + expect(registry.getAllApis()).toEqual(new Set([aRef, bRef, cRef])); + }); + + it('should identify ApiRefs by id but still return the correct factory ref when listing all apis', () => { + const ref1 = createApiRef({ id: 'a' }); + const ref2 = createApiRef({ id: 'a' }); + + const factory1 = { api: ref1, deps: {}, factory: () => 3 }; + const factory2 = { api: ref2, deps: {}, factory: () => 3 }; + + const registry = new ApiFactoryRegistry(); + expect(registry.register('default', factory1)).toBe(true); + expect(registry.register('default', factory2)).toBe(false); + expect(registry.get(ref1)).toEqual(factory1); + expect(registry.get(ref2)).toEqual(factory1); + expect(registry.getAllApis()).toEqual(new Set([ref1])); + + expect(registry.register('app', factory2)).toBe(true); + expect(registry.get(ref1)).toEqual(factory2); + expect(registry.get(ref2)).toEqual(factory2); + expect(Array.from(registry.getAllApis())[0]).toBe(ref2); + expect(Array.from(registry.getAllApis())[0]).not.toBe(ref1); + }); +}); diff --git a/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.ts b/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.ts new file mode 100644 index 0000000000..5f56793cae --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.ts @@ -0,0 +1,95 @@ +/* + * 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 { ApiFactoryHolder } from './types'; +import { + ApiRef, + ApiFactory, + AnyApiRef, + AnyApiFactory, +} from '@backstage/core-plugin-api'; + +/** + * Scope type when registering API factories. + * @public + */ +export type ApiFactoryScope = + | 'default' // Default factories registered by core and plugins + | 'app' // Factories registered in the app, overriding default ones + | 'static'; // APIs that can't be overridden, e.g. config + +enum ScopePriority { + default = 10, + app = 50, + static = 100, +} + +type FactoryTuple = { + priority: number; + factory: AnyApiFactory; +}; + +/** + * ApiFactoryRegistry is an ApiFactoryHolder implementation that enables + * registration of API Factories with different scope. + * + * Each scope has an assigned priority, where factories registered with + * higher priority scopes override ones with lower priority. + * + * @public + */ +export class ApiFactoryRegistry implements ApiFactoryHolder { + private readonly factories = new Map(); + + /** + * Register a new API factory. Returns true if the factory was added + * to the registry. + * + * A factory will not be added to the registry if there is already + * an existing factory with the same or higher priority. + */ + register( + scope: ApiFactoryScope, + factory: ApiFactory, + ) { + const priority = ScopePriority[scope]; + const existing = this.factories.get(factory.api.id); + if (existing && existing.priority >= priority) { + return false; + } + + this.factories.set(factory.api.id, { priority, factory }); + return true; + } + + get( + api: ApiRef, + ): ApiFactory | undefined { + const tuple = this.factories.get(api.id); + if (!tuple) { + return undefined; + } + return tuple.factory as ApiFactory; + } + + getAllApis(): Set { + const refs = new Set(); + for (const { factory } of this.factories.values()) { + refs.add(factory.api); + } + return refs; + } +} diff --git a/packages/frontend-plugin-api/src/system/ApiProvider.test.tsx b/packages/frontend-plugin-api/src/system/ApiProvider.test.tsx new file mode 100644 index 0000000000..6460e12900 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiProvider.test.tsx @@ -0,0 +1,234 @@ +/* + * 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 React from 'react'; +import { + useApi, + createApiRef, + withApis, + ApiHolder, + ApiRef, +} from '@backstage/core-plugin-api'; +import { ApiProvider } from './ApiProvider'; +import { ApiRegistry } from './ApiRegistry'; +import { render } from '@testing-library/react'; +import { withLogCollector } from '@backstage/test-utils'; +import { useVersionedContext } from '@backstage/version-bridge'; + +describe('ApiProvider', () => { + type Api = () => string; + const apiRef = createApiRef({ id: 'x' }); + const registry = ApiRegistry.from([[apiRef, () => 'hello']]); + + const MyHookConsumer = () => { + const api = useApi(apiRef); + return

hook message: {api()}

; + }; + + const MyHocConsumer = withApis({ getMessage: apiRef })(({ getMessage }) => { + return

hoc message: {getMessage()}

; + }); + + it('should provide apis', () => { + const renderedHook = render( + + + , + ); + renderedHook.getByText('hook message: hello'); + + const renderedHoc = render( + + + , + ); + renderedHoc.getByText('hoc message: hello'); + }); + + it('should provide nested access to apis', () => { + const aRef = createApiRef({ id: 'a' }); + const bRef = createApiRef({ id: 'b' }); + + const MyComponent = () => { + const a = useApi(aRef); + const b = useApi(bRef); + return ( +
+ a={a} b={b} +
+ ); + }; + + const renderedHook = render( + + + + + , + ); + renderedHook.getByText('a=z b=y'); + }); + + it('should ignore deps in prototype', () => { + // 100% coverage + happy typescript = hasOwnProperty + this atrocity + const xRef = createApiRef({ id: 'x' }); + + const proto = { x: xRef }; + const props = { getMessage: { enumerable: true, value: apiRef } }; + const obj = Object.create(proto, props) as { + getMessage: typeof apiRef; + x: typeof xRef; + }; + + const MyWeirdHocConsumer = withApis(obj)(({ getMessage }) => { + return

hoc message: {getMessage()}

; + }); + + const renderedHoc = render( + + + , + ); + renderedHoc.getByText('hoc message: hello'); + }); + + it('should error if no provider is available', () => { + expect( + withLogCollector(['error'], () => { + expect(() => { + render(); + }).toThrow(/^API context is not available/); + }).error, + ).toEqual([ + expect.objectContaining({ + detail: new Error('API context is not available'), + type: 'unhandled exception', + }), + expect.objectContaining({ + detail: new Error('API context is not available'), + type: 'unhandled exception', + }), + expect.stringMatching( + /^The above error occurred in the component/, + ), + ]); + + expect( + withLogCollector(['error'], () => { + expect(() => { + render(); + }).toThrow(/^API context is not available/); + }).error, + ).toEqual([ + expect.objectContaining({ + detail: new Error('API context is not available'), + type: 'unhandled exception', + }), + expect.objectContaining({ + detail: new Error('API context is not available'), + type: 'unhandled exception', + }), + expect.stringMatching( + /^The above error occurred in the component/, + ), + ]); + }); + + it('should error if api is not available', () => { + expect( + withLogCollector(['error'], () => { + expect(() => { + render( + + + , + ); + }).toThrow('No implementation available for apiRef{x}'); + }).error, + ).toEqual([ + expect.objectContaining({ + detail: new Error('No implementation available for apiRef{x}'), + type: 'unhandled exception', + }), + expect.objectContaining({ + detail: new Error('No implementation available for apiRef{x}'), + type: 'unhandled exception', + }), + expect.stringMatching( + /^The above error occurred in the component/, + ), + ]); + + expect( + withLogCollector(['error'], () => { + expect(() => { + render( + + + , + ); + }).toThrow('No implementation available for apiRef{x}'); + }).error, + ).toEqual([ + expect.objectContaining({ + detail: new Error('No implementation available for apiRef{x}'), + type: 'unhandled exception', + }), + expect.objectContaining({ + detail: new Error('No implementation available for apiRef{x}'), + type: 'unhandled exception', + }), + expect.stringMatching( + /^The above error occurred in the component/, + ), + ]); + }); +}); + +describe('v1 consumer', () => { + function useMockApiV1(apiRef: ApiRef): T { + const impl = useVersionedContext<{ 1: ApiHolder }>('api-context') + ?.atVersion(1) + ?.get(apiRef); + if (!impl) { + throw new Error('no impl'); + } + return impl; + } + + type Api = () => string; + const apiRef = createApiRef({ id: 'x' }); + const registry = ApiRegistry.from([[apiRef, () => 'hello']]); + + const MyHookConsumerV1 = () => { + const api = useMockApiV1(apiRef); + return

hook message: {api()}

; + }; + + it('should provide apis', () => { + const renderedHook = render( + + + , + ); + renderedHook.getByText('hook message: hello'); + }); +}); diff --git a/packages/frontend-plugin-api/src/system/ApiProvider.tsx b/packages/frontend-plugin-api/src/system/ApiProvider.tsx new file mode 100644 index 0000000000..4894d25de0 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiProvider.tsx @@ -0,0 +1,53 @@ +/* + * 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 React, { useContext, ReactNode, PropsWithChildren } from 'react'; +import { ApiHolder } from '@backstage/core-plugin-api'; +import { ApiAggregator } from './ApiAggregator'; +import { + createVersionedValueMap, + createVersionedContext, +} from '@backstage/version-bridge'; + +/** + * Prop types for the ApiProvider component. + * @public + */ +export type ApiProviderProps = { + apis: ApiHolder; + children: ReactNode; +}; + +const ApiContext = createVersionedContext<{ 1: ApiHolder }>('api-context'); + +/** + * Provides an {@link @backstage/core-plugin-api#ApiHolder} for consumption in + * the React tree. + * + * @public + */ +export const ApiProvider = (props: PropsWithChildren) => { + const { apis, children } = props; + const parentHolder = useContext(ApiContext)?.atVersion(1); + const holder = parentHolder ? new ApiAggregator(apis, parentHolder) : apis; + + return ( + + ); +}; diff --git a/packages/frontend-plugin-api/src/system/ApiRegistry.test.ts b/packages/frontend-plugin-api/src/system/ApiRegistry.test.ts new file mode 100644 index 0000000000..c0be9f8d02 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiRegistry.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { ApiRegistry } from './ApiRegistry'; + +describe('ApiRegistry', () => { + const x1Ref = createApiRef({ id: 'x1' }); + const x1DuplicateRef = createApiRef({ id: 'x1' }); + const x2Ref = createApiRef({ id: 'x2' }); + + it('should be created', () => { + const registry = ApiRegistry.from([]); + expect(registry.get(x1Ref)).toBe(undefined); + }); + + it('should be created with APIs', () => { + const registry = ApiRegistry.from([ + [x1Ref, 3], + [x2Ref, 'y'], + ]); + expect(registry.get(x1Ref)).toBe(3); + expect(registry.get(x1DuplicateRef)).toBe(3); + expect(registry.get(x2Ref)).toBe('y'); + }); + + it('should be built', () => { + const registry = ApiRegistry.builder().build(); + expect(registry.get(x1Ref)).toBe(undefined); + expect(registry.get(x1DuplicateRef)).toBe(undefined); + }); + + it('should be built with APIs', () => { + const builder = ApiRegistry.builder(); + builder.add(x1Ref, 3); + builder.add(x2Ref, 'y'); + + const registry = builder.build(); + expect(registry.get(x1Ref)).toBe(3); + expect(registry.get(x1DuplicateRef)).toBe(3); + expect(registry.get(x2Ref)).toBe('y'); + }); + + it('should be created with API', () => { + const reg1 = ApiRegistry.with(x1Ref, 3); + const reg2 = reg1.with(x2Ref, 'y'); + const reg3 = reg2.with(x2Ref, 'z'); + const reg4 = reg3.with(x1Ref, 2); + const reg5 = reg3.with(x1DuplicateRef, 4); + + expect(reg1.get(x1Ref)).toBe(3); + expect(reg1.get(x2Ref)).toBe(undefined); + expect(reg2.get(x1Ref)).toBe(3); + expect(reg2.get(x2Ref)).toBe('y'); + expect(reg3.get(x1Ref)).toBe(3); + expect(reg3.get(x2Ref)).toBe('z'); + expect(reg4.get(x1Ref)).toBe(2); + expect(reg4.get(x2Ref)).toBe('z'); + expect(reg5.get(x1Ref)).toBe(4); + expect(reg5.get(x2Ref)).toBe('z'); + }); +}); diff --git a/packages/frontend-plugin-api/src/system/ApiRegistry.ts b/packages/frontend-plugin-api/src/system/ApiRegistry.ts new file mode 100644 index 0000000000..d4990a04c5 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiRegistry.ts @@ -0,0 +1,80 @@ +/* + * 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 { ApiRef, ApiHolder } from '@backstage/core-plugin-api'; + +type ApiImpl = readonly [ApiRef, T]; + +/** @internal */ +class ApiRegistryBuilder { + private apis: [string, unknown][] = []; + + add(api: ApiRef, impl: I): I { + this.apis.push([api.id, impl]); + return impl; + } + + build(): ApiRegistry { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return new ApiRegistry(new Map(this.apis)); + } +} + +/** + * A registry for utility APIs. + * + * @internal + */ +export class ApiRegistry implements ApiHolder { + static builder() { + return new ApiRegistryBuilder(); + } + + /** + * Creates a new ApiRegistry with a list of API implementations. + * + * @param apis - A list of pairs mapping an ApiRef to its respective implementation + */ + static from(apis: ApiImpl[]) { + return new ApiRegistry(new Map(apis.map(([api, impl]) => [api.id, impl]))); + } + + /** + * Creates a new ApiRegistry with a single API implementation. + * + * @param api - ApiRef for the API to add + * @param impl - Implementation of the API to add + */ + static with(api: ApiRef, impl: T): ApiRegistry { + return new ApiRegistry(new Map([[api.id, impl]])); + } + + constructor(private readonly apis: Map) {} + + /** + * Returns a new ApiRegistry with the provided API added to the existing ones. + * + * @param api - ApiRef for the API to add + * @param impl - Implementation of the API to add + */ + with(api: ApiRef, impl: T): ApiRegistry { + return new ApiRegistry(new Map([...this.apis, [api.id, impl]])); + } + + get(api: ApiRef): T | undefined { + return this.apis.get(api.id) as T | undefined; + } +} diff --git a/packages/frontend-plugin-api/src/system/ApiResolver.test.ts b/packages/frontend-plugin-api/src/system/ApiResolver.test.ts new file mode 100644 index 0000000000..01491684f4 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiResolver.test.ts @@ -0,0 +1,263 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { ApiResolver } from './ApiResolver'; +import { ApiFactoryRegistry } from './ApiFactoryRegistry'; + +const aRef = createApiRef({ id: 'a' }); +const otherARef = createApiRef({ id: 'a' }); +const bRef = createApiRef({ id: 'b' }); +const otherBRef = createApiRef({ id: 'b' }); +const cRef = createApiRef<{ x: string }>({ id: 'c' }); +const otherCRef = createApiRef<{ x: string }>({ id: 'c' }); + +function createRegistry() { + const registry = new ApiFactoryRegistry(); + registry.register('default', { + api: aRef, + deps: {}, + factory: () => 1, + }); + registry.register('default', { + api: bRef, + deps: {}, + factory: () => 'b', + }); + registry.register('default', { + api: cRef, + deps: { b: otherBRef }, + factory: ({ b }) => ({ x: 'x', b }), + }); + return registry; +} + +function createSelfCyclicRegistry() { + const registry = new ApiFactoryRegistry(); + registry.register('default', { + api: aRef, + deps: { a: aRef }, + factory: () => 1, + }); + return registry; +} + +function createShortCyclicRegistry() { + const registry = new ApiFactoryRegistry(); + registry.register('default', { + api: aRef, + deps: { b: bRef }, + factory: () => 1, + }); + registry.register('default', { + api: bRef, + deps: { a: aRef }, + factory: () => 'x', + }); + return registry; +} + +function createShortCyclicRegistryWithOther() { + const registry = new ApiFactoryRegistry(); + registry.register('default', { + api: aRef, + deps: { b: bRef }, + factory: () => 1, + }); + registry.register('default', { + api: otherBRef, + deps: { a: otherARef }, + factory: () => 'x', + }); + return registry; +} + +function createLongCyclicRegistry() { + const registry = new ApiFactoryRegistry(); + registry.register('default', { + api: aRef, + deps: { b: otherBRef }, + factory: () => 1, + }); + registry.register('default', { + api: bRef, + deps: { c: cRef }, + factory: () => 'b', + }); + registry.register('default', { + api: cRef, + deps: { a: aRef }, + factory: () => ({ x: 'x' }), + }); + return registry; +} + +describe('ApiResolver', () => { + it('should be created empty', () => { + const resolver = new ApiResolver(new ApiFactoryRegistry()); + expect(resolver.get(aRef)).toBe(undefined); + expect(resolver.get(bRef)).toBe(undefined); + expect(resolver.get(otherBRef)).toBe(undefined); + expect(resolver.get(cRef)).toBe(undefined); + }); + + it('should instantiate APIs', () => { + const resolver = new ApiResolver(createRegistry()); + expect(resolver.get(aRef)).toBe(1); + expect(resolver.get(otherARef)).toBe(1); + expect(resolver.get(bRef)).toBe('b'); + expect(resolver.get(otherBRef)).toBe('b'); + expect(resolver.get(cRef)).toEqual({ x: 'x', b: 'b' }); + expect(resolver.get(cRef)).toBe(resolver.get(otherCRef)); + }); + + it('should detect self dependency cycles', () => { + const resolver = new ApiResolver(createSelfCyclicRegistry()); + expect(() => resolver.get(aRef)).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + }); + + it('should detect short dependency cycles', () => { + const resolver = new ApiResolver(createShortCyclicRegistry()); + expect(() => resolver.get(aRef)).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => resolver.get(bRef)).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + }); + + it('should detect short dependency cycles with other refs', () => { + const resolver = new ApiResolver(createShortCyclicRegistryWithOther()); + expect(() => resolver.get(aRef)).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => resolver.get(bRef)).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + expect(() => resolver.get(otherARef)).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => resolver.get(otherBRef)).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + }); + + it('should detect long dependency cycles', () => { + const resolver = new ApiResolver(createLongCyclicRegistry()); + expect(() => resolver.get(aRef)).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + // Second call for same ref should still throw + expect(() => resolver.get(aRef)).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => resolver.get(bRef)).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + expect(() => resolver.get(otherBRef)).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + expect(() => resolver.get(cRef)).toThrow( + 'Circular dependency of api factory for apiRef{c}', + ); + }); + + it('should validate a factory holder', () => { + expect(() => { + ApiResolver.validateFactories(createRegistry(), [ + aRef, + bRef, + otherBRef, + cRef, + ]); + }).not.toThrow(); + }); + + it('should find self cycles with validation', () => { + const self = createSelfCyclicRegistry(); + expect(() => ApiResolver.validateFactories(self, [aRef])).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => ApiResolver.validateFactories(self, [otherARef])).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + }); + + it('should find dependency cycles with validation', () => { + const short = createShortCyclicRegistry(); + expect(() => ApiResolver.validateFactories(short, [aRef])).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => ApiResolver.validateFactories(short, [otherARef])).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => ApiResolver.validateFactories(short, [bRef])).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + expect(() => ApiResolver.validateFactories(short, [otherBRef])).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + + const shortOther = createShortCyclicRegistryWithOther(); + expect(() => ApiResolver.validateFactories(shortOther, [aRef])).toThrow( + 'Circular dependency of api factory for apiRef{a}', + ); + expect(() => + ApiResolver.validateFactories(shortOther, [otherARef]), + ).toThrow('Circular dependency of api factory for apiRef{a}'); + expect(() => ApiResolver.validateFactories(shortOther, [bRef])).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + expect(() => + ApiResolver.validateFactories(shortOther, [otherBRef]), + ).toThrow('Circular dependency of api factory for apiRef{b}'); + + const long = createLongCyclicRegistry(); + expect(() => + ApiResolver.validateFactories(long, long.getAllApis()), + ).toThrow('Circular dependency of api factory for apiRef{a}'); + expect(() => ApiResolver.validateFactories(long, [bRef])).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + expect(() => ApiResolver.validateFactories(long, [otherBRef])).toThrow( + 'Circular dependency of api factory for apiRef{b}', + ); + expect(() => ApiResolver.validateFactories(long, [cRef])).toThrow( + 'Circular dependency of api factory for apiRef{c}', + ); + }); + + it('should only call factory func once', () => { + const registry = new ApiFactoryRegistry(); + const factory = jest.fn().mockReturnValue(2); + registry.register('default', { + api: aRef, + deps: {}, + factory, + }); + + const resolver = new ApiResolver(registry); + expect(factory).toHaveBeenCalledTimes(0); + expect(resolver.get(aRef)).toBe(2); + expect(factory).toHaveBeenCalledTimes(1); + expect(resolver.get(aRef)).toBe(2); + expect(factory).toHaveBeenCalledTimes(1); + expect(resolver.get(otherARef)).toBe(2); + expect(factory).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/frontend-plugin-api/src/system/ApiResolver.ts b/packages/frontend-plugin-api/src/system/ApiResolver.ts new file mode 100644 index 0000000000..0c050ebc30 --- /dev/null +++ b/packages/frontend-plugin-api/src/system/ApiResolver.ts @@ -0,0 +1,116 @@ +/* + * 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 { + ApiRef, + ApiHolder, + AnyApiRef, + TypesToApiRefs, +} from '@backstage/core-plugin-api'; +import { ApiFactoryHolder } from './types'; + +/** + * Handles the actual on-demand instantiation and memoization of APIs out of + * an {@link ApiFactoryHolder}. + * + * @public + */ +export class ApiResolver implements ApiHolder { + /** + * Validate factories by making sure that each of the apis can be created + * without hitting any circular dependencies. + */ + static validateFactories( + factories: ApiFactoryHolder, + apis: Iterable, + ) { + for (const api of apis) { + const heap = [api]; + const allDeps = new Set(); + + while (heap.length) { + const apiRef = heap.shift()!; + const factory = factories.get(apiRef); + if (!factory) { + continue; + } + + for (const dep of Object.values(factory.deps)) { + if (dep.id === api.id) { + throw new Error(`Circular dependency of api factory for ${api}`); + } + if (!allDeps.has(dep)) { + allDeps.add(dep); + heap.push(dep); + } + } + } + } + } + + private readonly apis = new Map(); + + constructor(private readonly factories: ApiFactoryHolder) {} + + get(ref: ApiRef): T | undefined { + return this.load(ref); + } + + private load(ref: ApiRef, loading: AnyApiRef[] = []): T | undefined { + const impl = this.apis.get(ref.id); + if (impl) { + return impl as T; + } + + const factory = this.factories.get(ref); + if (!factory) { + return undefined; + } + + if (loading.includes(factory.api)) { + throw new Error(`Circular dependency of api factory for ${factory.api}`); + } + + const deps = this.loadDeps(ref, factory.deps, [...loading, factory.api]); + const api = factory.factory(deps); + this.apis.set(ref.id, api); + return api as T; + } + + private loadDeps( + dependent: ApiRef, + apis: TypesToApiRefs, + loading: AnyApiRef[], + ): T { + const impls = {} as T; + + for (const key in apis) { + if (apis.hasOwnProperty(key)) { + const ref = apis[key]; + + const api = this.load(ref, loading); + if (!api) { + throw new Error( + `No API factory available for dependency ${ref} of dependent ${dependent}`, + ); + } + impls[key] = api; + } + } + + return impls; + } +} diff --git a/packages/frontend-plugin-api/src/system/index.ts b/packages/frontend-plugin-api/src/system/index.ts index 6aad24daa9..0650bff063 100644 --- a/packages/frontend-plugin-api/src/system/index.ts +++ b/packages/frontend-plugin-api/src/system/index.ts @@ -16,4 +16,7 @@ export { createApiRef } from './ApiRef'; export type { ApiRefConfig } from './ApiRef'; +export { ApiFactoryRegistry } from './ApiFactoryRegistry'; +export { ApiProvider } from './ApiProvider'; +export { ApiResolver } from './ApiResolver'; export * from './types'; diff --git a/packages/frontend-plugin-api/src/system/types.ts b/packages/frontend-plugin-api/src/system/types.ts index 96614c320c..730493c079 100644 --- a/packages/frontend-plugin-api/src/system/types.ts +++ b/packages/frontend-plugin-api/src/system/types.ts @@ -72,3 +72,12 @@ export type AnyApiFactory = ApiFactory< unknown, { [key in string]: unknown } >; + +/** + * @public + */ +export type ApiFactoryHolder = { + get( + api: ApiRef, + ): ApiFactory | undefined; +}; From 9ad4039efaa5bd6699d42a600a61c03e0c0e4596 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 6 Nov 2023 16:07:06 +0100 Subject: [PATCH 03/13] Add changeset & update api-report Co-authored-by: Camila Belo Signed-off-by: Philipp Hugenroth --- .changeset/long-wolves-return.md | 5 + packages/frontend-app-api/api-report.md | 2 +- packages/frontend-plugin-api/api-report.md | 476 +++++++++++++++++- .../frontend-plugin-api/src/system/index.ts | 2 + 4 files changed, 470 insertions(+), 15 deletions(-) create mode 100644 .changeset/long-wolves-return.md diff --git a/.changeset/long-wolves-return.md b/.changeset/long-wolves-return.md new file mode 100644 index 0000000000..8d4d78792f --- /dev/null +++ b/.changeset/long-wolves-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Bringing over apis from core-plugin-api diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 3ca51f6331..5781891549 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -5,7 +5,7 @@ ```ts import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; -import { ConfigApi } from '@backstage/core-plugin-api'; +import { ConfigApi } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionOverrides } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 794da9617b..08dec96eba 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -5,19 +5,54 @@ ```ts /// -import { AnyApiFactory } from '@backstage/core-plugin-api'; -import { AnyApiRef } from '@backstage/core-plugin-api'; -import { ApiRef } from '@backstage/core-plugin-api'; -import { AppTheme } from '@backstage/core-plugin-api'; -import { IconComponent } from '@backstage/core-plugin-api'; +import { AnyApiFactory as AnyApiFactory_2 } from '@backstage/core-plugin-api'; +import { AnyApiRef as AnyApiRef_2 } from '@backstage/core-plugin-api'; +import { ApiFactory as ApiFactory_2 } from '@backstage/core-plugin-api'; +import { ApiHolder as ApiHolder_2 } from '@backstage/core-plugin-api'; +import { ApiRef as ApiRef_2 } from '@backstage/core-plugin-api'; +import { AppTheme as AppTheme_2 } from '@backstage/core-plugin-api'; +import { ComponentType } from 'react'; +import { Config } from '@backstage/config'; +import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; +import { Observable } from '@backstage/types'; +import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { z } from 'zod'; import { ZodSchema } from 'zod'; import { ZodTypeDef } from 'zod'; +// @public +export type AlertApi = { + post(alert: AlertMessage): void; + alert$(): Observable; +}; + +// @public +export const alertApiRef: ApiRef; + +// @public +export type AlertMessage = { + message: string; + severity?: 'success' | 'info' | 'warning' | 'error'; + display?: 'permanent' | 'transient'; +}; + +// @public +export type AnyApiFactory = ApiFactory< + unknown, + unknown, + { + [key in string]: unknown; + } +>; + +// @public +export type AnyApiRef = ApiRef; + // @public (undocumented) export type AnyExtensionDataMap = { [name in string]: ExtensionDataRef< @@ -56,6 +91,96 @@ export type AnyRoutes = { [name in string]: RouteRef; }; +// @public +export type ApiFactory< + Api, + Impl extends Api, + Deps extends { + [name in string]: unknown; + }, +> = { + api: ApiRef; + deps: TypesToApiRefs; + factory(deps: Deps): Impl; +}; + +// @public (undocumented) +export type ApiFactoryHolder = { + get(api: ApiRef): + | ApiFactory< + T, + T, + { + [key in string]: unknown; + } + > + | undefined; +}; + +// @public +export class ApiFactoryRegistry implements ApiFactoryHolder { + // (undocumented) + get(api: ApiRef_2): + | ApiFactory_2< + T, + T, + { + [x: string]: unknown; + } + > + | undefined; + // (undocumented) + getAllApis(): Set; + register< + Api, + Impl extends Api, + Deps extends { + [name in string]: unknown; + }, + >(scope: ApiFactoryScope, factory: ApiFactory_2): boolean; +} + +// @public +export type ApiFactoryScope = 'default' | 'app' | 'static'; + +// @public +export type ApiHolder = { + get(api: ApiRef): T | undefined; +}; + +// @public +export const ApiProvider: ( + props: PropsWithChildren, +) => React_2.JSX.Element; + +// @public +export type ApiProviderProps = { + apis: ApiHolder_2; + children: ReactNode; +}; + +// @public +export type ApiRef = { + id: string; + T: T; +}; + +// @public +export type ApiRefConfig = { + id: string; +}; + +// @public +export class ApiResolver implements ApiHolder_2 { + constructor(factories: ApiFactoryHolder); + // (undocumented) + get(ref: ApiRef_2): T | undefined; + static validateFactories( + factories: ApiFactoryHolder, + apis: Iterable, + ): void; +} + // @public export interface AppNode { readonly edges: AppNodeEdges; @@ -99,6 +224,26 @@ export interface AppNodeSpec { readonly source?: BackstagePlugin; } +// @public +export type AppTheme = { + id: string; + title: string; + variant: 'light' | 'dark'; + icon?: React.ReactElement; + Provider(props: { children: ReactNode }): JSX.Element | null; +}; + +// @public +export type AppThemeApi = { + getInstalledThemes(): AppTheme[]; + activeThemeId$(): Observable; + getActiveThemeId(): string | undefined; + setActiveThemeId(themeId?: string): void; +}; + +// @public +export const appThemeApiRef: ApiRef; + // @public export interface AppTree { readonly nodes: ReadonlyMap; @@ -114,7 +259,39 @@ export interface AppTreeApi { } // @public -export const appTreeApiRef: ApiRef; +export const appTreeApiRef: ApiRef_2; + +// @public +export const atlassianAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + +// @public +export type AuthProviderInfo = { + id: string; + title: string; + icon: IconComponent; +}; + +// @public +export type AuthRequestOptions = { + optional?: boolean; + instantPopup?: boolean; +}; + +// @public +export type BackstageIdentityApi = { + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; +}; + +// @public +export type BackstageIdentityResponse = { + token: string; + expiresAt?: Date; + identity: BackstageUserIdentity; +}; // @public (undocumented) export interface BackstagePlugin< @@ -133,6 +310,29 @@ export interface BackstagePlugin< routes: Routes; } +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; +}; + +// @public +export const bitbucketAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + +// @public +export const bitbucketServerAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + +// @public +export type ConfigApi = Config; + +// @public +export const configApiRef: ApiRef; + // @public (undocumented) export interface ConfigurableExtensionDataRef< TData, @@ -153,10 +353,10 @@ export interface ConfigurableExtensionDataRef< export const coreExtensionData: { reactElement: ConfigurableExtensionDataRef; routePath: ConfigurableExtensionDataRef; - apiFactory: ConfigurableExtensionDataRef; + apiFactory: ConfigurableExtensionDataRef; routeRef: ConfigurableExtensionDataRef, {}>; navTarget: ConfigurableExtensionDataRef; - theme: ConfigurableExtensionDataRef; + theme: ConfigurableExtensionDataRef; }; // @public (undocumented) @@ -166,14 +366,14 @@ export function createApiExtension< >( options: ( | { - api: AnyApiRef; + api: AnyApiRef_2; factory: (options: { config: TConfig; inputs: Expand>; - }) => AnyApiFactory; + }) => AnyApiFactory_2; } | { - factory: AnyApiFactory; + factory: AnyApiFactory_2; } ) & { configSchema?: PortableSchema; @@ -181,6 +381,9 @@ export function createApiExtension< }, ): Extension; +// @public +export function createApiRef(config: ApiRefConfig): ApiRef; + // @public (undocumented) export function createExtension< TOutput extends AnyExtensionDataMap, @@ -277,7 +480,7 @@ export function createNavItemExtension(options: { id: string; routeRef: RouteRef; title: string; - icon: IconComponent; + icon: IconComponent_2; }): Extension<{ title: string; }>; @@ -355,7 +558,39 @@ export function createSubRouteRef< }): MakeSubRouteRef, ParentParams>; // @public (undocumented) -export function createThemeExtension(theme: AppTheme): Extension; +export function createThemeExtension(theme: AppTheme_2): Extension; + +// @public +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; + +// @public +export const discoveryApiRef: ApiRef; + +// @public +export type ErrorApi = { + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>; +}; + +// @public +export type ErrorApiError = { + name: string; + message: string; + stack?: string; +}; + +// @public +export type ErrorApiErrorContext = { + hidden?: boolean; +}; + +// @public +export const errorApiRef: ApiRef; // @public (undocumented) export interface Extension { @@ -488,13 +723,169 @@ export interface ExternalRouteRef< readonly T: TParams; } +// @public +export type FeatureFlag = { + name: string; + pluginId: string; + description?: string; +}; + +// @public +export interface FeatureFlagsApi { + getRegisteredFlags(): FeatureFlag[]; + isActive(name: string): boolean; + registerFlag(flag: FeatureFlag): void; + save(options: FeatureFlagsSaveOptions): void; +} + +// @public +export const featureFlagsApiRef: ApiRef; + +// @public +export type FeatureFlagsSaveOptions = { + states: Record; + merge?: boolean; +}; + +// @public +export enum FeatureFlagState { + Active = 1, + None = 0, +} + +// @public +export type FetchApi = { + fetch: typeof fetch; +}; + +// @public +export const fetchApiRef: ApiRef; + +// @public +export const githubAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + +// @public +export const gitlabAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; + +// @public +export const googleAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; + +// @public +export type IconComponent = ComponentType< + | { + fontSize?: 'large' | 'small' | 'default' | 'inherit'; + } + | { + fontSize?: 'medium' | 'large' | 'small' | 'inherit'; + } +>; + +// @public +export type IdentityApi = { + getProfileInfo(): Promise; + getBackstageIdentity(): Promise; + getCredentials(): Promise<{ + token?: string; + }>; + signOut(): Promise; +}; + +// @public +export const identityApiRef: ApiRef; + +// @public +export const microsoftAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; + // @public (undocumented) export type NavTarget = { title: string; - icon: IconComponent; + icon: IconComponent_2; routeRef: RouteRef; }; +// @public +export type OAuthApi = { + getAccessToken( + scope?: OAuthScope, + options?: AuthRequestOptions, + ): Promise; +}; + +// @public +export type OAuthRequestApi = { + createAuthRequester( + options: OAuthRequesterOptions, + ): OAuthRequester; + authRequest$(): Observable; +}; + +// @public +export const oauthRequestApiRef: ApiRef; + +// @public +export type OAuthRequester = ( + scopes: Set, +) => Promise; + +// @public +export type OAuthRequesterOptions = { + provider: AuthProviderInfo; + onAuthRequest(scopes: Set): Promise; +}; + +// @public +export type OAuthScope = string | string[]; + +// @public +export const oktaAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; + +// @public +export const oneloginAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; + +// @public +export type OpenIdConnectApi = { + getIdToken(options?: AuthRequestOptions): Promise; +}; + +// @public +export type PendingOAuthRequest = { + provider: AuthProviderInfo; + reject(): void; + trigger(): Promise; +}; + // @public (undocumented) export interface PluginOptions< Routes extends AnyRoutes, @@ -516,6 +907,18 @@ export type PortableSchema = { schema: JsonObject; }; +// @public +export type ProfileInfo = { + email?: string; + displayName?: string; + picture?: string; +}; + +// @public +export type ProfileInfoApi = { + getProfile(options?: AuthRequestOptions): Promise; +}; + // @public export type RouteFunc = ( ...[params]: TParams extends undefined @@ -533,6 +936,46 @@ export interface RouteRef< readonly T: TParams; } +// @public +export type SessionApi = { + signIn(): Promise; + signOut(): Promise; + sessionState$(): Observable; +}; + +// @public +export enum SessionState { + SignedIn = 'SignedIn', + SignedOut = 'SignedOut', +} + +// @public +export interface StorageApi { + forBucket(name: string): StorageApi; + observe$( + key: string, + ): Observable>; + remove(key: string): Promise; + set(key: string, data: T): Promise; + snapshot(key: string): StorageValueSnapshot; +} + +// @public +export const storageApiRef: ApiRef; + +// @public +export type StorageValueSnapshot = + | { + key: string; + presence: 'unknown' | 'absent'; + value?: undefined; + } + | { + key: string; + presence: 'present'; + value: TValue; + }; + // @public export interface SubRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, @@ -545,6 +988,11 @@ export interface SubRouteRef< readonly T: TParams; } +// @public +export type TypesToApiRefs = { + [key in keyof T]: ApiRef; +}; + // @public export function useRouteRef< TOptional extends boolean, diff --git a/packages/frontend-plugin-api/src/system/index.ts b/packages/frontend-plugin-api/src/system/index.ts index 0650bff063..ec8984f00f 100644 --- a/packages/frontend-plugin-api/src/system/index.ts +++ b/packages/frontend-plugin-api/src/system/index.ts @@ -17,6 +17,8 @@ export { createApiRef } from './ApiRef'; export type { ApiRefConfig } from './ApiRef'; export { ApiFactoryRegistry } from './ApiFactoryRegistry'; +export type { ApiFactoryScope } from './ApiFactoryRegistry'; export { ApiProvider } from './ApiProvider'; +export type { ApiProviderProps } from './ApiProvider'; export { ApiResolver } from './ApiResolver'; export * from './types'; From db40213615279dcea070c313a6165f1e47d75f50 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 6 Nov 2023 16:27:14 +0100 Subject: [PATCH 04/13] Update yarn.lock Co-authored-by: Camila Belo Signed-off-by: Philipp Hugenroth --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 6641e61326..034eb2e7b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4046,6 +4046,7 @@ __metadata: resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" dependencies: "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-app-api": "workspace:^" From 2096c38db6d605a9e41f05ae6ba5480cfef2a59a Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Nov 2023 15:26:33 +0100 Subject: [PATCH 05/13] fontend-plugin-api: use relative imports Signed-off-by: Vincenzo Scamporlino --- .../index.ts => apis/definitions/AlertApi.ts} | 24 +- .../{ => apis}/definitions/AppLanguageApi.ts | 25 +- .../src/apis/definitions/AppThemeApi.ts | 22 + .../src/{ => apis}/definitions/ConfigApi.ts | 20 +- .../{ => apis}/definitions/DiscoveryApi.ts | 0 .../src/{ => apis}/definitions/ErrorApi.ts | 0 .../{ => apis}/definitions/FeatureFlagsApi.ts | 0 .../src/{ => apis}/definitions/FetchApi.ts | 0 .../src/{ => apis}/definitions/IdentityApi.ts | 0 .../{ => apis}/definitions/OAuthRequestApi.ts | 0 .../src/{ => apis}/definitions/StorageApi.ts | 0 .../src/{ => apis}/definitions/alpha.ts | 0 .../src/apis/definitions/auth.ts | 40 ++ .../src/apis/definitions/index.ts | 19 + .../frontend-plugin-api/src/apis/index.ts | 1 + .../{ => apis}/system/ApiAggregator.test.ts | 0 .../src/{ => apis}/system/ApiAggregator.ts | 0 .../system/ApiFactoryRegistry.test.ts | 0 .../{ => apis}/system/ApiFactoryRegistry.ts | 0 .../{ => apis}/system/ApiProvider.test.tsx | 0 .../src/{ => apis}/system/ApiProvider.tsx | 0 .../src/{ => apis}/system/ApiRef.test.ts | 0 .../src/{ => apis}/system/ApiRef.ts | 0 .../src/{ => apis}/system/ApiRegistry.test.ts | 0 .../src/{ => apis}/system/ApiRegistry.ts | 0 .../src/{ => apis}/system/ApiResolver.test.ts | 0 .../src/{ => apis}/system/ApiResolver.ts | 0 .../src/{ => apis}/system/index.ts | 0 .../src/{ => apis}/system/types.ts | 0 .../src/definitions/AlertApi.ts | 56 --- .../src/definitions/AppThemeApi.ts | 87 ---- .../src/definitions/auth.ts | 452 ------------------ packages/frontend-plugin-api/src/index.ts | 3 +- 33 files changed, 96 insertions(+), 653 deletions(-) rename packages/frontend-plugin-api/src/{definitions/index.ts => apis/definitions/AlertApi.ts} (52%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/AppLanguageApi.ts (56%) create mode 100644 packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts rename packages/frontend-plugin-api/src/{ => apis}/definitions/ConfigApi.ts (59%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/DiscoveryApi.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/ErrorApi.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/FeatureFlagsApi.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/FetchApi.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/IdentityApi.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/OAuthRequestApi.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/StorageApi.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/definitions/alpha.ts (100%) create mode 100644 packages/frontend-plugin-api/src/apis/definitions/auth.ts rename packages/frontend-plugin-api/src/{ => apis}/system/ApiAggregator.test.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiAggregator.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiFactoryRegistry.test.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiFactoryRegistry.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiProvider.test.tsx (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiProvider.tsx (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiRef.test.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiRef.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiRegistry.test.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiRegistry.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiResolver.test.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/ApiResolver.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/index.ts (100%) rename packages/frontend-plugin-api/src/{ => apis}/system/types.ts (100%) delete mode 100644 packages/frontend-plugin-api/src/definitions/AlertApi.ts delete mode 100644 packages/frontend-plugin-api/src/definitions/AppThemeApi.ts delete mode 100644 packages/frontend-plugin-api/src/definitions/auth.ts diff --git a/packages/frontend-plugin-api/src/definitions/index.ts b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts similarity index 52% rename from packages/frontend-plugin-api/src/definitions/index.ts rename to packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts index cc02442f3d..eb42477f77 100644 --- a/packages/frontend-plugin-api/src/definitions/index.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts @@ -14,21 +14,9 @@ * limitations under the License. */ -// This folder contains definitions for all core APIs. -// -// Plugins should rely on these APIs for functionality as much as possible. -// -// If you think some API definition is missing, please open an Issue or send a PR! - -export * from './auth'; - -export * from './AlertApi'; -export * from './AppThemeApi'; -export * from './ConfigApi'; -export * from './DiscoveryApi'; -export * from './ErrorApi'; -export * from './FeatureFlagsApi'; -export * from './FetchApi'; -export * from './IdentityApi'; -export * from './OAuthRequestApi'; -export * from './StorageApi'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type AlertApi, + type AlertMessage, + alertApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/definitions/AppLanguageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts similarity index 56% rename from packages/frontend-plugin-api/src/definitions/AppLanguageApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts index 1207e84755..020742f9e4 100644 --- a/packages/frontend-plugin-api/src/definitions/AppLanguageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts @@ -14,23 +14,8 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/types'; - -/** @alpha */ -export type AppLanguageApi = { - getAvailableLanguages(): { languages: string[] }; - - setLanguage(language?: string): void; - - getLanguage(): { language: string }; - - language$(): Observable<{ language: string }>; -}; - -/** - * @alpha - */ -export const appLanguageApiRef: ApiRef = createApiRef({ - id: 'core.applanguage', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type AppLanguageApi, + appLanguageApiRef, +} from '../../../../core-plugin-api/src/alpha'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts new file mode 100644 index 0000000000..0764cfa6eb --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type AppTheme, + type AppThemeApi, + appThemeApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/definitions/ConfigApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts similarity index 59% rename from packages/frontend-plugin-api/src/definitions/ConfigApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts index d3bada9e46..dc28ff2980 100644 --- a/packages/frontend-plugin-api/src/definitions/ConfigApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts @@ -13,22 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { Config } from '@backstage/config'; -/** - * The Config API is used to provide a mechanism to access the - * runtime configuration of the system. - * - * @public - */ -export type ConfigApi = Config; - -/** - * The {@link ApiRef} of {@link ConfigApi}. - * - * @public - */ -export const configApiRef: ApiRef = createApiRef({ - id: 'core.config', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { type ConfigApi, configApiRef } from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/definitions/DiscoveryApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/DiscoveryApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts diff --git a/packages/frontend-plugin-api/src/definitions/ErrorApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/ErrorApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts diff --git a/packages/frontend-plugin-api/src/definitions/FeatureFlagsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/FeatureFlagsApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts diff --git a/packages/frontend-plugin-api/src/definitions/FetchApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/FetchApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts diff --git a/packages/frontend-plugin-api/src/definitions/IdentityApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/IdentityApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts diff --git a/packages/frontend-plugin-api/src/definitions/OAuthRequestApi.ts b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/OAuthRequestApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts diff --git a/packages/frontend-plugin-api/src/definitions/StorageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/StorageApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts diff --git a/packages/frontend-plugin-api/src/definitions/alpha.ts b/packages/frontend-plugin-api/src/apis/definitions/alpha.ts similarity index 100% rename from packages/frontend-plugin-api/src/definitions/alpha.ts rename to packages/frontend-plugin-api/src/apis/definitions/alpha.ts diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts new file mode 100644 index 0000000000..60ce40c6ad --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -0,0 +1,40 @@ +/* + * 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. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type BackstageIdentityApi, + type BackstageIdentityResponse, + type BackstageUserIdentity, + type AuthProviderInfo, + type AuthRequestOptions, + type OAuthScope, + type OAuthApi, + type OpenIdConnectApi, + type ProfileInfoApi, + type ProfileInfo, + type SessionApi, + SessionState, + atlassianAuthApiRef, + bitbucketAuthApiRef, + bitbucketServerAuthApiRef, + githubAuthApiRef, + gitlabAuthApiRef, + googleAuthApiRef, + oktaAuthApiRef, + microsoftAuthApiRef, + oneloginAuthApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/index.ts b/packages/frontend-plugin-api/src/apis/definitions/index.ts index 8facdca2ca..420942cb19 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/index.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/index.ts @@ -23,3 +23,22 @@ export { type AppTree, type AppTreeApi, } from './AppTreeApi'; + +// This folder contains definitions for all core APIs. +// +// Plugins should rely on these APIs for functionality as much as possible. +// +// If you think some API definition is missing, please open an Issue or send a PR! + +export * from './auth'; + +export * from './AlertApi'; +export * from './AppThemeApi'; +export * from './ConfigApi'; +export * from './DiscoveryApi'; +export * from './ErrorApi'; +export * from './FeatureFlagsApi'; +export * from './FetchApi'; +export * from './IdentityApi'; +export * from './OAuthRequestApi'; +export * from './StorageApi'; diff --git a/packages/frontend-plugin-api/src/apis/index.ts b/packages/frontend-plugin-api/src/apis/index.ts index 5a012c0553..5def6cb0f9 100644 --- a/packages/frontend-plugin-api/src/apis/index.ts +++ b/packages/frontend-plugin-api/src/apis/index.ts @@ -15,3 +15,4 @@ */ export * from './definitions'; +export * from './system'; diff --git a/packages/frontend-plugin-api/src/system/ApiAggregator.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiAggregator.test.ts rename to packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts diff --git a/packages/frontend-plugin-api/src/system/ApiAggregator.ts b/packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiAggregator.ts rename to packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts diff --git a/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiFactoryRegistry.test.ts rename to packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts diff --git a/packages/frontend-plugin-api/src/system/ApiFactoryRegistry.ts b/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiFactoryRegistry.ts rename to packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts diff --git a/packages/frontend-plugin-api/src/system/ApiProvider.test.tsx b/packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiProvider.test.tsx rename to packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx diff --git a/packages/frontend-plugin-api/src/system/ApiProvider.tsx b/packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiProvider.tsx rename to packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx diff --git a/packages/frontend-plugin-api/src/system/ApiRef.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiRef.test.ts rename to packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts diff --git a/packages/frontend-plugin-api/src/system/ApiRef.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiRef.ts rename to packages/frontend-plugin-api/src/apis/system/ApiRef.ts diff --git a/packages/frontend-plugin-api/src/system/ApiRegistry.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiRegistry.test.ts rename to packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts diff --git a/packages/frontend-plugin-api/src/system/ApiRegistry.ts b/packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiRegistry.ts rename to packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts diff --git a/packages/frontend-plugin-api/src/system/ApiResolver.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiResolver.test.ts rename to packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts diff --git a/packages/frontend-plugin-api/src/system/ApiResolver.ts b/packages/frontend-plugin-api/src/apis/system/ApiResolver.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/ApiResolver.ts rename to packages/frontend-plugin-api/src/apis/system/ApiResolver.ts diff --git a/packages/frontend-plugin-api/src/system/index.ts b/packages/frontend-plugin-api/src/apis/system/index.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/index.ts rename to packages/frontend-plugin-api/src/apis/system/index.ts diff --git a/packages/frontend-plugin-api/src/system/types.ts b/packages/frontend-plugin-api/src/apis/system/types.ts similarity index 100% rename from packages/frontend-plugin-api/src/system/types.ts rename to packages/frontend-plugin-api/src/apis/system/types.ts diff --git a/packages/frontend-plugin-api/src/definitions/AlertApi.ts b/packages/frontend-plugin-api/src/definitions/AlertApi.ts deleted file mode 100644 index afdcb6a617..0000000000 --- a/packages/frontend-plugin-api/src/definitions/AlertApi.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { createApiRef, ApiRef } from '../system'; -import { Observable } from '@backstage/types'; - -/** - * Message handled by the {@link AlertApi}. - * - * @public - */ -export type AlertMessage = { - message: string; - // Severity will default to success since that is what material ui defaults the value to. - severity?: 'success' | 'info' | 'warning' | 'error'; - display?: 'permanent' | 'transient'; -}; - -/** - * The alert API is used to report alerts to the app, and display them to the user. - * - * @public - */ -export type AlertApi = { - /** - * Post an alert for handling by the application. - */ - post(alert: AlertMessage): void; - - /** - * Observe alerts posted by other parts of the application. - */ - alert$(): Observable; -}; - -/** - * The {@link ApiRef} of {@link AlertApi}. - * - * @public - */ -export const alertApiRef: ApiRef = createApiRef({ - id: 'core.alert', -}); diff --git a/packages/frontend-plugin-api/src/definitions/AppThemeApi.ts b/packages/frontend-plugin-api/src/definitions/AppThemeApi.ts deleted file mode 100644 index e771fad597..0000000000 --- a/packages/frontend-plugin-api/src/definitions/AppThemeApi.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 { ReactNode } from 'react'; -import { ApiRef, createApiRef } from '../system'; -import { Observable } from '@backstage/types'; - -/** - * Describes a theme provided by the app. - * - * @public - */ -export type AppTheme = { - /** - * ID used to remember theme selections. - */ - id: string; - - /** - * Title of the theme - */ - title: string; - - /** - * Theme variant - */ - variant: 'light' | 'dark'; - - /** - * An Icon for the theme mode setting. - */ - icon?: React.ReactElement; - - Provider(props: { children: ReactNode }): JSX.Element | null; -}; - -/** - * The AppThemeApi gives access to the current app theme, and allows switching - * to other options that have been registered as a part of the App. - * - * @public - */ -export type AppThemeApi = { - /** - * Get a list of available themes. - */ - getInstalledThemes(): AppTheme[]; - - /** - * Observe the currently selected theme. A value of undefined means no specific theme has been selected. - */ - activeThemeId$(): Observable; - - /** - * Get the current theme ID. Returns undefined if no specific theme is selected. - */ - getActiveThemeId(): string | undefined; - - /** - * Set a specific theme to use in the app, overriding the default theme selection. - * - * Clear the selection by passing in undefined. - */ - setActiveThemeId(themeId?: string): void; -}; - -/** - * The {@link ApiRef} of {@link AppThemeApi}. - * - * @public - */ -export const appThemeApiRef: ApiRef = createApiRef({ - id: 'core.apptheme', -}); diff --git a/packages/frontend-plugin-api/src/definitions/auth.ts b/packages/frontend-plugin-api/src/definitions/auth.ts deleted file mode 100644 index 5ed873a566..0000000000 --- a/packages/frontend-plugin-api/src/definitions/auth.ts +++ /dev/null @@ -1,452 +0,0 @@ -/* - * 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 { ApiRef, createApiRef } from '../system'; -import { IconComponent } from '../icons/types'; -import { Observable } from '@backstage/types'; - -/** - * This file contains declarations for common interfaces of auth-related APIs. - * The declarations should be used to signal which type of authentication and - * authorization methods each separate auth provider supports. - * - * For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, - * would be declared as follows: - * - * const googleAuthApiRef = createApiRef({ ... }) - */ - -/** - * Information about the auth provider. - * - * @remarks - * - * This information is used both to connect the correct auth provider in the backend, as - * well as displaying the provider to the user. - * - * @public - */ -export type AuthProviderInfo = { - /** - * The ID of the auth provider. This should match with ID of the provider in the `@backstage/auth-backend`. - */ - id: string; - - /** - * Title for the auth provider, for example "GitHub" - */ - title: string; - - /** - * Icon for the auth provider. - */ - icon: IconComponent; -}; - -/** - * An array of scopes, or a scope string formatted according to the - * auth provider, which is typically a space separated list. - * - * @remarks - * - * See the documentation for each auth provider for the list of scopes - * supported by each provider. - * - * @public - */ -export type OAuthScope = string | string[]; - -/** - * Configuration of an authentication request. - * - * @public - */ -export type AuthRequestOptions = { - /** - * If this is set to true, the user will not be prompted to log in, - * and an empty response will be returned if there is no existing session. - * - * This can be used to perform a check whether the user is logged in, or if you don't - * want to force a user to be logged in, but provide functionality if they already are. - * - * @defaultValue false - */ - optional?: boolean; - - /** - * If this is set to true, the request will bypass the regular oauth login modal - * and open the login popup directly. - * - * The method must be called synchronously from a user action for this to work in all browsers. - * - * @defaultValue false - */ - instantPopup?: boolean; -}; - -/** - * This API provides access to OAuth 2 credentials. It lets you request access tokens, - * which can be used to act on behalf of the user when talking to APIs. - * - * @public - */ -export type OAuthApi = { - /** - * Requests an OAuth 2 Access Token, optionally with a set of scopes. The access token allows - * you to make requests on behalf of the user, and the copes may grant you broader access, depending - * on the auth provider. - * - * Each auth provider has separate handling of scope, so you need to look at the documentation - * for each one to know what scope you need to request. - * - * This method is cheap and should be called each time an access token is used. Do not for example - * store the access token in React component state, as that could cause the token to expire. Instead - * fetch a new access token for each request. - * - * Be sure to include all required scopes when requesting an access token. When testing your implementation - * it is best to log out the Backstage session and then visit your plugin page directly, as - * you might already have some required scopes in your existing session. Not requesting the correct - * scopes can lead to 403 or other authorization errors, which can be tricky to debug. - * - * If the user has not yet granted access to the provider and the set of requested scopes, the user - * will be prompted to log in. The returned promise will not resolve until the user has - * successfully logged in. The returned promise can be rejected, but only if the user rejects the login request. - */ - getAccessToken( - scope?: OAuthScope, - options?: AuthRequestOptions, - ): Promise; -}; - -/** - * This API provides access to OpenID Connect credentials. It lets you request ID tokens, - * which can be passed to backend services to prove the user's identity. - * - * @public - */ -export type OpenIdConnectApi = { - /** - * Requests an OpenID Connect ID Token. - * - * This method is cheap and should be called each time an ID token is used. Do not for example - * store the id token in React component state, as that could cause the token to expire. Instead - * fetch a new id token for each request. - * - * If the user has not yet logged in to Google inside Backstage, the user will be prompted - * to log in. The returned promise will not resolve until the user has successfully logged in. - * The returned promise can be rejected, but only if the user rejects the login request. - */ - getIdToken(options?: AuthRequestOptions): Promise; -}; - -/** - * This API provides access to profile information of the user from an auth provider. - * - * @public - */ -export type ProfileInfoApi = { - /** - * Get profile information for the user as supplied by this auth provider. - * - * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. - */ - getProfile(options?: AuthRequestOptions): Promise; -}; - -/** - * This API provides access to the user's identity within Backstage. - * - * @remarks - * - * An auth provider that implements this interface can be used to sign-in to backstage. It is - * not intended to be used directly from a plugin, but instead serves as a connection between - * this authentication method and the app's {@link IdentityApi} - * - * @public - */ -export type BackstageIdentityApi = { - /** - * Get the user's identity within Backstage. This should normally not be called directly, - * use the {@link IdentityApi} instead. - * - * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. - */ - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; -}; - -/** - * User identity information within Backstage. - * - * @public - */ -export type BackstageUserIdentity = { - /** - * The type of identity that this structure represents. In the frontend app - * this will currently always be 'user'. - */ - type: 'user'; - - /** - * The entityRef of the user in the catalog. - * For example User:default/sandra - */ - userEntityRef: string; - - /** - * The user and group entities that the user claims ownership through - */ - ownershipEntityRefs: string[]; -}; - -/** - * Token and Identity response, with the users claims in the Identity. - * - * @public - */ -export type BackstageIdentityResponse = { - /** - * The token used to authenticate the user within Backstage. - */ - token: string; - - /** - * The time at which the token expires. If not set, it can be assumed that the token does not expire. - */ - expiresAt?: Date; - - /** - * Identity information derived from the token. - */ - identity: BackstageUserIdentity; -}; - -/** - * Profile information of the user. - * - * @public - */ -export type ProfileInfo = { - /** - * Email ID. - */ - email?: string; - - /** - * Display name that can be presented to the user. - */ - displayName?: string; - - /** - * URL to an avatar image of the user. - */ - picture?: string; -}; - -/** - * Session state values passed to subscribers of the SessionApi. - * - * @public - */ -export enum SessionState { - /** - * User signed in. - */ - SignedIn = 'SignedIn', - /** - * User not signed in. - */ - SignedOut = 'SignedOut', -} - -/** - * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. - * - * @public - */ -export type SessionApi = { - /** - * Sign in with a minimum set of permissions. - */ - signIn(): Promise; - - /** - * Sign out from the current session. This will reload the page. - */ - signOut(): Promise; - - /** - * Observe the current state of the auth session. Emits the current state on subscription. - */ - sessionState$(): Observable; -}; - -/** - * Provides authentication towards Google APIs and identities. - * - * @public - * @remarks - * - * See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes. - * - * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, - * email and expiration information. Do not rely on any other fields, as they might not be present. - */ -export const googleAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.google', -}); - -/** - * Provides authentication towards GitHub APIs. - * - * @public - * @remarks - * - * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} - * for a full list of supported scopes. - */ -export const githubAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.github', -}); - -/** - * Provides authentication towards Okta APIs. - * - * @public - * @remarks - * - * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} - * for a full list of supported scopes. - */ -export const oktaAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.okta', -}); - -/** - * Provides authentication towards GitLab APIs. - * - * @public - * @remarks - * - * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} - * for a full list of supported scopes. - */ -export const gitlabAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.gitlab', -}); - -/** - * Provides authentication towards Microsoft APIs and identities. - * - * @public - * @remarks - * - * For more info and a full list of supported scopes, see: - * - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent} - * - {@link https://docs.microsoft.com/en-us/graph/permissions-reference} - */ -export const microsoftAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.microsoft', -}); - -/** - * Provides authentication towards OneLogin APIs. - * - * @public - */ -export const oneloginAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.onelogin', -}); - -/** - * Provides authentication towards Bitbucket APIs. - * - * @public - * @remarks - * - * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} - * for a full list of supported scopes. - */ -export const bitbucketAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.bitbucket', -}); - -/** - * Provides authentication towards Bitbucket Server APIs. - * - * @public - * @remarks - * - * See {@link https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html#BitbucketOAuth2.0providerAPI-scopes} - * for a full list of supported scopes. - */ -export const bitbucketServerAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.bitbucket-server', -}); - -/** - * Provides authentication towards Atlassian APIs. - * - * @public - * @remarks - * - * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} - * for a full list of supported scopes. - */ -export const atlassianAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.atlassian', -}); diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index f3656a0dc3..3248e79945 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -22,10 +22,9 @@ export * from './apis'; export * from './components'; -export * from './definitions'; export * from './extensions'; export * from './icons'; export * from './routing'; export * from './schema'; -export * from './system'; +export * from './apis/system'; export * from './wiring'; From cb1ed7d0810443280cb0d9de8b1b874fedbb27b3 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Nov 2023 17:40:46 +0100 Subject: [PATCH 06/13] frontend-plugin-api: more relative imports Signed-off-by: Vincenzo Scamporlino --- packages/frontend-plugin-api/api-report.md | 376 +++++------------- .../src/apis/definitions/DiscoveryApi.ts | 44 +- .../src/apis/definitions/ErrorApi.ts | 82 +--- .../src/apis/definitions/FeatureFlagsApi.ts | 102 +---- .../src/apis/definitions/FetchApi.ts | 37 +- .../src/apis/definitions/IdentityApi.ts | 42 +- .../src/apis/definitions/OAuthRequestApi.ts | 123 +----- .../src/apis/definitions/StorageApi.ts | 100 +---- 8 files changed, 142 insertions(+), 764 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 08dec96eba..4388414095 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -5,41 +5,80 @@ ```ts /// +import { AlertApi } from '../../../../core-plugin-api'; +import { alertApiRef } from '../../../../core-plugin-api'; +import { AlertMessage } from '../../../../core-plugin-api'; import { AnyApiFactory as AnyApiFactory_2 } from '@backstage/core-plugin-api'; import { AnyApiRef as AnyApiRef_2 } from '@backstage/core-plugin-api'; import { ApiFactory as ApiFactory_2 } from '@backstage/core-plugin-api'; import { ApiHolder as ApiHolder_2 } from '@backstage/core-plugin-api'; import { ApiRef as ApiRef_2 } from '@backstage/core-plugin-api'; +import { AppTheme } from '../../../../core-plugin-api'; import { AppTheme as AppTheme_2 } from '@backstage/core-plugin-api'; +import { AppThemeApi } from '../../../../core-plugin-api'; +import { appThemeApiRef } from '../../../../core-plugin-api'; +import { atlassianAuthApiRef } from '../../../../core-plugin-api'; +import { AuthProviderInfo } from '../../../../core-plugin-api'; +import { AuthRequestOptions } from '../../../../core-plugin-api'; +import { BackstageIdentityApi } from '../../../../core-plugin-api'; +import { BackstageIdentityResponse } from '../../../../core-plugin-api'; +import { BackstageUserIdentity } from '../../../../core-plugin-api'; +import { bitbucketAuthApiRef } from '../../../../core-plugin-api'; +import { bitbucketServerAuthApiRef } from '../../../../core-plugin-api'; import { ComponentType } from 'react'; -import { Config } from '@backstage/config'; +import { ConfigApi } from '../../../../core-plugin-api'; +import { configApiRef } from '../../../../core-plugin-api'; +import { DiscoveryApi } from '../../../../core-plugin-api'; +import { discoveryApiRef } from '../../../../core-plugin-api'; +import { ErrorApi } from '../../../../core-plugin-api'; +import { ErrorApiError } from '../../../../core-plugin-api'; +import { ErrorApiErrorContext } from '../../../../core-plugin-api'; +import { errorApiRef } from '../../../../core-plugin-api'; +import { FeatureFlag } from '../../../../core-plugin-api'; +import { FeatureFlagsApi } from '../../../../core-plugin-api'; +import { featureFlagsApiRef } from '../../../../core-plugin-api'; +import { FeatureFlagsSaveOptions } from '../../../../core-plugin-api'; +import { FeatureFlagState } from '../../../../core-plugin-api'; +import { FetchApi } from '../../../../core-plugin-api'; +import { fetchApiRef } from '../../../../core-plugin-api'; +import { githubAuthApiRef } from '../../../../core-plugin-api'; +import { gitlabAuthApiRef } from '../../../../core-plugin-api'; +import { googleAuthApiRef } from '../../../../core-plugin-api'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; +import { IdentityApi } from '../../../../core-plugin-api'; +import { identityApiRef } from '../../../../core-plugin-api'; import { JsonObject } from '@backstage/types'; -import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; -import { Observable } from '@backstage/types'; +import { microsoftAuthApiRef } from '../../../../core-plugin-api'; +import { OAuthApi } from '../../../../core-plugin-api'; +import { OAuthRequestApi } from '../../../../core-plugin-api'; +import { oauthRequestApiRef } from '../../../../core-plugin-api'; +import { OAuthRequester } from '../../../../core-plugin-api'; +import { OAuthRequesterOptions } from '../../../../core-plugin-api'; +import { OAuthScope } from '../../../../core-plugin-api'; +import { oktaAuthApiRef } from '../../../../core-plugin-api'; +import { oneloginAuthApiRef } from '../../../../core-plugin-api'; +import { OpenIdConnectApi } from '../../../../core-plugin-api'; +import { PendingOAuthRequest } from '../../../../core-plugin-api'; +import { ProfileInfo } from '../../../../core-plugin-api'; +import { ProfileInfoApi } from '../../../../core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; +import { SessionApi } from '../../../../core-plugin-api'; +import { SessionState } from '../../../../core-plugin-api'; +import { StorageApi } from '../../../../core-plugin-api'; +import { storageApiRef } from '../../../../core-plugin-api'; +import { StorageValueSnapshot } from '../../../../core-plugin-api'; import { z } from 'zod'; import { ZodSchema } from 'zod'; import { ZodTypeDef } from 'zod'; -// @public -export type AlertApi = { - post(alert: AlertMessage): void; - alert$(): Observable; -}; +export { AlertApi }; -// @public -export const alertApiRef: ApiRef; +export { alertApiRef }; -// @public -export type AlertMessage = { - message: string; - severity?: 'success' | 'info' | 'warning' | 'error'; - display?: 'permanent' | 'transient'; -}; +export { AlertMessage }; // @public export type AnyApiFactory = ApiFactory< @@ -224,25 +263,11 @@ export interface AppNodeSpec { readonly source?: BackstagePlugin; } -// @public -export type AppTheme = { - id: string; - title: string; - variant: 'light' | 'dark'; - icon?: React.ReactElement; - Provider(props: { children: ReactNode }): JSX.Element | null; -}; +export { AppTheme }; -// @public -export type AppThemeApi = { - getInstalledThemes(): AppTheme[]; - activeThemeId$(): Observable; - getActiveThemeId(): string | undefined; - setActiveThemeId(themeId?: string): void; -}; +export { AppThemeApi }; -// @public -export const appThemeApiRef: ApiRef; +export { appThemeApiRef }; // @public export interface AppTree { @@ -261,37 +286,15 @@ export interface AppTreeApi { // @public export const appTreeApiRef: ApiRef_2; -// @public -export const atlassianAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { atlassianAuthApiRef }; -// @public -export type AuthProviderInfo = { - id: string; - title: string; - icon: IconComponent; -}; +export { AuthProviderInfo }; -// @public -export type AuthRequestOptions = { - optional?: boolean; - instantPopup?: boolean; -}; +export { AuthRequestOptions }; -// @public -export type BackstageIdentityApi = { - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; -}; +export { BackstageIdentityApi }; -// @public -export type BackstageIdentityResponse = { - token: string; - expiresAt?: Date; - identity: BackstageUserIdentity; -}; +export { BackstageIdentityResponse }; // @public (undocumented) export interface BackstagePlugin< @@ -310,28 +313,15 @@ export interface BackstagePlugin< routes: Routes; } -// @public -export type BackstageUserIdentity = { - type: 'user'; - userEntityRef: string; - ownershipEntityRefs: string[]; -}; +export { BackstageUserIdentity }; -// @public -export const bitbucketAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { bitbucketAuthApiRef }; -// @public -export const bitbucketServerAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { bitbucketServerAuthApiRef }; -// @public -export type ConfigApi = Config; +export { ConfigApi }; -// @public -export const configApiRef: ApiRef; +export { configApiRef }; // @public (undocumented) export interface ConfigurableExtensionDataRef< @@ -560,37 +550,17 @@ export function createSubRouteRef< // @public (undocumented) export function createThemeExtension(theme: AppTheme_2): Extension; -// @public -export type DiscoveryApi = { - getBaseUrl(pluginId: string): Promise; -}; +export { DiscoveryApi }; -// @public -export const discoveryApiRef: ApiRef; +export { discoveryApiRef }; -// @public -export type ErrorApi = { - post(error: ErrorApiError, context?: ErrorApiErrorContext): void; - error$(): Observable<{ - error: ErrorApiError; - context?: ErrorApiErrorContext; - }>; -}; +export { ErrorApi }; -// @public -export type ErrorApiError = { - name: string; - message: string; - stack?: string; -}; +export { ErrorApiError }; -// @public -export type ErrorApiErrorContext = { - hidden?: boolean; -}; +export { ErrorApiErrorContext }; -// @public -export const errorApiRef: ApiRef; +export { errorApiRef }; // @public (undocumented) export interface Extension { @@ -723,66 +693,25 @@ export interface ExternalRouteRef< readonly T: TParams; } -// @public -export type FeatureFlag = { - name: string; - pluginId: string; - description?: string; -}; +export { FeatureFlag }; -// @public -export interface FeatureFlagsApi { - getRegisteredFlags(): FeatureFlag[]; - isActive(name: string): boolean; - registerFlag(flag: FeatureFlag): void; - save(options: FeatureFlagsSaveOptions): void; -} +export { FeatureFlagsApi }; -// @public -export const featureFlagsApiRef: ApiRef; +export { featureFlagsApiRef }; -// @public -export type FeatureFlagsSaveOptions = { - states: Record; - merge?: boolean; -}; +export { FeatureFlagsSaveOptions }; -// @public -export enum FeatureFlagState { - Active = 1, - None = 0, -} +export { FeatureFlagState }; -// @public -export type FetchApi = { - fetch: typeof fetch; -}; +export { FetchApi }; -// @public -export const fetchApiRef: ApiRef; +export { fetchApiRef }; -// @public -export const githubAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { githubAuthApiRef }; -// @public -export const gitlabAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { gitlabAuthApiRef }; -// @public -export const googleAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { googleAuthApiRef }; // @public export type IconComponent = ComponentType< @@ -794,27 +723,11 @@ export type IconComponent = ComponentType< } >; -// @public -export type IdentityApi = { - getProfileInfo(): Promise; - getBackstageIdentity(): Promise; - getCredentials(): Promise<{ - token?: string; - }>; - signOut(): Promise; -}; +export { IdentityApi }; -// @public -export const identityApiRef: ApiRef; +export { identityApiRef }; -// @public -export const microsoftAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { microsoftAuthApiRef }; // @public (undocumented) export type NavTarget = { @@ -823,68 +736,25 @@ export type NavTarget = { routeRef: RouteRef; }; -// @public -export type OAuthApi = { - getAccessToken( - scope?: OAuthScope, - options?: AuthRequestOptions, - ): Promise; -}; +export { OAuthApi }; -// @public -export type OAuthRequestApi = { - createAuthRequester( - options: OAuthRequesterOptions, - ): OAuthRequester; - authRequest$(): Observable; -}; +export { OAuthRequestApi }; -// @public -export const oauthRequestApiRef: ApiRef; +export { oauthRequestApiRef }; -// @public -export type OAuthRequester = ( - scopes: Set, -) => Promise; +export { OAuthRequester }; -// @public -export type OAuthRequesterOptions = { - provider: AuthProviderInfo; - onAuthRequest(scopes: Set): Promise; -}; +export { OAuthRequesterOptions }; -// @public -export type OAuthScope = string | string[]; +export { OAuthScope }; -// @public -export const oktaAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { oktaAuthApiRef }; -// @public -export const oneloginAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { oneloginAuthApiRef }; -// @public -export type OpenIdConnectApi = { - getIdToken(options?: AuthRequestOptions): Promise; -}; +export { OpenIdConnectApi }; -// @public -export type PendingOAuthRequest = { - provider: AuthProviderInfo; - reject(): void; - trigger(): Promise; -}; +export { PendingOAuthRequest }; // @public (undocumented) export interface PluginOptions< @@ -907,17 +777,9 @@ export type PortableSchema = { schema: JsonObject; }; -// @public -export type ProfileInfo = { - email?: string; - displayName?: string; - picture?: string; -}; +export { ProfileInfo }; -// @public -export type ProfileInfoApi = { - getProfile(options?: AuthRequestOptions): Promise; -}; +export { ProfileInfoApi }; // @public export type RouteFunc = ( @@ -936,45 +798,15 @@ export interface RouteRef< readonly T: TParams; } -// @public -export type SessionApi = { - signIn(): Promise; - signOut(): Promise; - sessionState$(): Observable; -}; +export { SessionApi }; -// @public -export enum SessionState { - SignedIn = 'SignedIn', - SignedOut = 'SignedOut', -} +export { SessionState }; -// @public -export interface StorageApi { - forBucket(name: string): StorageApi; - observe$( - key: string, - ): Observable>; - remove(key: string): Promise; - set(key: string, data: T): Promise; - snapshot(key: string): StorageValueSnapshot; -} +export { StorageApi }; -// @public -export const storageApiRef: ApiRef; +export { storageApiRef }; -// @public -export type StorageValueSnapshot = - | { - key: string; - presence: 'unknown' | 'absent'; - value?: undefined; - } - | { - key: string; - presence: 'present'; - value: TValue; - }; +export { StorageValueSnapshot }; // @public export interface SubRouteRef< diff --git a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts index d23fe3db6c..db0edc3d20 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts @@ -13,43 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -/** - * The discovery API is used to provide a mechanism for plugins to - * discover the endpoint to use to talk to their backend counterpart. - * - * @remarks - * - * The purpose of the discovery API is to allow for many different deployment - * setups and routing methods through a central configuration, instead - * of letting each individual plugin manage that configuration. - * - * Implementations of the discovery API can be a simple as a URL pattern - * using the pluginId, but could also have overrides for individual plugins, - * or query a separate discovery service. - * - * @public - */ -export type DiscoveryApi = { - /** - * Returns the HTTP base backend URL for a given plugin, without a trailing slash. - * - * This method must always be called just before making a request, as opposed to - * fetching the URL when constructing an API client. That is to ensure that more - * flexible routing patterns can be supported. - * - * For example, asking for the URL for `auth` may return something - * like `https://backstage.example.com/api/auth` - */ - getBaseUrl(pluginId: string): Promise; -}; - -/** - * The {@link ApiRef} of {@link DiscoveryApi}. - * - * @public - */ -export const discoveryApiRef: ApiRef = createApiRef({ - id: 'core.discovery', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type DiscoveryApi, + discoveryApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts index 9c73d94cac..0e77faa4df 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts @@ -14,78 +14,10 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { Observable } from '@backstage/types'; - -/** - * Mirrors the JavaScript Error class, for the purpose of - * providing documentation and optional fields. - * - * @public - */ -export type ErrorApiError = { - name: string; - message: string; - stack?: string; -}; - -/** - * Provides additional information about an error that was posted to the application. - * - * @public - */ -export type ErrorApiErrorContext = { - /** - * If set to true, this error should not be displayed to the user. - * - * Hidden errors are typically not displayed in the UI, but the ErrorApi - * implementation may still report them to error tracking services - * or other utilities that care about all errors. - * - * @defaultValue false - */ - hidden?: boolean; -}; - -/** - * The error API is used to report errors to the app, and display them to the user. - * - * @remarks - * - * Plugins can use this API as a method of displaying errors to the user, but also - * to report errors for collection by error reporting services. - * - * If an error can be displayed inline, e.g. as feedback in a form, that should be - * preferred over relying on this API to display the error. The main use of this API - * for displaying errors should be for asynchronous errors, such as a failing background process. - * - * Even if an error is displayed inline, it should still be reported through this API - * if it would be useful to collect or log it for debugging purposes, but with - * the hidden flag set. For example, an error arising from form field validation - * should probably not be reported, while a failed REST call would be useful to report. - * - * @public - */ -export type ErrorApi = { - /** - * Post an error for handling by the application. - */ - post(error: ErrorApiError, context?: ErrorApiErrorContext): void; - - /** - * Observe errors posted by other parts of the application. - */ - error$(): Observable<{ - error: ErrorApiError; - context?: ErrorApiErrorContext; - }>; -}; - -/** - * The {@link ApiRef} of {@link ErrorApi}. - * - * @public - */ -export const errorApiRef: ApiRef = createApiRef({ - id: 'core.error', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type ErrorApiError, + type ErrorApiErrorContext, + type ErrorApi, + errorApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts index 66260afa32..02ff3fd9ee 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -14,97 +14,11 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; - -/** - * Feature flag descriptor. - * - * @public - */ -export type FeatureFlag = { - name: string; - pluginId: string; - description?: string; -}; - -/** - * Enum representing the state of a feature flag (inactive/active). - * - * @public - */ -export enum FeatureFlagState { - /** - * Feature flag inactive (disabled). - */ - None = 0, - /** - * Feature flag active (enabled). - */ - Active = 1, -} - -/** - * Options to use when saving feature flags. - * - * @public - */ -export type FeatureFlagsSaveOptions = { - /** - * The new feature flag states to save. - */ - states: Record; - - /** - * Whether the saves states should be merged into the existing ones, or replace them. - * - * Defaults to false. - */ - merge?: boolean; -}; - -/** - * The feature flags API is used to toggle functionality to users across plugins and Backstage. - * - * @remarks - * - * Plugins can use this API to register feature flags that they have available - * for users to enable/disable, and this API will centralize the current user's - * state of which feature flags they would like to enable. - * - * This is ideal for Backstage plugins, as well as your own App, to trial incomplete - * or unstable upcoming features. Although there will be a common interface for users - * to enable and disable feature flags, this API acts as another way to enable/disable. - * - * @public - */ -export interface FeatureFlagsApi { - /** - * Registers a new feature flag. Once a feature flag has been registered it - * can be toggled by users, and read back to enable or disable features. - */ - registerFlag(flag: FeatureFlag): void; - - /** - * Get a list of all registered flags. - */ - getRegisteredFlags(): FeatureFlag[]; - - /** - * Whether the feature flag with the given name is currently activated for the user. - */ - isActive(name: string): boolean; - - /** - * Save the user's choice of feature flag states. - */ - save(options: FeatureFlagsSaveOptions): void; -} - -/** - * The {@link ApiRef} of {@link FeatureFlagsApi}. - * - * @public - */ -export const featureFlagsApiRef: ApiRef = createApiRef({ - id: 'core.featureflags', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type FeatureFlag, + type FeatureFlagState, + type FeatureFlagsSaveOptions, + type FeatureFlagsApi, + featureFlagsApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts index aba0e53bb7..8d090fc6cc 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts @@ -14,38 +14,5 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; - -/** - * A wrapper for the fetch API, that has additional behaviors such as the - * ability to automatically inject auth information where necessary. - * - * @public - */ -export type FetchApi = { - /** - * The `fetch` implementation. - */ - fetch: typeof fetch; -}; - -/** - * The {@link ApiRef} of {@link FetchApi}. - * - * @remarks - * - * This is a wrapper for the fetch API, that has additional behaviors such as - * the ability to automatically inject auth information where necessary. - * - * Note that the default behavior of this API (unless overridden by your org), - * is to require that the user is already signed in so that it has auth - * information to inject. Therefore, using the default implementation of this - * utility API e.g. on the `SignInPage` or similar, would cause issues. In - * special circumstances like those, you can use the regular system `fetch` - * instead. - * - * @public - */ -export const fetchApiRef: ApiRef = createApiRef({ - id: 'core.fetch', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { type FetchApi, fetchApiRef } from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts index 1b127a1971..72ca324f32 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts @@ -13,44 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { BackstageUserIdentity, ProfileInfo } from './auth'; -/** - * The Identity API used to identify and get information about the signed in user. - * - * @public - */ -export type IdentityApi = { - /** - * The profile of the signed in user. - */ - getProfileInfo(): Promise; - - /** - * User identity information within Backstage. - */ - getBackstageIdentity(): Promise; - - /** - * Provides credentials in the form of a token which proves the identity of the signed in user. - * - * The token will be undefined if the signed in user does not have a verified - * identity, such as a demo user or mocked user for e2e tests. - */ - getCredentials(): Promise<{ token?: string }>; - - /** - * Sign out the current user - */ - signOut(): Promise; -}; - -/** - * The {@link ApiRef} of {@link IdentityApi}. - * - * @public - */ -export const identityApiRef: ApiRef = createApiRef({ - id: 'core.identity', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { type IdentityApi, identityApiRef } from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 75bf3a3864..2b22625873 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -14,118 +14,11 @@ * limitations under the License. */ -import { Observable } from '@backstage/types'; -import { ApiRef, createApiRef } from '../system'; -import { AuthProviderInfo } from './auth'; - -/** - * Describes how to handle auth requests. Both how to show them to the user, and what to do when - * the user accesses the auth request. - * - * @public - */ -export type OAuthRequesterOptions = { - /** - * Information about the auth provider, which will be forwarded to auth requests. - */ - provider: AuthProviderInfo; - - /** - * Implementation of the auth flow, which will be called synchronously when - * trigger() is called on an auth requests. - */ - onAuthRequest(scopes: Set): Promise; -}; - -/** - * Function used to trigger new auth requests for a set of scopes. - * - * @remarks - * - * The returned promise will resolve to the same value returned by the onAuthRequest in the - * {@link OAuthRequesterOptions}. Or rejected, if the request is rejected. - * - * This function can be called multiple times before the promise resolves. All calls - * will be merged into one request, and the scopes forwarded to the onAuthRequest will be the - * union of all requested scopes. - * - * @public - */ -export type OAuthRequester = ( - scopes: Set, -) => Promise; - -/** - * An pending auth request for a single auth provider. The request will remain in this pending - * state until either reject() or trigger() is called. - * - * @remarks - * - * Any new requests for the same provider are merged into the existing pending request, meaning - * there will only ever be a single pending request for a given provider. - * - * @public - */ -export type PendingOAuthRequest = { - /** - * Information about the auth provider, as given in the AuthRequesterOptions - */ - provider: AuthProviderInfo; - - /** - * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError". - */ - reject(): void; - - /** - * Trigger the auth request to continue the auth flow, by for example showing a popup. - * - * Synchronously calls onAuthRequest with all scope currently in the request. - */ - trigger(): Promise; -}; - -/** - * Provides helpers for implemented OAuth login flows within Backstage. - * - * @public - */ -export type OAuthRequestApi = { - /** - * A utility for showing login popups or similar things, and merging together multiple requests for - * different scopes into one request that includes all scopes. - * - * The passed in options provide information about the login provider, and how to handle auth requests. - * - * The returned AuthRequester function is used to request login with new scopes. These requests - * are merged together and forwarded to the auth handler, as soon as a consumer of auth requests - * triggers an auth flow. - * - * See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. - */ - createAuthRequester( - options: OAuthRequesterOptions, - ): OAuthRequester; - - /** - * Observers pending auth requests. The returned observable will emit all - * current active auth request, at most one for each created auth requester. - * - * Each request has its own info about the login provider, forwarded from the auth requester options. - * - * Depending on user interaction, the request should either be rejected, or used to trigger the auth handler. - * If the request is rejected, all pending AuthRequester calls will fail with a "RejectedError". - * If a auth is triggered, and the auth handler resolves successfully, then all currently pending - * AuthRequester calls will resolve to the value returned by the onAuthRequest call. - */ - authRequest$(): Observable; -}; - -/** - * The {@link ApiRef} of {@link OAuthRequestApi}. - * - * @public - */ -export const oauthRequestApiRef: ApiRef = createApiRef({ - id: 'core.oauthrequest', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type OAuthRequesterOptions, + type OAuthRequester, + type PendingOAuthRequest, + type OAuthRequestApi, + oauthRequestApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts index 1506e5f143..346912e9d7 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts @@ -14,97 +14,9 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { JsonValue, Observable } from '@backstage/types'; - -/** - * A snapshot in time of the current known value of a storage key. - * - * @public - */ -export type StorageValueSnapshot = - | { - key: string; - presence: 'unknown' | 'absent'; - value?: undefined; - } - | { - key: string; - presence: 'present'; - value: TValue; - }; - -/** - * Provides a key-value persistence API. - * - * @public - */ -export interface StorageApi { - /** - * Create a bucket to store data in. - * - * @param name - Namespace for the storage to be stored under, - * will inherit previous namespaces too - */ - forBucket(name: string): StorageApi; - - /** - * Remove persistent data. - * - * @param key - Unique key associated with the data. - */ - remove(key: string): Promise; - - /** - * Save persistent data, and emit messages to anyone that is using - * {@link StorageApi.observe$} for this key. - * - * @param key - Unique key associated with the data. - * @param data - The data to be stored under the key. - */ - set(key: string, data: T): Promise; - - /** - * Observe the value over time for a particular key in the current bucket. - * - * @remarks - * - * The observable will only emit values when the value changes in the underlying - * storage, although multiple values with the same shape may be emitted in a row. - * - * If a {@link StorageApi.snapshot} of a key is retrieved and the presence is - * `'unknown'`, then you are guaranteed to receive a snapshot with a known - * presence, as long as you observe the key within the same tick. - * - * Since the emitted values are shared across all subscribers, it is important - * not to mutate the returned values. The values may be frozen as a precaution. - * - * @param key - Unique key associated with the data - */ - observe$( - key: string, - ): Observable>; - - /** - * Returns an immediate snapshot value for the given key, if possible. - * - * @remarks - * - * Combine with {@link StorageApi.observe$} to get notified of value changes. - * - * Note that this method is synchronous, and some underlying storages may be - * unable to retrieve a value using this method - the result may or may not - * consistently have a presence of 'unknown'. Use {@link StorageApi.observe$} - * to be sure to receive an actual value eventually. - */ - snapshot(key: string): StorageValueSnapshot; -} - -/** - * The {@link ApiRef} of {@link StorageApi}. - * - * @public - */ -export const storageApiRef: ApiRef = createApiRef({ - id: 'core.storage', -}); +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type StorageValueSnapshot, + type StorageApi, + storageApiRef, +} from '../../../../core-plugin-api'; From b0716b55d3c5488d91faf7d35036ff982f2e1780 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Nov 2023 19:25:54 +0100 Subject: [PATCH 07/13] frontend-plugin-api: remove wrong apis Signed-off-by: Vincenzo Scamporlino --- .../src/apis/system/ApiAggregator.test.ts | 46 --- .../src/apis/system/ApiAggregator.ts | 39 --- .../apis/system/ApiFactoryRegistry.test.ts | 91 ------ .../src/apis/system/ApiFactoryRegistry.ts | 95 ------- .../src/apis/system/ApiProvider.test.tsx | 234 ---------------- .../src/apis/system/ApiProvider.tsx | 53 ---- .../src/apis/system/ApiRegistry.test.ts | 75 ----- .../src/apis/system/ApiRegistry.ts | 80 ------ .../src/apis/system/ApiResolver.test.ts | 263 ------------------ .../src/apis/system/ApiResolver.ts | 116 -------- .../src/apis/system/index.ts | 5 - 11 files changed, 1097 deletions(-) delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiResolver.ts diff --git a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts deleted file mode 100644 index c2754878f2..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { createApiRef } from '@backstage/core-plugin-api'; -import { ApiAggregator } from './ApiAggregator'; -import { ApiRegistry } from './ApiRegistry'; - -describe('ApiAggregator', () => { - const apiARef = createApiRef({ id: 'a' }); - const apiBRef = createApiRef({ id: 'b' }); - - it('should forward implementations', () => { - const agg = new ApiAggregator( - ApiRegistry.from([ - [apiARef, 5], - [apiBRef, 10], - ]), - ); - expect(agg.get(apiARef)).toBe(5); - expect(agg.get(apiBRef)).toBe(10); - }); - - it('should return the first implementation', () => { - const agg = new ApiAggregator( - ApiRegistry.from([ - [apiARef, 1], - [apiARef, 2], - ]), - ); - expect(agg.get(apiARef)).toBe(2); - expect(agg.get(apiBRef)).toBe(undefined); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts b/packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts deleted file mode 100644 index a6b4471f8c..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { ApiRef, ApiHolder } from '@backstage/core-plugin-api'; - -/** - * An ApiHolder that queries multiple other holders from for - * an Api implementation, returning the first one encountered.. - */ -export class ApiAggregator implements ApiHolder { - private readonly holders: ApiHolder[]; - - constructor(...holders: ApiHolder[]) { - this.holders = holders; - } - - get(apiRef: ApiRef): T | undefined { - for (const holder of this.holders) { - const api = holder.get(apiRef); - if (api) { - return api; - } - } - return undefined; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts deleted file mode 100644 index e859e8a491..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 { createApiRef } from '@backstage/core-plugin-api'; -import { ApiFactoryRegistry } from './ApiFactoryRegistry'; - -const aRef = createApiRef({ id: 'a' }); -const aFactory1 = { api: aRef, deps: {}, factory: () => 1 }; -const aFactory2 = { api: aRef, deps: {}, factory: () => 2 }; -const bRef = createApiRef({ id: 'b' }); -const bFactory = { api: bRef, deps: {}, factory: () => 'x' }; -const cRef = createApiRef({ id: 'c' }); -const cFactory = { api: cRef, deps: {}, factory: () => 'y' }; - -describe('ApiFactoryRegistry', () => { - it('should be empty when created', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.getAllApis()).toEqual(new Set()); - }); - - it('should register a factory', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.register('default', aFactory1)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.getAllApis()).toEqual(new Set([aRef])); - }); - - it('should prioritize factories based on scope', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.register('default', aFactory1)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('default', aFactory2)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('app', aFactory2)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory2); - expect(registry.register('default', aFactory1)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory2); - expect(registry.register('static', aFactory1)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('static', aFactory2)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.register('app', aFactory2)).toBe(false); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.getAllApis()).toEqual(new Set([aRef])); - }); - - it('should register multiple factories without conflict', () => { - const registry = new ApiFactoryRegistry(); - expect(registry.register('static', aFactory1)).toBe(true); - expect(registry.register('default', bFactory)).toBe(true); - expect(registry.register('app', cFactory)).toBe(true); - expect(registry.get(aRef)).toBe(aFactory1); - expect(registry.get(bRef)).toBe(bFactory); - expect(registry.get(cRef)).toBe(cFactory); - expect(registry.getAllApis()).toEqual(new Set([aRef, bRef, cRef])); - }); - - it('should identify ApiRefs by id but still return the correct factory ref when listing all apis', () => { - const ref1 = createApiRef({ id: 'a' }); - const ref2 = createApiRef({ id: 'a' }); - - const factory1 = { api: ref1, deps: {}, factory: () => 3 }; - const factory2 = { api: ref2, deps: {}, factory: () => 3 }; - - const registry = new ApiFactoryRegistry(); - expect(registry.register('default', factory1)).toBe(true); - expect(registry.register('default', factory2)).toBe(false); - expect(registry.get(ref1)).toEqual(factory1); - expect(registry.get(ref2)).toEqual(factory1); - expect(registry.getAllApis()).toEqual(new Set([ref1])); - - expect(registry.register('app', factory2)).toBe(true); - expect(registry.get(ref1)).toEqual(factory2); - expect(registry.get(ref2)).toEqual(factory2); - expect(Array.from(registry.getAllApis())[0]).toBe(ref2); - expect(Array.from(registry.getAllApis())[0]).not.toBe(ref1); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts b/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts deleted file mode 100644 index 5f56793cae..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 { ApiFactoryHolder } from './types'; -import { - ApiRef, - ApiFactory, - AnyApiRef, - AnyApiFactory, -} from '@backstage/core-plugin-api'; - -/** - * Scope type when registering API factories. - * @public - */ -export type ApiFactoryScope = - | 'default' // Default factories registered by core and plugins - | 'app' // Factories registered in the app, overriding default ones - | 'static'; // APIs that can't be overridden, e.g. config - -enum ScopePriority { - default = 10, - app = 50, - static = 100, -} - -type FactoryTuple = { - priority: number; - factory: AnyApiFactory; -}; - -/** - * ApiFactoryRegistry is an ApiFactoryHolder implementation that enables - * registration of API Factories with different scope. - * - * Each scope has an assigned priority, where factories registered with - * higher priority scopes override ones with lower priority. - * - * @public - */ -export class ApiFactoryRegistry implements ApiFactoryHolder { - private readonly factories = new Map(); - - /** - * Register a new API factory. Returns true if the factory was added - * to the registry. - * - * A factory will not be added to the registry if there is already - * an existing factory with the same or higher priority. - */ - register( - scope: ApiFactoryScope, - factory: ApiFactory, - ) { - const priority = ScopePriority[scope]; - const existing = this.factories.get(factory.api.id); - if (existing && existing.priority >= priority) { - return false; - } - - this.factories.set(factory.api.id, { priority, factory }); - return true; - } - - get( - api: ApiRef, - ): ApiFactory | undefined { - const tuple = this.factories.get(api.id); - if (!tuple) { - return undefined; - } - return tuple.factory as ApiFactory; - } - - getAllApis(): Set { - const refs = new Set(); - for (const { factory } of this.factories.values()) { - refs.add(factory.api); - } - return refs; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx b/packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx deleted file mode 100644 index 6460e12900..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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 React from 'react'; -import { - useApi, - createApiRef, - withApis, - ApiHolder, - ApiRef, -} from '@backstage/core-plugin-api'; -import { ApiProvider } from './ApiProvider'; -import { ApiRegistry } from './ApiRegistry'; -import { render } from '@testing-library/react'; -import { withLogCollector } from '@backstage/test-utils'; -import { useVersionedContext } from '@backstage/version-bridge'; - -describe('ApiProvider', () => { - type Api = () => string; - const apiRef = createApiRef({ id: 'x' }); - const registry = ApiRegistry.from([[apiRef, () => 'hello']]); - - const MyHookConsumer = () => { - const api = useApi(apiRef); - return

hook message: {api()}

; - }; - - const MyHocConsumer = withApis({ getMessage: apiRef })(({ getMessage }) => { - return

hoc message: {getMessage()}

; - }); - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - - const renderedHoc = render( - - - , - ); - renderedHoc.getByText('hoc message: hello'); - }); - - it('should provide nested access to apis', () => { - const aRef = createApiRef({ id: 'a' }); - const bRef = createApiRef({ id: 'b' }); - - const MyComponent = () => { - const a = useApi(aRef); - const b = useApi(bRef); - return ( -
- a={a} b={b} -
- ); - }; - - const renderedHook = render( - - - - - , - ); - renderedHook.getByText('a=z b=y'); - }); - - it('should ignore deps in prototype', () => { - // 100% coverage + happy typescript = hasOwnProperty + this atrocity - const xRef = createApiRef({ id: 'x' }); - - const proto = { x: xRef }; - const props = { getMessage: { enumerable: true, value: apiRef } }; - const obj = Object.create(proto, props) as { - getMessage: typeof apiRef; - x: typeof xRef; - }; - - const MyWeirdHocConsumer = withApis(obj)(({ getMessage }) => { - return

hoc message: {getMessage()}

; - }); - - const renderedHoc = render( - - - , - ); - renderedHoc.getByText('hoc message: hello'); - }); - - it('should error if no provider is available', () => { - expect( - withLogCollector(['error'], () => { - expect(() => { - render(); - }).toThrow(/^API context is not available/); - }).error, - ).toEqual([ - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - - expect( - withLogCollector(['error'], () => { - expect(() => { - render(); - }).toThrow(/^API context is not available/); - }).error, - ).toEqual([ - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - }); - - it('should error if api is not available', () => { - expect( - withLogCollector(['error'], () => { - expect(() => { - render( - - - , - ); - }).toThrow('No implementation available for apiRef{x}'); - }).error, - ).toEqual([ - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - - expect( - withLogCollector(['error'], () => { - expect(() => { - render( - - - , - ); - }).toThrow('No implementation available for apiRef{x}'); - }).error, - ).toEqual([ - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - }); -}); - -describe('v1 consumer', () => { - function useMockApiV1(apiRef: ApiRef): T { - const impl = useVersionedContext<{ 1: ApiHolder }>('api-context') - ?.atVersion(1) - ?.get(apiRef); - if (!impl) { - throw new Error('no impl'); - } - return impl; - } - - type Api = () => string; - const apiRef = createApiRef({ id: 'x' }); - const registry = ApiRegistry.from([[apiRef, () => 'hello']]); - - const MyHookConsumerV1 = () => { - const api = useMockApiV1(apiRef); - return

hook message: {api()}

; - }; - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx b/packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx deleted file mode 100644 index 4894d25de0..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 React, { useContext, ReactNode, PropsWithChildren } from 'react'; -import { ApiHolder } from '@backstage/core-plugin-api'; -import { ApiAggregator } from './ApiAggregator'; -import { - createVersionedValueMap, - createVersionedContext, -} from '@backstage/version-bridge'; - -/** - * Prop types for the ApiProvider component. - * @public - */ -export type ApiProviderProps = { - apis: ApiHolder; - children: ReactNode; -}; - -const ApiContext = createVersionedContext<{ 1: ApiHolder }>('api-context'); - -/** - * Provides an {@link @backstage/core-plugin-api#ApiHolder} for consumption in - * the React tree. - * - * @public - */ -export const ApiProvider = (props: PropsWithChildren) => { - const { apis, children } = props; - const parentHolder = useContext(ApiContext)?.atVersion(1); - const holder = parentHolder ? new ApiAggregator(apis, parentHolder) : apis; - - return ( - - ); -}; diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts deleted file mode 100644 index c0be9f8d02..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 { createApiRef } from '@backstage/core-plugin-api'; -import { ApiRegistry } from './ApiRegistry'; - -describe('ApiRegistry', () => { - const x1Ref = createApiRef({ id: 'x1' }); - const x1DuplicateRef = createApiRef({ id: 'x1' }); - const x2Ref = createApiRef({ id: 'x2' }); - - it('should be created', () => { - const registry = ApiRegistry.from([]); - expect(registry.get(x1Ref)).toBe(undefined); - }); - - it('should be created with APIs', () => { - const registry = ApiRegistry.from([ - [x1Ref, 3], - [x2Ref, 'y'], - ]); - expect(registry.get(x1Ref)).toBe(3); - expect(registry.get(x1DuplicateRef)).toBe(3); - expect(registry.get(x2Ref)).toBe('y'); - }); - - it('should be built', () => { - const registry = ApiRegistry.builder().build(); - expect(registry.get(x1Ref)).toBe(undefined); - expect(registry.get(x1DuplicateRef)).toBe(undefined); - }); - - it('should be built with APIs', () => { - const builder = ApiRegistry.builder(); - builder.add(x1Ref, 3); - builder.add(x2Ref, 'y'); - - const registry = builder.build(); - expect(registry.get(x1Ref)).toBe(3); - expect(registry.get(x1DuplicateRef)).toBe(3); - expect(registry.get(x2Ref)).toBe('y'); - }); - - it('should be created with API', () => { - const reg1 = ApiRegistry.with(x1Ref, 3); - const reg2 = reg1.with(x2Ref, 'y'); - const reg3 = reg2.with(x2Ref, 'z'); - const reg4 = reg3.with(x1Ref, 2); - const reg5 = reg3.with(x1DuplicateRef, 4); - - expect(reg1.get(x1Ref)).toBe(3); - expect(reg1.get(x2Ref)).toBe(undefined); - expect(reg2.get(x1Ref)).toBe(3); - expect(reg2.get(x2Ref)).toBe('y'); - expect(reg3.get(x1Ref)).toBe(3); - expect(reg3.get(x2Ref)).toBe('z'); - expect(reg4.get(x1Ref)).toBe(2); - expect(reg4.get(x2Ref)).toBe('z'); - expect(reg5.get(x1Ref)).toBe(4); - expect(reg5.get(x2Ref)).toBe('z'); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts b/packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts deleted file mode 100644 index d4990a04c5..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 { ApiRef, ApiHolder } from '@backstage/core-plugin-api'; - -type ApiImpl = readonly [ApiRef, T]; - -/** @internal */ -class ApiRegistryBuilder { - private apis: [string, unknown][] = []; - - add(api: ApiRef, impl: I): I { - this.apis.push([api.id, impl]); - return impl; - } - - build(): ApiRegistry { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - return new ApiRegistry(new Map(this.apis)); - } -} - -/** - * A registry for utility APIs. - * - * @internal - */ -export class ApiRegistry implements ApiHolder { - static builder() { - return new ApiRegistryBuilder(); - } - - /** - * Creates a new ApiRegistry with a list of API implementations. - * - * @param apis - A list of pairs mapping an ApiRef to its respective implementation - */ - static from(apis: ApiImpl[]) { - return new ApiRegistry(new Map(apis.map(([api, impl]) => [api.id, impl]))); - } - - /** - * Creates a new ApiRegistry with a single API implementation. - * - * @param api - ApiRef for the API to add - * @param impl - Implementation of the API to add - */ - static with(api: ApiRef, impl: T): ApiRegistry { - return new ApiRegistry(new Map([[api.id, impl]])); - } - - constructor(private readonly apis: Map) {} - - /** - * Returns a new ApiRegistry with the provided API added to the existing ones. - * - * @param api - ApiRef for the API to add - * @param impl - Implementation of the API to add - */ - with(api: ApiRef, impl: T): ApiRegistry { - return new ApiRegistry(new Map([...this.apis, [api.id, impl]])); - } - - get(api: ApiRef): T | undefined { - return this.apis.get(api.id) as T | undefined; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts deleted file mode 100644 index 01491684f4..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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 { createApiRef } from '@backstage/core-plugin-api'; -import { ApiResolver } from './ApiResolver'; -import { ApiFactoryRegistry } from './ApiFactoryRegistry'; - -const aRef = createApiRef({ id: 'a' }); -const otherARef = createApiRef({ id: 'a' }); -const bRef = createApiRef({ id: 'b' }); -const otherBRef = createApiRef({ id: 'b' }); -const cRef = createApiRef<{ x: string }>({ id: 'c' }); -const otherCRef = createApiRef<{ x: string }>({ id: 'c' }); - -function createRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: {}, - factory: () => 1, - }); - registry.register('default', { - api: bRef, - deps: {}, - factory: () => 'b', - }); - registry.register('default', { - api: cRef, - deps: { b: otherBRef }, - factory: ({ b }) => ({ x: 'x', b }), - }); - return registry; -} - -function createSelfCyclicRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { a: aRef }, - factory: () => 1, - }); - return registry; -} - -function createShortCyclicRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { b: bRef }, - factory: () => 1, - }); - registry.register('default', { - api: bRef, - deps: { a: aRef }, - factory: () => 'x', - }); - return registry; -} - -function createShortCyclicRegistryWithOther() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { b: bRef }, - factory: () => 1, - }); - registry.register('default', { - api: otherBRef, - deps: { a: otherARef }, - factory: () => 'x', - }); - return registry; -} - -function createLongCyclicRegistry() { - const registry = new ApiFactoryRegistry(); - registry.register('default', { - api: aRef, - deps: { b: otherBRef }, - factory: () => 1, - }); - registry.register('default', { - api: bRef, - deps: { c: cRef }, - factory: () => 'b', - }); - registry.register('default', { - api: cRef, - deps: { a: aRef }, - factory: () => ({ x: 'x' }), - }); - return registry; -} - -describe('ApiResolver', () => { - it('should be created empty', () => { - const resolver = new ApiResolver(new ApiFactoryRegistry()); - expect(resolver.get(aRef)).toBe(undefined); - expect(resolver.get(bRef)).toBe(undefined); - expect(resolver.get(otherBRef)).toBe(undefined); - expect(resolver.get(cRef)).toBe(undefined); - }); - - it('should instantiate APIs', () => { - const resolver = new ApiResolver(createRegistry()); - expect(resolver.get(aRef)).toBe(1); - expect(resolver.get(otherARef)).toBe(1); - expect(resolver.get(bRef)).toBe('b'); - expect(resolver.get(otherBRef)).toBe('b'); - expect(resolver.get(cRef)).toEqual({ x: 'x', b: 'b' }); - expect(resolver.get(cRef)).toBe(resolver.get(otherCRef)); - }); - - it('should detect self dependency cycles', () => { - const resolver = new ApiResolver(createSelfCyclicRegistry()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - }); - - it('should detect short dependency cycles', () => { - const resolver = new ApiResolver(createShortCyclicRegistry()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(bRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - }); - - it('should detect short dependency cycles with other refs', () => { - const resolver = new ApiResolver(createShortCyclicRegistryWithOther()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(bRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => resolver.get(otherARef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(otherBRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - }); - - it('should detect long dependency cycles', () => { - const resolver = new ApiResolver(createLongCyclicRegistry()); - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - // Second call for same ref should still throw - expect(() => resolver.get(aRef)).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => resolver.get(bRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => resolver.get(otherBRef)).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => resolver.get(cRef)).toThrow( - 'Circular dependency of api factory for apiRef{c}', - ); - }); - - it('should validate a factory holder', () => { - expect(() => { - ApiResolver.validateFactories(createRegistry(), [ - aRef, - bRef, - otherBRef, - cRef, - ]); - }).not.toThrow(); - }); - - it('should find self cycles with validation', () => { - const self = createSelfCyclicRegistry(); - expect(() => ApiResolver.validateFactories(self, [aRef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => ApiResolver.validateFactories(self, [otherARef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - }); - - it('should find dependency cycles with validation', () => { - const short = createShortCyclicRegistry(); - expect(() => ApiResolver.validateFactories(short, [aRef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => ApiResolver.validateFactories(short, [otherARef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => ApiResolver.validateFactories(short, [bRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => ApiResolver.validateFactories(short, [otherBRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - - const shortOther = createShortCyclicRegistryWithOther(); - expect(() => ApiResolver.validateFactories(shortOther, [aRef])).toThrow( - 'Circular dependency of api factory for apiRef{a}', - ); - expect(() => - ApiResolver.validateFactories(shortOther, [otherARef]), - ).toThrow('Circular dependency of api factory for apiRef{a}'); - expect(() => ApiResolver.validateFactories(shortOther, [bRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => - ApiResolver.validateFactories(shortOther, [otherBRef]), - ).toThrow('Circular dependency of api factory for apiRef{b}'); - - const long = createLongCyclicRegistry(); - expect(() => - ApiResolver.validateFactories(long, long.getAllApis()), - ).toThrow('Circular dependency of api factory for apiRef{a}'); - expect(() => ApiResolver.validateFactories(long, [bRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => ApiResolver.validateFactories(long, [otherBRef])).toThrow( - 'Circular dependency of api factory for apiRef{b}', - ); - expect(() => ApiResolver.validateFactories(long, [cRef])).toThrow( - 'Circular dependency of api factory for apiRef{c}', - ); - }); - - it('should only call factory func once', () => { - const registry = new ApiFactoryRegistry(); - const factory = jest.fn().mockReturnValue(2); - registry.register('default', { - api: aRef, - deps: {}, - factory, - }); - - const resolver = new ApiResolver(registry); - expect(factory).toHaveBeenCalledTimes(0); - expect(resolver.get(aRef)).toBe(2); - expect(factory).toHaveBeenCalledTimes(1); - expect(resolver.get(aRef)).toBe(2); - expect(factory).toHaveBeenCalledTimes(1); - expect(resolver.get(otherARef)).toBe(2); - expect(factory).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiResolver.ts b/packages/frontend-plugin-api/src/apis/system/ApiResolver.ts deleted file mode 100644 index 0c050ebc30..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiResolver.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 { - ApiRef, - ApiHolder, - AnyApiRef, - TypesToApiRefs, -} from '@backstage/core-plugin-api'; -import { ApiFactoryHolder } from './types'; - -/** - * Handles the actual on-demand instantiation and memoization of APIs out of - * an {@link ApiFactoryHolder}. - * - * @public - */ -export class ApiResolver implements ApiHolder { - /** - * Validate factories by making sure that each of the apis can be created - * without hitting any circular dependencies. - */ - static validateFactories( - factories: ApiFactoryHolder, - apis: Iterable, - ) { - for (const api of apis) { - const heap = [api]; - const allDeps = new Set(); - - while (heap.length) { - const apiRef = heap.shift()!; - const factory = factories.get(apiRef); - if (!factory) { - continue; - } - - for (const dep of Object.values(factory.deps)) { - if (dep.id === api.id) { - throw new Error(`Circular dependency of api factory for ${api}`); - } - if (!allDeps.has(dep)) { - allDeps.add(dep); - heap.push(dep); - } - } - } - } - } - - private readonly apis = new Map(); - - constructor(private readonly factories: ApiFactoryHolder) {} - - get(ref: ApiRef): T | undefined { - return this.load(ref); - } - - private load(ref: ApiRef, loading: AnyApiRef[] = []): T | undefined { - const impl = this.apis.get(ref.id); - if (impl) { - return impl as T; - } - - const factory = this.factories.get(ref); - if (!factory) { - return undefined; - } - - if (loading.includes(factory.api)) { - throw new Error(`Circular dependency of api factory for ${factory.api}`); - } - - const deps = this.loadDeps(ref, factory.deps, [...loading, factory.api]); - const api = factory.factory(deps); - this.apis.set(ref.id, api); - return api as T; - } - - private loadDeps( - dependent: ApiRef, - apis: TypesToApiRefs, - loading: AnyApiRef[], - ): T { - const impls = {} as T; - - for (const key in apis) { - if (apis.hasOwnProperty(key)) { - const ref = apis[key]; - - const api = this.load(ref, loading); - if (!api) { - throw new Error( - `No API factory available for dependency ${ref} of dependent ${dependent}`, - ); - } - impls[key] = api; - } - } - - return impls; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/index.ts b/packages/frontend-plugin-api/src/apis/system/index.ts index ec8984f00f..6aad24daa9 100644 --- a/packages/frontend-plugin-api/src/apis/system/index.ts +++ b/packages/frontend-plugin-api/src/apis/system/index.ts @@ -16,9 +16,4 @@ export { createApiRef } from './ApiRef'; export type { ApiRefConfig } from './ApiRef'; -export { ApiFactoryRegistry } from './ApiFactoryRegistry'; -export type { ApiFactoryScope } from './ApiFactoryRegistry'; -export { ApiProvider } from './ApiProvider'; -export type { ApiProviderProps } from './ApiProvider'; -export { ApiResolver } from './ApiResolver'; export * from './types'; From f6aa11291e9e4d4b67d690037c8bcc1455db6203 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 8 Nov 2023 00:05:36 +0100 Subject: [PATCH 08/13] frontend-plugin-api: add missing implementations Signed-off-by: Vincenzo Scamporlino --- packages/frontend-plugin-api/api-report.md | 133 ++++-------------- .../src/apis/system/ApiRef.test.ts | 50 ------- .../src/apis/system/ApiRef.ts | 54 +------ .../src/apis/system/helpers.ts | 18 +++ .../src/apis/system/index.ts | 2 + .../src/apis/system/types.ts | 78 ++-------- .../src/apis/system/useApi.tsx | 18 +++ 7 files changed, 82 insertions(+), 271 deletions(-) delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts create mode 100644 packages/frontend-plugin-api/src/apis/system/helpers.ts create mode 100644 packages/frontend-plugin-api/src/apis/system/useApi.tsx diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 4388414095..951922447d 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -8,11 +8,15 @@ import { AlertApi } from '../../../../core-plugin-api'; import { alertApiRef } from '../../../../core-plugin-api'; import { AlertMessage } from '../../../../core-plugin-api'; +import { AnyApiFactory } from '../../../../core-plugin-api'; import { AnyApiFactory as AnyApiFactory_2 } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '../../../../core-plugin-api'; import { AnyApiRef as AnyApiRef_2 } from '@backstage/core-plugin-api'; -import { ApiFactory as ApiFactory_2 } from '@backstage/core-plugin-api'; -import { ApiHolder as ApiHolder_2 } from '@backstage/core-plugin-api'; +import { ApiFactory } from '../../../../core-plugin-api'; +import { ApiHolder } from '../../../../core-plugin-api'; +import { ApiRef } from '../../../../core-plugin-api'; import { ApiRef as ApiRef_2 } from '@backstage/core-plugin-api'; +import { ApiRefConfig } from '../../../../core-plugin-api'; import { AppTheme } from '../../../../core-plugin-api'; import { AppTheme as AppTheme_2 } from '@backstage/core-plugin-api'; import { AppThemeApi } from '../../../../core-plugin-api'; @@ -28,6 +32,8 @@ import { bitbucketServerAuthApiRef } from '../../../../core-plugin-api'; import { ComponentType } from 'react'; import { ConfigApi } from '../../../../core-plugin-api'; import { configApiRef } from '../../../../core-plugin-api'; +import { createApiFactory } from '../../../../core-plugin-api'; +import { createApiRef } from '../../../../core-plugin-api'; import { DiscoveryApi } from '../../../../core-plugin-api'; import { discoveryApiRef } from '../../../../core-plugin-api'; import { ErrorApi } from '../../../../core-plugin-api'; @@ -62,7 +68,6 @@ import { OpenIdConnectApi } from '../../../../core-plugin-api'; import { PendingOAuthRequest } from '../../../../core-plugin-api'; import { ProfileInfo } from '../../../../core-plugin-api'; import { ProfileInfoApi } from '../../../../core-plugin-api'; -import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '../../../../core-plugin-api'; @@ -70,6 +75,10 @@ import { SessionState } from '../../../../core-plugin-api'; import { StorageApi } from '../../../../core-plugin-api'; import { storageApiRef } from '../../../../core-plugin-api'; import { StorageValueSnapshot } from '../../../../core-plugin-api'; +import { TypesToApiRefs } from '../../../../core-plugin-api'; +import { useApi } from '../../../../core-plugin-api'; +import { useApiHolder } from '../../../../core-plugin-api'; +import { withApis } from '../../../../core-plugin-api'; import { z } from 'zod'; import { ZodSchema } from 'zod'; import { ZodTypeDef } from 'zod'; @@ -80,17 +89,9 @@ export { alertApiRef }; export { AlertMessage }; -// @public -export type AnyApiFactory = ApiFactory< - unknown, - unknown, - { - [key in string]: unknown; - } ->; +export { AnyApiFactory }; -// @public -export type AnyApiRef = ApiRef; +export { AnyApiRef }; // @public (undocumented) export type AnyExtensionDataMap = { @@ -130,95 +131,13 @@ export type AnyRoutes = { [name in string]: RouteRef; }; -// @public -export type ApiFactory< - Api, - Impl extends Api, - Deps extends { - [name in string]: unknown; - }, -> = { - api: ApiRef; - deps: TypesToApiRefs; - factory(deps: Deps): Impl; -}; +export { ApiFactory }; -// @public (undocumented) -export type ApiFactoryHolder = { - get(api: ApiRef): - | ApiFactory< - T, - T, - { - [key in string]: unknown; - } - > - | undefined; -}; +export { ApiHolder }; -// @public -export class ApiFactoryRegistry implements ApiFactoryHolder { - // (undocumented) - get(api: ApiRef_2): - | ApiFactory_2< - T, - T, - { - [x: string]: unknown; - } - > - | undefined; - // (undocumented) - getAllApis(): Set; - register< - Api, - Impl extends Api, - Deps extends { - [name in string]: unknown; - }, - >(scope: ApiFactoryScope, factory: ApiFactory_2): boolean; -} +export { ApiRef }; -// @public -export type ApiFactoryScope = 'default' | 'app' | 'static'; - -// @public -export type ApiHolder = { - get(api: ApiRef): T | undefined; -}; - -// @public -export const ApiProvider: ( - props: PropsWithChildren, -) => React_2.JSX.Element; - -// @public -export type ApiProviderProps = { - apis: ApiHolder_2; - children: ReactNode; -}; - -// @public -export type ApiRef = { - id: string; - T: T; -}; - -// @public -export type ApiRefConfig = { - id: string; -}; - -// @public -export class ApiResolver implements ApiHolder_2 { - constructor(factories: ApiFactoryHolder); - // (undocumented) - get(ref: ApiRef_2): T | undefined; - static validateFactories( - factories: ApiFactoryHolder, - apis: Iterable, - ): void; -} +export { ApiRefConfig }; // @public export interface AppNode { @@ -371,8 +290,9 @@ export function createApiExtension< }, ): Extension; -// @public -export function createApiRef(config: ApiRefConfig): ApiRef; +export { createApiFactory }; + +export { createApiRef }; // @public (undocumented) export function createExtension< @@ -820,10 +740,11 @@ export interface SubRouteRef< readonly T: TParams; } -// @public -export type TypesToApiRefs = { - [key in keyof T]: ApiRef; -}; +export { TypesToApiRefs }; + +export { useApi }; + +export { useApiHolder }; // @public export function useRouteRef< @@ -842,4 +763,6 @@ export function useRouteRef( export function useRouteRefParams( _routeRef: RouteRef | SubRouteRef, ): Params; + +export { withApis }; ``` diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts deleted file mode 100644 index dab872236f..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { createApiRef } from './ApiRef'; - -describe('ApiRef', () => { - it('should be created', () => { - const ref = createApiRef({ id: 'abc' }); - expect(ref.id).toBe('abc'); - expect(String(ref)).toBe('apiRef{abc}'); - expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); - }); - - it('should reject invalid ids', () => { - for (const id of ['a', 'abc', 'ab-c', 'a.b.c', 'a-b.c', 'abc.a-b-c.abc3']) { - expect(createApiRef({ id }).id).toBe(id); - } - - for (const id of [ - '123', - 'ab-3', - 'ab_c', - '.', - '2ac', - 'ab.3a', - '.abc', - 'abc.', - 'ab..s', - '', - '_', - ]) { - expect(() => createApiRef({ id }).id).toThrow( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`, - ); - } - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts index adedff9f73..c60bb5f111 100644 --- a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts +++ b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts @@ -14,51 +14,9 @@ * limitations under the License. */ -import type { ApiRef } from './types'; - -/** - * API reference configuration - holds an ID of the referenced API. - * - * @public - */ -export type ApiRefConfig = { - id: string; -}; - -class ApiRefImpl implements ApiRef { - constructor(private readonly config: ApiRefConfig) { - const valid = config.id - .split('.') - .flatMap(part => part.split('-')) - .every(part => part.match(/^[a-z][a-z0-9]*$/)); - if (!valid) { - throw new Error( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`, - ); - } - } - - get id(): string { - return this.config.id; - } - - // Utility for getting type of an api, using `typeof apiRef.T` - get T(): T { - throw new Error(`tried to read ApiRef.T of ${this}`); - } - - toString() { - return `apiRef{${this.config.id}}`; - } -} - -/** - * Creates a reference to an API. - * - * @param config - The descriptor of the API to reference. - * @returns An API reference. - * @public - */ -export function createApiRef(config: ApiRefConfig): ApiRef { - return new ApiRefImpl(config); -} +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type ApiRef, + type ApiRefConfig, + createApiRef, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/system/helpers.ts b/packages/frontend-plugin-api/src/apis/system/helpers.ts new file mode 100644 index 0000000000..53dbcfb4e3 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/system/helpers.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { createApiFactory } from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/system/index.ts b/packages/frontend-plugin-api/src/apis/system/index.ts index 6aad24daa9..b1bd3ac2b3 100644 --- a/packages/frontend-plugin-api/src/apis/system/index.ts +++ b/packages/frontend-plugin-api/src/apis/system/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +export { useApi, useApiHolder, withApis } from './useApi'; export { createApiRef } from './ApiRef'; export type { ApiRefConfig } from './ApiRef'; export * from './types'; +export * from './helpers'; diff --git a/packages/frontend-plugin-api/src/apis/system/types.ts b/packages/frontend-plugin-api/src/apis/system/types.ts index 730493c079..d56674625e 100644 --- a/packages/frontend-plugin-api/src/apis/system/types.ts +++ b/packages/frontend-plugin-api/src/apis/system/types.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,70 +14,12 @@ * limitations under the License. */ -/** - * API reference. - * - * @public - */ -export type ApiRef = { - id: string; - T: T; -}; - -/** - * Catch-all {@link ApiRef} type. - * - * @public - */ -export type AnyApiRef = ApiRef; - -/** - * Wraps a type with API properties into a type holding their respective {@link ApiRef}s. - * - * @public - */ -export type TypesToApiRefs = { [key in keyof T]: ApiRef }; - -/** - * Provides lookup of APIs through their {@link ApiRef}s. - * - * @public - */ -export type ApiHolder = { - get(api: ApiRef): T | undefined; -}; - -/** - * Describes type returning API implementations. - * - * @public - */ -export type ApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown }, -> = { - api: ApiRef; - deps: TypesToApiRefs; - factory(deps: Deps): Impl; -}; - -/** - * Catch-all {@link ApiFactory} type. - * - * @public - */ -export type AnyApiFactory = ApiFactory< - unknown, - unknown, - { [key in string]: unknown } ->; - -/** - * @public - */ -export type ApiFactoryHolder = { - get( - api: ApiRef, - ): ApiFactory | undefined; -}; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export type { + ApiRef, + AnyApiRef, + TypesToApiRefs, + ApiHolder, + ApiFactory, + AnyApiFactory, +} from '../../../../core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.tsx new file mode 100644 index 0000000000..bb0f816cfc --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/system/useApi.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { useApiHolder, useApi, withApis } from '../../../../core-plugin-api'; From fb33851bc1c706b2919766566b5af8606120900b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 8 Nov 2023 00:10:50 +0100 Subject: [PATCH 09/13] fontend-app-api: fix imports Signed-off-by: Vincenzo Scamporlino --- .../frontend-app-api/src/wiring/createApp.tsx | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 59b62f3051..8495f961db 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -14,67 +14,47 @@ * limitations under the License. */ -import { Config, ConfigReader } from '@backstage/config'; -import { SidebarItem } from '@backstage/core-components'; +import React, { JSX } from 'react'; +import { ConfigReader, Config } from '@backstage/config'; +import { + AppTree, + appTreeApiRef, + BackstagePlugin, + coreExtensionData, + ExtensionDataRef, + ExtensionOverrides, + RouteRef, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { Core } from '../extensions/Core'; +import { CoreRoutes } from '../extensions/CoreRoutes'; +import { CoreLayout } from '../extensions/CoreLayout'; +import { CoreNav } from '../extensions/CoreNav'; import { AnyApiFactory, ApiHolder, AppComponents, AppContext, + appThemeApiRef, + ConfigApi, + configApiRef, + IconComponent, BackstagePlugin as LegacyBackstagePlugin, + featureFlagsApiRef, attachComponentData, + identityApiRef, + AppTheme, } from '@backstage/core-plugin-api'; -import { - appLanguageApiRef, - translationApiRef, -} from '@backstage/core-plugin-api/alpha'; +import { getAvailableFeatures } from './discovery'; import { ApiFactoryRegistry, ApiProvider, ApiResolver, - AppNode, - AppTheme, - AppTree, - BackstagePlugin, - ConfigApi, - ExtensionDataRef, - ExtensionOverrides, - IconComponent, - RouteRef, - appThemeApiRef, - appTreeApiRef, - configApiRef, - coreExtensionData, - featureFlagsApiRef, - identityApiRef, - useRouteRef, -} from '@backstage/frontend-plugin-api'; -import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; -import { JSX, default as React } from 'react'; -import { BrowserRouter, Route } from 'react-router-dom'; -import { Core } from '../extensions/Core'; -import { CoreLayout } from '../extensions/CoreLayout'; -import { CoreNav } from '../extensions/CoreNav'; -import { CoreRoutes } from '../extensions/CoreRoutes'; -import { DarkTheme, LightTheme } from '../extensions/themes'; -import { AppRouteBinder } from '../routing'; -import { RoutingProvider } from '../routing/RoutingProvider'; -import { collectRouteIds } from '../routing/collectRouteIds'; -import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; -import { resolveRouteBindings } from '../routing/resolveRouteBindings'; -import { createAppTree } from '../tree'; -import { getAvailableFeatures } from './discovery'; + AppThemeSelector, +} from '@backstage/core-app-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - apis as defaultApis, - components as defaultComponents, - icons as defaultIcons, -} from '../../../app-defaults/src/defaults'; // TODO: Get rid of all of these // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { AppThemeSelector } from '../../../core-app-api/src/apis/implementations/AppThemeApi'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppThemeProvider } from '../../../core-app-api/src/app/AppThemeProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy'; @@ -91,6 +71,26 @@ import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementati // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { + apis as defaultApis, + components as defaultComponents, + icons as defaultIcons, +} from '../../../app-defaults/src/defaults'; +import { BrowserRouter, Route } from 'react-router-dom'; +import { SidebarItem } from '@backstage/core-components'; +import { DarkTheme, LightTheme } from '../extensions/themes'; +import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; +import { + appLanguageApiRef, + translationApiRef, +} from '@backstage/core-plugin-api/alpha'; +import { AppRouteBinder } from '../routing'; +import { RoutingProvider } from '../routing/RoutingProvider'; +import { resolveRouteBindings } from '../routing/resolveRouteBindings'; +import { collectRouteIds } from '../routing/collectRouteIds'; +import { createAppTree } from '../tree'; +import { AppNode } from '@backstage/frontend-plugin-api'; const builtinExtensions = [ Core, From e3bd5ab72607c60423d5542f7cc1f8db0e8a6a75 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 8 Nov 2023 17:51:47 +0100 Subject: [PATCH 10/13] frontend-app-api: options not optionals Signed-off-by: Vincenzo Scamporlino --- packages/frontend-app-api/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 5781891549..3ca51f6331 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -5,7 +5,7 @@ ```ts import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; -import { ConfigApi } from '@backstage/frontend-plugin-api'; +import { ConfigApi } from '@backstage/core-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionOverrides } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; From 50a9e40122c76bd9c5deb8bfcfcc9daf029eba48 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 13 Nov 2023 14:20:45 +0100 Subject: [PATCH 11/13] frontend-app-api: fix circular dependency Signed-off-by: Vincenzo Scamporlino --- packages/frontend-app-api/api-report.md | 2 +- .../routing/extractRouteInfoFromAppNode.ts | 3 +- .../src/routing/toLegacyPlugin.ts | 55 +++++++++++++++++++ .../frontend-app-api/src/wiring/createApp.tsx | 38 +------------ 4 files changed, 58 insertions(+), 40 deletions(-) create mode 100644 packages/frontend-app-api/src/routing/toLegacyPlugin.ts diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 3ca51f6331..f87eaf6684 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -27,7 +27,7 @@ export type AppRouteBinder = < ) => void; // @public (undocumented) -export function createApp(options?: { +export function createApp(options: { features?: (BackstagePlugin | ExtensionOverrides)[]; configLoader?: () => Promise; bindRoutes?(context: { bind: AppRouteBinder }): void; diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts index 43955328a8..2bfd09a3e8 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts @@ -15,10 +15,9 @@ */ import { RouteRef, coreExtensionData } from '@backstage/frontend-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toLegacyPlugin } from '../wiring/createApp'; import { BackstageRouteObject } from './types'; import { AppNode } from '@backstage/frontend-plugin-api'; +import { toLegacyPlugin } from './toLegacyPlugin'; // We always add a child that matches all subroutes but without any route refs. This makes // sure that we're always able to match each route no matter how deep the navigation goes. diff --git a/packages/frontend-app-api/src/routing/toLegacyPlugin.ts b/packages/frontend-app-api/src/routing/toLegacyPlugin.ts new file mode 100644 index 0000000000..2d04d04dda --- /dev/null +++ b/packages/frontend-app-api/src/routing/toLegacyPlugin.ts @@ -0,0 +1,55 @@ +/* + * 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 { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { BackstagePlugin as LegacyBackstagePlugin } from '@backstage/core-plugin-api'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; + +// Make sure that we only convert each new plugin instance to its legacy equivalent once +const legacyPluginStore = getOrCreateGlobalSingleton( + 'legacy-plugin-compatibility-store', + () => new WeakMap(), +); + +export function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin { + let legacy = legacyPluginStore.get(plugin); + if (legacy) { + return legacy; + } + + const errorMsg = 'Not implemented in legacy plugin compatibility layer'; + const notImplemented = () => { + throw new Error(errorMsg); + }; + + legacy = { + getId(): string { + return plugin.id; + }, + get routes() { + return {}; + }, + get externalRoutes() { + return {}; + }, + getApis: notImplemented, + getFeatureFlags: notImplemented, + provide: notImplemented, + }; + + legacyPluginStore.set(plugin, legacy); + return legacy; +} diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 8495f961db..d39111eb99 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -80,7 +80,6 @@ import { BrowserRouter, Route } from 'react-router-dom'; import { SidebarItem } from '@backstage/core-components'; import { DarkTheme, LightTheme } from '../extensions/themes'; import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; -import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; import { appLanguageApiRef, translationApiRef, @@ -91,6 +90,7 @@ import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; import { createAppTree } from '../tree'; import { AppNode } from '@backstage/frontend-plugin-api'; +import { toLegacyPlugin } from '../routing/toLegacyPlugin'; const builtinExtensions = [ Core, @@ -328,42 +328,6 @@ export function createSpecializedApp(options?: { }; } -// Make sure that we only convert each new plugin instance to its legacy equivalent once -const legacyPluginStore = getOrCreateGlobalSingleton( - 'legacy-plugin-compatibility-store', - () => new WeakMap(), -); - -export function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin { - let legacy = legacyPluginStore.get(plugin); - if (legacy) { - return legacy; - } - - const errorMsg = 'Not implemented in legacy plugin compatibility layer'; - const notImplemented = () => { - throw new Error(errorMsg); - }; - - legacy = { - getId(): string { - return plugin.id; - }, - get routes() { - return {}; - }, - get externalRoutes() { - return {}; - }, - getApis: notImplemented, - getFeatureFlags: notImplemented, - provide: notImplemented, - }; - - legacyPluginStore.set(plugin, legacy); - return legacy; -} - function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { return { getPlugins(): LegacyBackstagePlugin[] { From 413dd61e16097bf57877209949fe55ccd95fee21 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 15 Nov 2023 22:57:53 +0100 Subject: [PATCH 12/13] fontend-plugin-api: absolute imports Signed-off-by: Vincenzo Scamporlino --- packages/frontend-plugin-api/api-report.md | 146 +++++++++--------- .../src/apis/definitions/AlertApi.ts | 3 +- .../src/apis/definitions/AppLanguageApi.ts | 3 +- .../src/apis/definitions/AppThemeApi.ts | 3 +- .../src/apis/definitions/ConfigApi.ts | 3 +- .../src/apis/definitions/DiscoveryApi.ts | 6 +- .../src/apis/definitions/ErrorApi.ts | 3 +- .../src/apis/definitions/FeatureFlagsApi.ts | 3 +- .../src/apis/definitions/FetchApi.ts | 3 +- .../src/apis/definitions/IdentityApi.ts | 3 +- .../src/apis/definitions/OAuthRequestApi.ts | 3 +- .../src/apis/definitions/StorageApi.ts | 3 +- .../src/apis/definitions/auth.ts | 3 +- .../src/apis/system/ApiRef.ts | 3 +- .../src/apis/system/helpers.ts | 3 +- .../src/apis/system/types.ts | 3 +- .../src/apis/system/useApi.tsx | 3 +- 17 files changed, 87 insertions(+), 110 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 951922447d..f1537fd5b7 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -5,80 +5,76 @@ ```ts /// -import { AlertApi } from '../../../../core-plugin-api'; -import { alertApiRef } from '../../../../core-plugin-api'; -import { AlertMessage } from '../../../../core-plugin-api'; -import { AnyApiFactory } from '../../../../core-plugin-api'; -import { AnyApiFactory as AnyApiFactory_2 } from '@backstage/core-plugin-api'; -import { AnyApiRef } from '../../../../core-plugin-api'; -import { AnyApiRef as AnyApiRef_2 } from '@backstage/core-plugin-api'; -import { ApiFactory } from '../../../../core-plugin-api'; -import { ApiHolder } from '../../../../core-plugin-api'; -import { ApiRef } from '../../../../core-plugin-api'; -import { ApiRef as ApiRef_2 } from '@backstage/core-plugin-api'; -import { ApiRefConfig } from '../../../../core-plugin-api'; -import { AppTheme } from '../../../../core-plugin-api'; -import { AppTheme as AppTheme_2 } from '@backstage/core-plugin-api'; -import { AppThemeApi } from '../../../../core-plugin-api'; -import { appThemeApiRef } from '../../../../core-plugin-api'; -import { atlassianAuthApiRef } from '../../../../core-plugin-api'; -import { AuthProviderInfo } from '../../../../core-plugin-api'; -import { AuthRequestOptions } from '../../../../core-plugin-api'; -import { BackstageIdentityApi } from '../../../../core-plugin-api'; -import { BackstageIdentityResponse } from '../../../../core-plugin-api'; -import { BackstageUserIdentity } from '../../../../core-plugin-api'; -import { bitbucketAuthApiRef } from '../../../../core-plugin-api'; -import { bitbucketServerAuthApiRef } from '../../../../core-plugin-api'; +import { AlertApi } from '@backstage/core-plugin-api'; +import { alertApiRef } from '@backstage/core-plugin-api'; +import { AlertMessage } from '@backstage/core-plugin-api'; +import { AnyApiFactory } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; +import { ApiFactory } from '@backstage/core-plugin-api'; +import { ApiHolder } from '@backstage/core-plugin-api'; +import { ApiRef } from '@backstage/core-plugin-api'; +import { ApiRefConfig } from '@backstage/core-plugin-api'; +import { AppTheme } from '@backstage/core-plugin-api'; +import { AppThemeApi } from '@backstage/core-plugin-api'; +import { appThemeApiRef } from '@backstage/core-plugin-api'; +import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; +import { AuthProviderInfo } from '@backstage/core-plugin-api'; +import { AuthRequestOptions } from '@backstage/core-plugin-api'; +import { BackstageIdentityApi } from '@backstage/core-plugin-api'; +import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; +import { BackstageUserIdentity } from '@backstage/core-plugin-api'; +import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; +import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; -import { ConfigApi } from '../../../../core-plugin-api'; -import { configApiRef } from '../../../../core-plugin-api'; -import { createApiFactory } from '../../../../core-plugin-api'; -import { createApiRef } from '../../../../core-plugin-api'; -import { DiscoveryApi } from '../../../../core-plugin-api'; -import { discoveryApiRef } from '../../../../core-plugin-api'; -import { ErrorApi } from '../../../../core-plugin-api'; -import { ErrorApiError } from '../../../../core-plugin-api'; -import { ErrorApiErrorContext } from '../../../../core-plugin-api'; -import { errorApiRef } from '../../../../core-plugin-api'; -import { FeatureFlag } from '../../../../core-plugin-api'; -import { FeatureFlagsApi } from '../../../../core-plugin-api'; -import { featureFlagsApiRef } from '../../../../core-plugin-api'; -import { FeatureFlagsSaveOptions } from '../../../../core-plugin-api'; -import { FeatureFlagState } from '../../../../core-plugin-api'; -import { FetchApi } from '../../../../core-plugin-api'; -import { fetchApiRef } from '../../../../core-plugin-api'; -import { githubAuthApiRef } from '../../../../core-plugin-api'; -import { gitlabAuthApiRef } from '../../../../core-plugin-api'; -import { googleAuthApiRef } from '../../../../core-plugin-api'; +import { ConfigApi } from '@backstage/core-plugin-api'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { createApiFactory } from '@backstage/core-plugin-api'; +import { createApiRef } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { discoveryApiRef } from '@backstage/core-plugin-api'; +import { ErrorApi } from '@backstage/core-plugin-api'; +import { ErrorApiError } from '@backstage/core-plugin-api'; +import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; +import { errorApiRef } from '@backstage/core-plugin-api'; +import { FeatureFlag } from '@backstage/core-plugin-api'; +import { FeatureFlagsApi } from '@backstage/core-plugin-api'; +import { featureFlagsApiRef } from '@backstage/core-plugin-api'; +import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; +import { FeatureFlagState } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { fetchApiRef } from '@backstage/core-plugin-api'; +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; +import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; -import { IdentityApi } from '../../../../core-plugin-api'; -import { identityApiRef } from '../../../../core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; +import { identityApiRef } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; -import { microsoftAuthApiRef } from '../../../../core-plugin-api'; -import { OAuthApi } from '../../../../core-plugin-api'; -import { OAuthRequestApi } from '../../../../core-plugin-api'; -import { oauthRequestApiRef } from '../../../../core-plugin-api'; -import { OAuthRequester } from '../../../../core-plugin-api'; -import { OAuthRequesterOptions } from '../../../../core-plugin-api'; -import { OAuthScope } from '../../../../core-plugin-api'; -import { oktaAuthApiRef } from '../../../../core-plugin-api'; -import { oneloginAuthApiRef } from '../../../../core-plugin-api'; -import { OpenIdConnectApi } from '../../../../core-plugin-api'; -import { PendingOAuthRequest } from '../../../../core-plugin-api'; -import { ProfileInfo } from '../../../../core-plugin-api'; -import { ProfileInfoApi } from '../../../../core-plugin-api'; +import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; +import { OAuthApi } from '@backstage/core-plugin-api'; +import { OAuthRequestApi } from '@backstage/core-plugin-api'; +import { oauthRequestApiRef } from '@backstage/core-plugin-api'; +import { OAuthRequester } from '@backstage/core-plugin-api'; +import { OAuthRequesterOptions } from '@backstage/core-plugin-api'; +import { OAuthScope } from '@backstage/core-plugin-api'; +import { oktaAuthApiRef } from '@backstage/core-plugin-api'; +import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; +import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { PendingOAuthRequest } from '@backstage/core-plugin-api'; +import { ProfileInfo } from '@backstage/core-plugin-api'; +import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; -import { SessionApi } from '../../../../core-plugin-api'; -import { SessionState } from '../../../../core-plugin-api'; -import { StorageApi } from '../../../../core-plugin-api'; -import { storageApiRef } from '../../../../core-plugin-api'; -import { StorageValueSnapshot } from '../../../../core-plugin-api'; -import { TypesToApiRefs } from '../../../../core-plugin-api'; -import { useApi } from '../../../../core-plugin-api'; -import { useApiHolder } from '../../../../core-plugin-api'; -import { withApis } from '../../../../core-plugin-api'; +import { SessionApi } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { StorageApi } from '@backstage/core-plugin-api'; +import { storageApiRef } from '@backstage/core-plugin-api'; +import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { TypesToApiRefs } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useApiHolder } from '@backstage/core-plugin-api'; +import { withApis } from '@backstage/core-plugin-api'; import { z } from 'zod'; import { ZodSchema } from 'zod'; import { ZodTypeDef } from 'zod'; @@ -203,7 +199,7 @@ export interface AppTreeApi { } // @public -export const appTreeApiRef: ApiRef_2; +export const appTreeApiRef: ApiRef; export { atlassianAuthApiRef }; @@ -262,10 +258,10 @@ export interface ConfigurableExtensionDataRef< export const coreExtensionData: { reactElement: ConfigurableExtensionDataRef; routePath: ConfigurableExtensionDataRef; - apiFactory: ConfigurableExtensionDataRef; + apiFactory: ConfigurableExtensionDataRef; routeRef: ConfigurableExtensionDataRef, {}>; navTarget: ConfigurableExtensionDataRef; - theme: ConfigurableExtensionDataRef; + theme: ConfigurableExtensionDataRef; }; // @public (undocumented) @@ -275,14 +271,14 @@ export function createApiExtension< >( options: ( | { - api: AnyApiRef_2; + api: AnyApiRef; factory: (options: { config: TConfig; inputs: Expand>; - }) => AnyApiFactory_2; + }) => AnyApiFactory; } | { - factory: AnyApiFactory_2; + factory: AnyApiFactory; } ) & { configSchema?: PortableSchema; @@ -468,7 +464,7 @@ export function createSubRouteRef< }): MakeSubRouteRef, ParentParams>; // @public (undocumented) -export function createThemeExtension(theme: AppTheme_2): Extension; +export function createThemeExtension(theme: AppTheme): Extension; export { DiscoveryApi }; diff --git a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts index eb42477f77..649a930b50 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type AlertApi, type AlertMessage, alertApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts index 020742f9e4..37ffcfa820 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type AppLanguageApi, appLanguageApiRef, -} from '../../../../core-plugin-api/src/alpha'; +} from '@backstage/core-plugin-api/alpha'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts index 0764cfa6eb..ce62cd72c3 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type AppTheme, type AppThemeApi, appThemeApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts index dc28ff2980..968a2843ba 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { type ConfigApi, configApiRef } from '../../../../core-plugin-api'; +export { type ConfigApi, configApiRef } from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts index db0edc3d20..98b3f0bbf0 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { - type DiscoveryApi, - discoveryApiRef, -} from '../../../../core-plugin-api'; +export { type DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts index 0e77faa4df..57782a05dc 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type ErrorApiError, type ErrorApiErrorContext, type ErrorApi, errorApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts index 02ff3fd9ee..820efc0a25 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type FeatureFlag, type FeatureFlagState, type FeatureFlagsSaveOptions, type FeatureFlagsApi, featureFlagsApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts index 8d090fc6cc..bcc9bc6a91 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { type FetchApi, fetchApiRef } from '../../../../core-plugin-api'; +export { type FetchApi, fetchApiRef } from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts index 72ca324f32..03503a8fba 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { type IdentityApi, identityApiRef } from '../../../../core-plugin-api'; +export { type IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 2b22625873..ff7a0f2852 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type OAuthRequesterOptions, type OAuthRequester, type PendingOAuthRequest, type OAuthRequestApi, oauthRequestApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts index 346912e9d7..2309d2414c 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type StorageValueSnapshot, type StorageApi, storageApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index 60ce40c6ad..d27aebbf47 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type BackstageIdentityApi, type BackstageIdentityResponse, @@ -37,4 +36,4 @@ export { oktaAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts index c60bb5f111..0d1f8312cb 100644 --- a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts +++ b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export { type ApiRef, type ApiRefConfig, createApiRef, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/system/helpers.ts b/packages/frontend-plugin-api/src/apis/system/helpers.ts index 53dbcfb4e3..5de265be99 100644 --- a/packages/frontend-plugin-api/src/apis/system/helpers.ts +++ b/packages/frontend-plugin-api/src/apis/system/helpers.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { createApiFactory } from '../../../../core-plugin-api'; +export { createApiFactory } from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/system/types.ts b/packages/frontend-plugin-api/src/apis/system/types.ts index d56674625e..cc44e19e3f 100644 --- a/packages/frontend-plugin-api/src/apis/system/types.ts +++ b/packages/frontend-plugin-api/src/apis/system/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports export type { ApiRef, AnyApiRef, @@ -22,4 +21,4 @@ export type { ApiHolder, ApiFactory, AnyApiFactory, -} from '../../../../core-plugin-api'; +} from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.tsx index bb0f816cfc..d2efa49fa1 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.tsx @@ -14,5 +14,4 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { useApiHolder, useApi, withApis } from '../../../../core-plugin-api'; +export { useApiHolder, useApi, withApis } from '@backstage/core-plugin-api'; From f0c97c7aea9b46923b44ddb88d61d38d975250ff Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Nov 2023 14:36:14 +0100 Subject: [PATCH 13/13] frontend-app-api: update API report Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index f87eaf6684..3ca51f6331 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -27,7 +27,7 @@ export type AppRouteBinder = < ) => void; // @public (undocumented) -export function createApp(options: { +export function createApp(options?: { features?: (BackstagePlugin | ExtensionOverrides)[]; configLoader?: () => Promise; bindRoutes?(context: { bind: AppRouteBinder }): void;