From e7640ed60932dfed1e823132cbf3df920de89921 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 22:36:48 +0000 Subject: [PATCH 01/32] fix(deps): update dependency react-hook-form to v7.46.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5e3a19ba2c..63ed701843 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37123,11 +37123,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.12.2, react-hook-form@npm:^7.13.0": - version: 7.32.2 - resolution: "react-hook-form@npm:7.32.2" + version: 7.46.1 + resolution: "react-hook-form@npm:7.46.1" peerDependencies: react: ^16.8.0 || ^17 || ^18 - checksum: abe8ba146c313d206f99a01260457bc72af03b2c73d7bd4be5af11598140ec45ef1ef37bac64b80c4dbc298d6aa21af4caa259a133f305a04ddf583faa18e7d0 + checksum: 9c11ba454ce5b2a16e7499f2ca710e0c0cff227f39689b8e7985902f27f99bfc7a2c49f145ef228528764cf8286bf6101fc8a0f5094ce652eb31cab1684518d1 languageName: node linkType: hard From bd6399242e127f18ba002d4ac94f2d9720850d1f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:35:45 +0000 Subject: [PATCH 02/32] fix(deps): update dependency classnames to v2.3.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3b0da861cf..dfe405a61c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21662,9 +21662,9 @@ __metadata: linkType: hard "classnames@npm:*, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1": - version: 2.3.1 - resolution: "classnames@npm:2.3.1" - checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960 + version: 2.3.2 + resolution: "classnames@npm:2.3.2" + checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e languageName: node linkType: hard From a4c04bf4a6666a091cfc13ef80306ad51d5e427a Mon Sep 17 00:00:00 2001 From: Jasper Boeijenga Date: Tue, 12 Sep 2023 15:05:08 +0200 Subject: [PATCH 03/32] docs: fix typo Signed-off-by: Jasper Boeijenga --- docs/local-dev/cli-build-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index eb1bda79db..b22b6dd284 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -62,7 +62,7 @@ steps: - **Bundling** - Combines a package and all of its dependencies into a production-ready bundle -These steps are generally kept isolated form each other, with each step focusing +These steps are generally kept isolated from each other, with each step focusing on its specific task. For example, we do not do linting or type checking together with the building or bundling. This is so that we can provide more flexibility and avoid duplicate work, improving performance. It is strongly From c60aee81b38ff4c43fa639cfd09b4b610b393085 Mon Sep 17 00:00:00 2001 From: Valber Junior <84424883+ValberJunior@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:54:20 -0300 Subject: [PATCH 04/32] Create veecode-github-workflows Signed-off-by: Valber Junior <84424883+ValberJunior@users.noreply.github.com> --- microsite/data/plugins/veecode-github-workflows | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 microsite/data/plugins/veecode-github-workflows diff --git a/microsite/data/plugins/veecode-github-workflows b/microsite/data/plugins/veecode-github-workflows new file mode 100644 index 0000000000..cd518dd8b8 --- /dev/null +++ b/microsite/data/plugins/veecode-github-workflows @@ -0,0 +1,15 @@ +--- +title: Veecode GitHub Workflows +author: Veecode Platform +authorUrl: https://github.com/veecode-platform +category: CI/CD +description: The Github Workflows plugin provides an alternative for manually triggering GitHub workflows from within your Backstage component. +documentation: https://github.com/veecode-platform/platform-backstage-plugins/tree/master/plugins/github-workflows +iconUrl: https://veecode-platform.github.io/support/imgs/logo_2.svg +npmPackageName: '@veecode-platform/backstage-plugin-github-workflows' +tags: + - ci + - cd + - github + - workflows +addedDate: '2023-09-12' From 316c610649730c903d7db234f9183e48ad173caf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 09:57:48 +0200 Subject: [PATCH 05/32] core-plugin-api: refactor translation ref to be opaque + fix some types Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/alpha-api-report.md | 95 +++++-------- .../src/apis/definitions/AppTranslationApi.ts | 6 +- .../src/translation/TranslationRef.test.ts | 27 ++-- .../src/translation/TranslationRef.ts | 133 +++++++++++++----- .../core-plugin-api/src/translation/index.ts | 12 +- .../core-plugin-api/src/translation/types.ts | 45 ------ .../src/translation/useTranslationRef.ts | 23 +-- 7 files changed, 170 insertions(+), 171 deletions(-) delete mode 100644 packages/core-plugin-api/src/translation/types.ts diff --git a/packages/core-plugin-api/alpha-api-report.md b/packages/core-plugin-api/alpha-api-report.md index 57cf4a2cb4..119f5c570e 100644 --- a/packages/core-plugin-api/alpha-api-report.md +++ b/packages/core-plugin-api/alpha-api-report.md @@ -11,8 +11,8 @@ import { ReactNode } from 'react'; // @alpha (undocumented) export type AppTranslationApi = { getI18n(): i18n; - addResourcesByRef>( - translationRef: TranslationRef, + addResourcesByRef>( + translationRef: TranslationRef, ): void; }; @@ -20,11 +20,14 @@ export type AppTranslationApi = { export const appTranslationApiRef: ApiRef; // @alpha (undocumented) -export const createTranslationRef: < - Messages extends Record = {}, +export function createTranslationRef< + TMessages extends { + [key in string]: string; + }, + TId extends string = string, >( - config: TranslationRefConfig, -) => TranslationRef; + config: TranslationRefOptions, +): TranslationRef; // @alpha export interface PluginOptionsProviderProps { @@ -38,75 +41,45 @@ export interface PluginOptionsProviderProps { export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element; // @alpha (undocumented) -export type TranslationOptions< - Messages extends Record = Record, -> = Messages; +export interface TranslationOptions {} // @alpha (undocumented) export interface TranslationRef< - Messages extends Record = Record, + TMessages extends { + [key in string]: string; + } = { + [key in string]: string; + }, + TId extends string = string, > { // (undocumented) - getDefaultMessages(): Messages; + $$type: '@backstage/TranslationRef'; // (undocumented) - getId(): string; + id: TId; // (undocumented) - getLazyResources(): - | Record< - string, - () => Promise<{ - messages: Messages; - }> - > - | undefined; - // (undocumented) - getResources(): Record | undefined; + T: TMessages; } // @alpha (undocumented) -export interface TranslationRefConfig< - Messages extends Record, +export interface TranslationRefOptions< + TMessages extends { + [key in string]: string; + }, + TId extends string = string, > { // (undocumented) - id: string; + id: TId; // (undocumented) lazyResources?: Record< string, () => Promise<{ - messages: Messages; + messages: TMessages; }> >; // (undocumented) - messages: Messages; + messages: TMessages; // (undocumented) - resources?: Record; -} - -// @alpha (undocumented) -export class TranslationRefImpl> - implements TranslationRef -{ - // (undocumented) - static create>( - config: TranslationRefConfig, - ): TranslationRefImpl; - // (undocumented) - getDefaultMessages(): Messages; - // (undocumented) - getId(): string; - // (undocumented) - getLazyResources(): - | Record< - string, - () => Promise<{ - messages: Messages; - }> - > - | undefined; - // (undocumented) - getResources(): Record | undefined; - // (undocumented) - toString(): string; + resources?: Record; } // @alpha @@ -116,13 +89,15 @@ export function usePluginOptions< // @alpha (undocumented) export const useTranslationRef: < - Messages extends Record, + TMessages extends { + [x: string]: string; + }, >( - translationRef: TranslationRef, -) => ( - key: Tkey, + translationRef: TranslationRef, +) => ( + key: TKey, options?: TranslationOptions, -) => Messages[Tkey]; +) => TMessages[TKey]; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts index 463aee02bf..b94e5add87 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts @@ -22,8 +22,8 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; export type AppTranslationApi = { getI18n(): i18n; - addResourcesByRef>( - translationRef: TranslationRef, + addResourcesByRef>( + translationRef: TranslationRef, ): void; }; @@ -31,5 +31,5 @@ export type AppTranslationApi = { * @alpha */ export const appTranslationApiRef: ApiRef = createApiRef({ - id: 'core.apptranslation', + id: 'core.translation', }); diff --git a/packages/core-plugin-api/src/translation/TranslationRef.test.ts b/packages/core-plugin-api/src/translation/TranslationRef.test.ts index 155624a122..41d5d9ab85 100644 --- a/packages/core-plugin-api/src/translation/TranslationRef.test.ts +++ b/packages/core-plugin-api/src/translation/TranslationRef.test.ts @@ -13,30 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TranslationRefImpl, createTranslationRef } from './TranslationRef'; +import { + createTranslationRef, + toInternalTranslationRef, +} from './TranslationRef'; describe('TranslationRefImpl', () => { - it('should create a TranslationRef instance', () => { - const config = { - id: 'testId', - messages: { key: 'value' }, - }; - - const translationRef = TranslationRefImpl.create(config); - - expect(translationRef.getId()).toBe('testId'); - expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' }); - }); - it('should create a TranslationRef instance using the factory function', () => { const config = { id: 'testId', messages: { key: 'value' }, }; - const translationRef = createTranslationRef(config); + const translationRef = toInternalTranslationRef( + createTranslationRef(config), + ); - expect(translationRef.getId()).toBe('testId'); + expect(translationRef.id).toBe('testId'); expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' }); }); @@ -49,7 +42,9 @@ describe('TranslationRefImpl', () => { }, }; - const translationRef = TranslationRefImpl.create(config); + const translationRef = toInternalTranslationRef( + createTranslationRef(config), + ); const lazyResources = translationRef.getLazyResources(); diff --git a/packages/core-plugin-api/src/translation/TranslationRef.ts b/packages/core-plugin-api/src/translation/TranslationRef.ts index d01f7ca261..2a135fe825 100644 --- a/packages/core-plugin-api/src/translation/TranslationRef.ts +++ b/packages/core-plugin-api/src/translation/TranslationRef.ts @@ -14,48 +14,109 @@ * limitations under the License. */ -import { TranslationRef, TranslationRefConfig } from './types'; - /** @alpha */ -export class TranslationRefImpl> - implements TranslationRef -{ - static create>( - config: TranslationRefConfig, - ) { - return new TranslationRefImpl(config); - } +export interface TranslationRef< + TMessages extends { [key in string]: string } = { [key in string]: string }, + TId extends string = string, +> { + $$type: '@backstage/TranslationRef'; - getId() { - return this.config.id; - } + id: TId; - getDefaultMessages(): Messages { - return this.config.messages; - } + T: TMessages; +} + +/** @internal */ +type AnyMessages = { [key in string]: string }; + +/** @internal */ +export interface InternalTranslationRef< + TMessages extends { [key in string]: string } = { [key in string]: string }, + TId extends string = string, +> extends TranslationRef { + version: 'v1'; + + getDefaultMessages(): AnyMessages; + + getResources(): Record | undefined; getLazyResources(): - | Record Promise<{ messages: Messages }>> - | undefined { - return this.config.lazyResources; - } - - getResources(): Record | undefined { - return this.config.resources; - } - - toString() { - return `TranslationRef(${this.getId()})`; - } - - private constructor( - private readonly config: TranslationRefConfig, - ) {} + | Record Promise<{ messages: AnyMessages }>> + | undefined; } /** @alpha */ -export const createTranslationRef = < - Messages extends Record = {}, +export interface TranslationRefOptions< + TMessages extends { [key in string]: string }, + TId extends string = string, +> { + id: TId; + messages: TMessages; + lazyResources?: Record Promise<{ messages: TMessages }>>; + resources?: Record; +} + +/** @internal */ +class TranslationRefImpl< + TMessages extends { [key in string]: string }, + TId extends string = string, +> implements InternalTranslationRef +{ + constructor( + private readonly options: TranslationRefOptions, + ) {} + + $$type = '@backstage/TranslationRef' as const; + + version = 'v1' as const; + + get id(): TId { + return this.options.id; + } + + get T(): never { + throw new Error('Not implemented'); + } + + getDefaultMessages(): AnyMessages { + return this.options.messages; + } + + getLazyResources(): + | Record Promise<{ messages: AnyMessages }>> + | undefined { + return this.options.lazyResources; + } + + getResources(): Record | undefined { + return this.options.resources; + } + + toString() { + return `TranslationRef{id=${this.id}}`; + } +} + +/** @alpha */ +export function createTranslationRef< + TMessages extends { [key in string]: string }, + TId extends string = string, >( - config: TranslationRefConfig, -): TranslationRef => TranslationRefImpl.create(config); + config: TranslationRefOptions, +): TranslationRef { + return new TranslationRefImpl(config); +} + +/** @internal */ +export function toInternalTranslationRef( + ref: TranslationRef, +): InternalTranslationRef { + const r = ref as InternalTranslationRef; + if (r.$$type !== '@backstage/TranslationRef') { + throw new Error(`Invalid translation ref, bad type '${r.$$type}'`); + } + if (r.version !== 'v1') { + throw new Error(`Invalid translation ref, bad version '${r.version}'`); + } + return r; +} diff --git a/packages/core-plugin-api/src/translation/index.ts b/packages/core-plugin-api/src/translation/index.ts index 9eaef57206..6c97fb772e 100644 --- a/packages/core-plugin-api/src/translation/index.ts +++ b/packages/core-plugin-api/src/translation/index.ts @@ -14,6 +14,12 @@ * limitations under the License. */ -export * from './TranslationRef'; -export * from './types'; -export * from './useTranslationRef'; +export { + type TranslationRef, + type TranslationRefOptions, + createTranslationRef, +} from './TranslationRef'; +export { + type TranslationOptions, + useTranslationRef, +} from './useTranslationRef'; diff --git a/packages/core-plugin-api/src/translation/types.ts b/packages/core-plugin-api/src/translation/types.ts deleted file mode 100644 index 4811c82eb7..0000000000 --- a/packages/core-plugin-api/src/translation/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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. - */ - -/** @alpha */ -export interface TranslationRefConfig< - Messages extends Record, -> { - id: string; - messages: Messages; - lazyResources?: Record Promise<{ messages: Messages }>>; - resources?: Record; -} - -/** @alpha */ -export interface TranslationRef< - Messages extends Record = Record, -> { - getId(): string; - - getDefaultMessages(): Messages; - - getResources(): Record | undefined; - - getLazyResources(): - | Record Promise<{ messages: Messages }>> - | undefined; -} - -/** @alpha */ -export type TranslationOptions< - Messages extends Record = Record, -> = Messages; diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/core-plugin-api/src/translation/useTranslationRef.ts index 083bcd1f0e..f88cba36a4 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.ts +++ b/packages/core-plugin-api/src/translation/useTranslationRef.ts @@ -16,26 +16,33 @@ import { useTranslation } from 'react-i18next'; -import { TranslationOptions, TranslationRef } from './types'; import { useApi } from '../apis'; import { appTranslationApiRef } from '../apis/alpha'; +import { toInternalTranslationRef, TranslationRef } from './TranslationRef'; + +/** @alpha */ +export interface TranslationOptions { + /* no options supported for now */ +} /** @alpha */ export const useTranslationRef = < - Messages extends Record, + TMessages extends { [key in string]: string }, >( - translationRef: TranslationRef, + translationRef: TranslationRef, ) => { const appTranslationApi = useApi(appTranslationApiRef); appTranslationApi.addResourcesByRef(translationRef); - const { t } = useTranslation(translationRef.getId()); + const internalRef = toInternalTranslationRef(translationRef); - const defaulteMessage = translationRef.getDefaultMessages(); + const { t } = useTranslation(internalRef.id); - return ( - key: Tkey, + const defaultMessages = internalRef.getDefaultMessages(); + + return ( + key: TKey, options?: TranslationOptions, - ): Messages[Tkey] => t(key as string, defaulteMessage[key], options); + ): TMessages[TKey] => t(key, defaultMessages[key], options); }; From 5c3d68fce2f49a32a9207d6c838b90ff9d41856f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 10:15:53 +0200 Subject: [PATCH 06/32] core-plugin-api: avoid using suspense mode of useTranslation in tests Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/src/translation/useTranslationRef.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/core-plugin-api/src/translation/useTranslationRef.ts index f88cba36a4..605a220914 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.ts +++ b/packages/core-plugin-api/src/translation/useTranslationRef.ts @@ -37,7 +37,9 @@ export const useTranslationRef = < const internalRef = toInternalTranslationRef(translationRef); - const { t } = useTranslation(internalRef.id); + const { t } = useTranslation(internalRef.id, { + useSuspense: process.env.NODE_ENV !== 'test', + }); const defaultMessages = internalRef.getDefaultMessages(); From de3904a5112a987060c795133dfb4a5180e6939c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 10:36:36 +0200 Subject: [PATCH 07/32] user-settings: cleanup test + declare translations as const Signed-off-by: Patrik Oldsberg --- plugins/user-settings/alpha-api-report.md | 33 ++++++++++--------- .../General/UserSettingsThemeToggle.test.tsx | 16 --------- plugins/user-settings/src/translation.ts | 2 +- 3 files changed, 19 insertions(+), 32 deletions(-) diff --git a/plugins/user-settings/alpha-api-report.md b/plugins/user-settings/alpha-api-report.md index 925d387882..673763745f 100644 --- a/plugins/user-settings/alpha-api-report.md +++ b/plugins/user-settings/alpha-api-report.md @@ -6,21 +6,24 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) -export const userSettingsTranslationRef: TranslationRef<{ - language: string; - change_the_language: string; - theme: string; - theme_light: string; - theme_dark: string; - theme_auto: string; - change_the_theme_mode: string; - select_theme_light: string; - select_theme_dark: string; - select_theme_auto: string; - select_theme_custom: string; - lng: string; - select_lng: string; -}>; +export const userSettingsTranslationRef: TranslationRef< + { + readonly language: 'Language'; + readonly change_the_language: 'Change the language'; + readonly theme: 'Theme'; + readonly theme_light: 'Light'; + readonly theme_dark: 'Dark'; + readonly theme_auto: 'Auto'; + readonly change_the_theme_mode: 'Change the theme mode'; + readonly select_theme_light: 'Select light'; + readonly select_theme_dark: 'Select dark'; + readonly select_theme_auto: 'Select Auto Theme'; + readonly select_theme_custom: 'Select {{custom}}'; + readonly lng: '{{language}}'; + readonly select_lng: 'Select language {{language}}'; + }, + 'user-settings' +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx index 48d16a9fde..0e07fa8c59 100644 --- a/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx @@ -15,7 +15,6 @@ */ import { AppTheme, appThemeApiRef } from '@backstage/core-plugin-api'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { renderWithEffects, TestApiRegistry, @@ -28,7 +27,6 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsThemeToggle } from './UserSettingsThemeToggle'; import { ApiProvider, AppThemeSelector } from '@backstage/core-app-api'; -import { userSettingsTranslationRef } from '../../translation'; const mockTheme: AppTheme = { id: 'light-theme', @@ -41,11 +39,6 @@ const mockTheme: AppTheme = { ), }; -jest.mock('@backstage/core-plugin-api/alpha', () => ({ - ...jest.requireActual('@backstage/core-plugin-api/alpha'), - useTranslationRef: jest.fn(), -})); - const apiRegistry = TestApiRegistry.from([ appThemeApiRef, AppThemeSelector.createWithStorage([mockTheme]), @@ -54,15 +47,6 @@ const apiRegistry = TestApiRegistry.from([ describe('', () => { it('toggles the theme select button', async () => { const themeApi = apiRegistry.get(appThemeApiRef); - // todo: general test provider - const messages: Record = - userSettingsTranslationRef.getDefaultMessages(); - - const useTranslationRefMock = jest - .fn() - .mockReturnValue((key: string) => messages[key]); - - (useTranslationRef as jest.Mock).mockImplementation(useTranslationRefMock); const rendered = await renderWithEffects( wrapInTestApp( diff --git a/plugins/user-settings/src/translation.ts b/plugins/user-settings/src/translation.ts index 920f499082..ffe4174d99 100644 --- a/plugins/user-settings/src/translation.ts +++ b/plugins/user-settings/src/translation.ts @@ -33,5 +33,5 @@ export const userSettingsTranslationRef = createTranslationRef({ select_theme_custom: 'Select {{custom}}', lng: '{{language}}', select_lng: 'Select language {{language}}', - }, + } as const, }); From a75247a722894bbfd2d17baf966eda3801bb151a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 10:37:05 +0200 Subject: [PATCH 08/32] core-app-api: updates for new translation ref type Signed-off-by: Patrik Oldsberg --- .../AppTranslationApi/AppTranslationImpl.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts index 69820e7b67..519a406628 100644 --- a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts @@ -23,6 +23,9 @@ import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import { TranslationMessages } from '../../../alpha'; +// Internal import to avoid code duplication, this will lead to duplication in build output +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/translation/TranslationRef'; /** @alpha */ export type ExperimentalI18n = { @@ -94,15 +97,16 @@ export class AppTranslationApiImpl implements AppTranslationApi { TranslationMessages> >, ) { - const resources = initResources || translationRef.getResources(); - if (!resources || this.cache.has(translationRef)) { + const internalRef = toInternalTranslationRef(translationRef); + const resources = initResources || internalRef.getResources(); + if (!resources || this.cache.has(internalRef)) { return; } - this.cache.add(translationRef); + this.cache.add(internalRef); Object.entries(resources).forEach(([language, messages]) => { this.i18n.addResourceBundle( language, - translationRef.getId(), + internalRef.id, messages, true, false, @@ -136,8 +140,9 @@ export class AppTranslationApiImpl implements AppTranslationApi { return; } - const namespace = translationRef.getId(); - const lazyResources = initResources || translationRef.getLazyResources(); + const internalRef = toInternalTranslationRef(translationRef); + const namespace = internalRef.id; + const lazyResources = initResources || internalRef.getLazyResources(); Promise.allSettled((options.supportedLngs || []).map(addLanguage)).then( results => { From 2fcadae9df42e0decb73127828f688125c3153c1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 11:19:52 +0200 Subject: [PATCH 09/32] core-plugin-api: additional translation ref refactor Signed-off-by: Patrik Oldsberg --- .../src/translation/TranslationRef.test.ts | 62 +++++++++----- .../src/translation/TranslationRef.ts | 80 ++++++++++++------- 2 files changed, 92 insertions(+), 50 deletions(-) diff --git a/packages/core-plugin-api/src/translation/TranslationRef.test.ts b/packages/core-plugin-api/src/translation/TranslationRef.test.ts index 41d5d9ab85..b73b521774 100644 --- a/packages/core-plugin-api/src/translation/TranslationRef.test.ts +++ b/packages/core-plugin-api/src/translation/TranslationRef.test.ts @@ -17,39 +17,57 @@ import { createTranslationRef, toInternalTranslationRef, } from './TranslationRef'; +import { toInternalTranslationResource } from './TranslationResource'; describe('TranslationRefImpl', () => { it('should create a TranslationRef instance using the factory function', () => { - const config = { - id: 'testId', + const ref = createTranslationRef({ + id: 'test', messages: { key: 'value' }, - }; + }); - const translationRef = toInternalTranslationRef( - createTranslationRef(config), - ); + const internalRef = toInternalTranslationRef(ref); - expect(translationRef.id).toBe('testId'); - expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' }); + expect(internalRef.$$type).toBe('@backstage/TranslationRef'); + expect(internalRef.version).toBe('v1'); + expect(internalRef.id).toBe('test'); + expect(internalRef.getDefaultMessages()).toEqual({ key: 'value' }); }); - it('should get lazy resources', async () => { - const config = { - id: 'testId', + it('should be created with lazy translations', async () => { + const ref = createTranslationRef({ + id: 'test', messages: { key: 'value' }, - lazyResources: { - en: () => Promise.resolve({ messages: { key: 'value' } }), + translations: { + de: () => Promise.resolve({ default: { key: 'other-value' } }), }, - }; + }); - const translationRef = toInternalTranslationRef( - createTranslationRef(config), + const internalRef = toInternalTranslationRef(ref); + + expect(internalRef.$$type).toBe('@backstage/TranslationRef'); + expect(internalRef.version).toBe('v1'); + expect(internalRef.id).toBe('test'); + expect(internalRef.getDefaultMessages()).toEqual({ key: 'value' }); + + const internalResource = toInternalTranslationResource( + internalRef.getDefaultResource()!, ); - - const lazyResources = translationRef.getLazyResources(); - - const messages = await lazyResources?.en(); - - expect(messages).toEqual({ messages: { key: 'value' } }); + expect(internalResource).toEqual({ + $$type: '@backstage/TranslationResource', + version: 'v1', + id: 'test', + resources: [ + { + language: 'de', + loader: expect.any(Function), + }, + ], + }); + await expect(internalResource.resources[0].loader()).resolves.toEqual({ + messages: { + key: 'other-value', + }, + }); }); }); diff --git a/packages/core-plugin-api/src/translation/TranslationRef.ts b/packages/core-plugin-api/src/translation/TranslationRef.ts index 2a135fe825..91b5d7850d 100644 --- a/packages/core-plugin-api/src/translation/TranslationRef.ts +++ b/packages/core-plugin-api/src/translation/TranslationRef.ts @@ -14,10 +14,15 @@ * limitations under the License. */ +import { + createTranslationResource, + TranslationResource, +} from './TranslationResource'; + /** @alpha */ export interface TranslationRef< - TMessages extends { [key in string]: string } = { [key in string]: string }, TId extends string = string, + TMessages extends { [key in string]: string } = { [key in string]: string }, > { $$type: '@backstage/TranslationRef'; @@ -31,47 +36,54 @@ type AnyMessages = { [key in string]: string }; /** @internal */ export interface InternalTranslationRef< - TMessages extends { [key in string]: string } = { [key in string]: string }, TId extends string = string, -> extends TranslationRef { + TMessages extends { [key in string]: string } = { [key in string]: string }, +> extends TranslationRef { version: 'v1'; getDefaultMessages(): AnyMessages; - getResources(): Record | undefined; - - getLazyResources(): - | Record Promise<{ messages: AnyMessages }>> - | undefined; + getDefaultResource(): TranslationResource | undefined; } +export type NoInfer = T extends infer S ? S : never; + /** @alpha */ export interface TranslationRefOptions< + TId extends string, TMessages extends { [key in string]: string }, - TId extends string = string, + TTranslations extends { + [language in string]: () => Promise<{ + default: { [key in keyof TMessages]: string | null }; + }>; + }, > { id: TId; messages: TMessages; - lazyResources?: Record Promise<{ messages: TMessages }>>; - resources?: Record; + translations?: TTranslations; } /** @internal */ class TranslationRefImpl< + TId extends string, TMessages extends { [key in string]: string }, - TId extends string = string, -> implements InternalTranslationRef +> implements InternalTranslationRef { - constructor( - private readonly options: TranslationRefOptions, - ) {} + #id: TId; + #messages: TMessages; + #resources: TranslationResource | undefined; + + constructor(options: TranslationRefOptions) { + this.#id = options.id; + this.#messages = options.messages; + } $$type = '@backstage/TranslationRef' as const; version = 'v1' as const; get id(): TId { - return this.options.id; + return this.#id; } get T(): never { @@ -79,17 +91,15 @@ class TranslationRefImpl< } getDefaultMessages(): AnyMessages { - return this.options.messages; + return this.#messages; } - getLazyResources(): - | Record Promise<{ messages: AnyMessages }>> - | undefined { - return this.options.lazyResources; + setDefaultResource(resources: TranslationResource): void { + this.#resources = resources; } - getResources(): Record | undefined { - return this.options.resources; + getDefaultResource(): TranslationResource | undefined { + return this.#resources; } toString() { @@ -99,12 +109,26 @@ class TranslationRefImpl< /** @alpha */ export function createTranslationRef< + TId extends string, TMessages extends { [key in string]: string }, - TId extends string = string, + TTranslations extends { + [language in string]: () => Promise<{ + default: { [key in keyof TMessages]: string | null }; + }>; + }, >( - config: TranslationRefOptions, -): TranslationRef { - return new TranslationRefImpl(config); + config: TranslationRefOptions, +): TranslationRef { + const ref = new TranslationRefImpl(config); + if (config.translations) { + ref.setDefaultResource( + createTranslationResource({ + ref, + translations: config.translations as any, + }), + ); + } + return ref; } /** @internal */ From 7d85a9da3cadd84c4cef8a4178de90d74e866efc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 11:20:30 +0200 Subject: [PATCH 10/32] core-plugin-api: add TranslationMessages Signed-off-by: Patrik Oldsberg --- .../translation/TranslationMessages.test.ts | 92 +++++++++++++++++++ .../src/translation/TranslationMessages.ts | 80 ++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 packages/core-plugin-api/src/translation/TranslationMessages.test.ts create mode 100644 packages/core-plugin-api/src/translation/TranslationMessages.ts diff --git a/packages/core-plugin-api/src/translation/TranslationMessages.test.ts b/packages/core-plugin-api/src/translation/TranslationMessages.test.ts new file mode 100644 index 0000000000..5314c8a054 --- /dev/null +++ b/packages/core-plugin-api/src/translation/TranslationMessages.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { createTranslationMessages } from './TranslationMessages'; +import { createTranslationRef } from './TranslationRef'; + +const ref = createTranslationRef({ + id: 'counting', + messages: { + one: 'one', + two: 'two', + three: 'three', + } as const, +}); + +describe('createTranslationMessages', () => { + it('should create a partial message set', () => { + expect( + createTranslationMessages({ + ref, + messages: { + one: 'uno', + }, + }), + ).toEqual({ + $$type: '@backstage/TranslationMessages', + id: 'counting', + full: false, + messages: { + one: 'uno', + }, + }); + }); + + it('should create a full message set', () => { + expect( + createTranslationMessages({ + ref, + full: true, + messages: { + one: 'uno', + two: 'dos', + three: null, + }, + }), + ).toEqual({ + $$type: '@backstage/TranslationMessages', + id: 'counting', + full: true, + messages: { + one: 'uno', + two: 'dos', + three: null, + }, + }); + }); + + it('should require all messages in a full set', () => { + expect( + createTranslationMessages({ + ref, + full: true, + // @ts-expect-error + messages: { + one: 'uno', + two: 'dos', + }, + }), + ).toEqual({ + $$type: '@backstage/TranslationMessages', + id: 'counting', + full: true, + messages: { + one: 'uno', + two: 'dos', + }, + }); + }); +}); diff --git a/packages/core-plugin-api/src/translation/TranslationMessages.ts b/packages/core-plugin-api/src/translation/TranslationMessages.ts new file mode 100644 index 0000000000..0108fb9f18 --- /dev/null +++ b/packages/core-plugin-api/src/translation/TranslationMessages.ts @@ -0,0 +1,80 @@ +/* + * 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 { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** + * Represents a collection of messages to be provided for a given translation ref. + * + * @alpha + * @remarks + * + * This collection of messages can either be used directly as an override for the + * default messages, or it can be used to provide translations for a language by + * by being referenced by a {@link TranslationResource}. + */ +export interface TranslationMessages< + TId extends string = string, + TMessages extends { [key in string]: string } = { [key in string]: string }, + TFull extends boolean = boolean, +> { + $$type: '@backstage/TranslationMessages'; + /** The ID of the translation ref that these messages are for */ + id: TId; + /** Whether or not these messages override all known messages */ + full: TFull; + /** The messages provided for the given translation ref */ + messages: TMessages; +} + +/** + * Options for {@link createTranslationMessages}. + * + * @alpha + */ +export interface TranslationMessagesOptions< + TId extends string, + TMessages extends { [key in string]: string }, + TFull extends boolean, +> { + ref: TranslationRef; + + full?: TFull; + + messages: false extends TFull + ? { [key in keyof TMessages]?: string | null } + : { [key in keyof TMessages]: string | null }; +} + +/** + * Creates a collection of messages for a given translation ref. + * + * @alpha + */ +export function createTranslationMessages< + TId extends string, + TMessages extends { [key in string]: string }, + TFull extends boolean, +>( + options: TranslationMessagesOptions, +): TranslationMessages { + return { + $$type: '@backstage/TranslationMessages', + id: options.ref.id, + full: Boolean(options.full) as TFull, + messages: options.messages as TMessages, + }; +} From a13d052a916064a1c5079cee954895bb956732db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 11:22:26 +0200 Subject: [PATCH 11/32] core-plugin-api: move TranslationResource from app-api + refactor Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/alpha.ts | 1 - .../translation/TranslationResource.test.ts | 116 ++++++++++++++++++ .../src/translation/TranslationResource.ts | 104 ++++++++++++++++ .../translation/__fixtures__/counting-de.ts | 27 ++++ .../translation/__fixtures__/counting-sv.json | 5 + .../translation/__fixtures__/fruits-de.json | 4 + .../src/translation/__fixtures__/fruits-sv.ts | 26 ++++ .../src/translation/__fixtures__/refs.ts} | 35 +++--- 8 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 packages/core-plugin-api/src/translation/TranslationResource.test.ts create mode 100644 packages/core-plugin-api/src/translation/TranslationResource.ts create mode 100644 packages/core-plugin-api/src/translation/__fixtures__/counting-de.ts create mode 100644 packages/core-plugin-api/src/translation/__fixtures__/counting-sv.json create mode 100644 packages/core-plugin-api/src/translation/__fixtures__/fruits-de.json create mode 100644 packages/core-plugin-api/src/translation/__fixtures__/fruits-sv.ts rename packages/{core-app-api/src/app/TranslationResource.ts => core-plugin-api/src/translation/__fixtures__/refs.ts} (57%) diff --git a/packages/core-app-api/src/alpha.ts b/packages/core-app-api/src/alpha.ts index 288676f509..41037a539c 100644 --- a/packages/core-app-api/src/alpha.ts +++ b/packages/core-app-api/src/alpha.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export * from './apis/implementations/AppTranslationApi'; -export * from './app/TranslationResource'; diff --git a/packages/core-plugin-api/src/translation/TranslationResource.test.ts b/packages/core-plugin-api/src/translation/TranslationResource.test.ts new file mode 100644 index 0000000000..1814311f71 --- /dev/null +++ b/packages/core-plugin-api/src/translation/TranslationResource.test.ts @@ -0,0 +1,116 @@ +/* + * 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 { + createTranslationResource, + toInternalTranslationResource, +} from './TranslationResource'; +import { countingTranslationRef } from './__fixtures__/refs'; + +describe('createTranslationResource', () => { + it('should create a simple resource', async () => { + const resource = createTranslationResource({ + ref: countingTranslationRef, + translations: { + sv: () => + Promise.resolve({ + default: { + one: 'ett', + two: 'två', + three: 'tre', + }, + }), + }, + }); + expect(toInternalTranslationResource(resource)).toEqual({ + $$type: '@backstage/TranslationResource', + version: 'v1', + id: 'counting', + resources: [ + { + language: 'sv', + loader: expect.any(Function), + }, + ], + }); + await expect( + toInternalTranslationResource(resource).resources[0].loader(), + ).resolves.toEqual({ + messages: { + one: 'ett', + two: 'två', + three: 'tre', + }, + }); + }); + + it('should create a resource with lazy loaded messages', async () => { + const resource = createTranslationResource({ + ref: countingTranslationRef, + translations: { + de: () => import('./__fixtures__/counting-de'), + sv: () => import('./__fixtures__/counting-sv.json'), + // @ts-expect-error + deBad: () => import('./__fixtures__/fruits-de.json'), + // @ts-expect-error + svBad: () => import('./__fixtures__/fruits-sv'), + }, + }); + expect(toInternalTranslationResource(resource)).toEqual({ + $$type: '@backstage/TranslationResource', + version: 'v1', + id: 'counting', + resources: [ + { + language: 'de', + loader: expect.any(Function), + }, + { + language: 'sv', + loader: expect.any(Function), + }, + { + language: 'deBad', + loader: expect.any(Function), + }, + { + language: 'svBad', + loader: expect.any(Function), + }, + ], + }); + + await expect( + toInternalTranslationResource(resource).resources[0].loader(), + ).resolves.toEqual({ + messages: { + one: 'eins', + two: 'zwei', + three: 'polizei', + }, + }); + + await expect( + toInternalTranslationResource(resource).resources[1].loader(), + ).resolves.toEqual({ + messages: { + one: 'ett', + two: 'två', + three: 'tre', + }, + }); + }); +}); diff --git a/packages/core-plugin-api/src/translation/TranslationResource.ts b/packages/core-plugin-api/src/translation/TranslationResource.ts new file mode 100644 index 0000000000..d10d1b80e3 --- /dev/null +++ b/packages/core-plugin-api/src/translation/TranslationResource.ts @@ -0,0 +1,104 @@ +/* + * 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 { + TranslationMessages, + TranslationRef, +} from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export interface TranslationResource { + $$type: '@backstage/TranslationResource'; + id: TId; +} + +/** @internal */ +export interface InternalTranslationResource + extends TranslationResource { + version: 'v1'; + resources: Array<{ + language: string; + loader(): Promise<{ messages: TranslationMessages }>; + }>; +} + +/** @internal */ +export function toInternalTranslationResource( + resource: TranslationResource, +): InternalTranslationResource { + const r = resource as InternalTranslationResource; + if (r.$$type !== '@backstage/TranslationResource') { + throw new Error(`Invalid translation resource, bad type '${r.$$type}'`); + } + if (r.version !== 'v1') { + throw new Error(`Invalid translation resource, bad version '${r.version}'`); + } + + return r; +} + +/** @alpha */ +export interface TranslationResourceOptions< + TId extends string, + TMessages extends { [key in string]: string }, + TTranslations extends { + [language in string]: () => Promise<{ + default: + | TranslationMessages + | { [key in keyof TMessages]: string | null }; + }>; + }, +> { + ref: TranslationRef; + + translations: TTranslations; +} + +/** @alpha */ +export function createTranslationResource< + TId extends string, + TMessages extends { [key in string]: string }, + TTranslations extends { + [language in string]: () => Promise<{ + default: + | TranslationMessages + | { [key in keyof TMessages]: string | null }; + }>; + }, +>( + options: TranslationResourceOptions, +): TranslationResource { + return { + $$type: '@backstage/TranslationResource', + version: 'v1', + id: options.ref.id, + resources: Object.entries(options.translations).map( + ([language, loader]) => ({ + language, + loader: () => + loader().then(m => { + const value = m.default; + return { + messages: + value?.$$type === '@backstage/TranslationMessages' + ? value.messages + : value, + }; + }), + }), + ), + } as InternalTranslationResource; +} diff --git a/packages/core-plugin-api/src/translation/__fixtures__/counting-de.ts b/packages/core-plugin-api/src/translation/__fixtures__/counting-de.ts new file mode 100644 index 0000000000..038432141d --- /dev/null +++ b/packages/core-plugin-api/src/translation/__fixtures__/counting-de.ts @@ -0,0 +1,27 @@ +/* + * 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 { createTranslationMessages } from '../TranslationMessages'; +import { countingTranslationRef } from './refs'; + +export default createTranslationMessages({ + ref: countingTranslationRef, + messages: { + one: 'eins', + two: 'zwei', + three: 'polizei', + }, +}); diff --git a/packages/core-plugin-api/src/translation/__fixtures__/counting-sv.json b/packages/core-plugin-api/src/translation/__fixtures__/counting-sv.json new file mode 100644 index 0000000000..fdfbde7bd8 --- /dev/null +++ b/packages/core-plugin-api/src/translation/__fixtures__/counting-sv.json @@ -0,0 +1,5 @@ +{ + "one": "ett", + "two": "två", + "three": "tre" +} diff --git a/packages/core-plugin-api/src/translation/__fixtures__/fruits-de.json b/packages/core-plugin-api/src/translation/__fixtures__/fruits-de.json new file mode 100644 index 0000000000..11fb3e78f6 --- /dev/null +++ b/packages/core-plugin-api/src/translation/__fixtures__/fruits-de.json @@ -0,0 +1,4 @@ +{ + "apple": "apfel", + "orange": "apfelsine" +} diff --git a/packages/core-plugin-api/src/translation/__fixtures__/fruits-sv.ts b/packages/core-plugin-api/src/translation/__fixtures__/fruits-sv.ts new file mode 100644 index 0000000000..f6c022abb4 --- /dev/null +++ b/packages/core-plugin-api/src/translation/__fixtures__/fruits-sv.ts @@ -0,0 +1,26 @@ +/* + * 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 { createTranslationMessages } from '../TranslationMessages'; +import { fruitsTranslationRef } from './refs'; + +export default createTranslationMessages({ + ref: fruitsTranslationRef, + messages: { + apple: 'äpple', + orange: 'apelsin', + }, +}); diff --git a/packages/core-app-api/src/app/TranslationResource.ts b/packages/core-plugin-api/src/translation/__fixtures__/refs.ts similarity index 57% rename from packages/core-app-api/src/app/TranslationResource.ts rename to packages/core-plugin-api/src/translation/__fixtures__/refs.ts index 65f1c4622c..45fc692c9a 100644 --- a/packages/core-app-api/src/app/TranslationResource.ts +++ b/packages/core-plugin-api/src/translation/__fixtures__/refs.ts @@ -14,21 +14,24 @@ * limitations under the License. */ -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { createTranslationRef } from '../TranslationRef'; -/** @alpha */ -export type TranslationMessages = T extends TranslationRef - ? Partial - : never; +export const countingTranslationRef = createTranslationRef({ + id: 'counting', + messages: { + one: 'one', + two: 'two', + three: 'three', + }, +}); -/** @alpha */ -export function createTranslationResource(options: { - ref: T; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ messages: TranslationMessages }> - >; -}) { - return options; -} +export const fruitsTranslationRef = createTranslationRef({ + id: 'fruits', + messages: { + apple: 'apple', + orange: 'orange', + }, + translations: { + de: () => import('./fruits-de.json'), + }, +}); From 2294b438dca1bc9248f9b636cee7db9961b1f16b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 11:23:07 +0200 Subject: [PATCH 12/32] core-plugin-api: export new translation APIs Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/src/translation/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/core-plugin-api/src/translation/index.ts b/packages/core-plugin-api/src/translation/index.ts index 6c97fb772e..a03bf4b1f7 100644 --- a/packages/core-plugin-api/src/translation/index.ts +++ b/packages/core-plugin-api/src/translation/index.ts @@ -14,6 +14,16 @@ * limitations under the License. */ +export { + type TranslationMessages, + type TranslationMessagesOptions, + createTranslationMessages, +} from './TranslationMessages'; +export { + type TranslationResource, + type TranslationResourceOptions, + createTranslationResource, +} from './TranslationResource'; export { type TranslationRef, type TranslationRefOptions, From d3c7b71929568509162370b61124e1589c67bb46 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 12:30:29 +0200 Subject: [PATCH 13/32] core-app-api: initial translation api refactor for new message/resource strucutre Signed-off-by: Patrik Oldsberg --- ...ion.test.ts => AppTranslationImpl.test.ts} | 118 ++++++------------ .../AppTranslationApi/AppTranslationImpl.ts | 115 ++++++++--------- packages/core-app-api/src/app/types.ts | 34 ++--- .../src/apis/definitions/AppTranslationApi.ts | 4 +- .../src/translation/useTranslationRef.ts | 7 +- 5 files changed, 105 insertions(+), 173 deletions(-) rename packages/core-app-api/src/apis/implementations/AppTranslationApi/{AppTranslation.test.ts => AppTranslationImpl.test.ts} (55%) diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.test.ts similarity index 55% rename from packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts rename to packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.test.ts index b6eba8a1c7..5b7304d36b 100644 --- a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { + createTranslationMessages, + createTranslationRef, + createTranslationResource, +} from '@backstage/core-plugin-api/alpha'; import { AppTranslationApiImpl } from './AppTranslationImpl'; import i18next from 'i18next'; @@ -70,112 +74,68 @@ describe('AppTranslationApiImpl', () => { }); it('should init messages correctly', () => { - const useResourcesMock = jest.spyOn( + const addMessagesMock = jest.spyOn( AppTranslationApiImpl.prototype, - 'addResources', + 'addMessages', ); - const useLazyResourcesMock = jest.spyOn( + const addLazyResourcesMock = jest.spyOn( AppTranslationApiImpl.prototype, 'addLazyResources', ); const ref = createTranslationRef({ id: 'ref-id', messages: { - key1: '', + key: '', }, }); - const options = { - supportedLanguages: ['en'], - messages: [ - { - ref, - messages: { - en: { key1: 'value1' }, - }, - lazyMessages: { - en: () => Promise.resolve({ key2: 'value2' }), - } as any, - }, - ], - }; - - const instance = AppTranslationApiImpl.create({ - supportedLanguages: ['en'], + const overrides = createTranslationMessages({ + ref, + messages: { key: 'value1' }, + }); + const resource = createTranslationResource({ + ref, + translations: { + en: () => + Promise.resolve({ + default: { + key: 'value2', + }, + }), + }, }); - instance.initMessages(options); - expect(useResourcesMock).toHaveBeenCalledWith( - options.messages[0].ref, - options.messages[0].messages, - ); - expect(useLazyResourcesMock).toHaveBeenCalledWith( - options.messages[0].ref, - options.messages[0].lazyMessages, - ); + AppTranslationApiImpl.create({ + supportedLanguages: ['en'], + resources: [overrides, resource], + }); + + expect(addMessagesMock).toHaveBeenCalledWith(overrides); + expect(addLazyResourcesMock).toHaveBeenCalledWith(resource); }); it('should useResources correctly', () => { - const i18nMock = i18next.createInstance() as any; - const useReturnMock = i18nMock.use(); - jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock); - jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock); - - const ref = createTranslationRef({ - id: 'ref-id', - messages: { - key1: 'value1', - }, - resources: { - en: { - key1: 'value2', - }, - }, - }); - - const instance = AppTranslationApiImpl.create({ - supportedLanguages: ['en'], - }); - instance.addResources(ref); - - expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'ref-id', - { key1: 'value2' }, - true, - false, + const addLazyResourcesMock = jest.spyOn( + AppTranslationApiImpl.prototype, + 'addLazyResources', ); - }); - - it('should useLazyResources correctly', () => { - const i18nMock = i18next.createInstance() as any; - const useReturnMock = i18nMock.use(); - - jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock); - jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock); const ref = createTranslationRef({ id: 'ref-id', messages: { key1: 'value1', }, - lazyResources: { - en: () => Promise.resolve({ messages: { key1: 'value2' } }), + translations: { + en: () => Promise.resolve({ default: { key1: 'value2' } }), }, }); const instance = AppTranslationApiImpl.create({ supportedLanguages: ['en'], }); - instance.addLazyResources(ref); - setTimeout(() => { - expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'ref-id', - { key1: 'value2' }, - true, - false, - ); - }); + instance.addResource(ref); + + expect(addLazyResourcesMock).toHaveBeenCalledTimes(1); + expect(addLazyResourcesMock.mock.calls[0][0].id).toBe('ref-id'); }); }); diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts index 519a406628..c55c8bd7e8 100644 --- a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts @@ -16,31 +16,39 @@ import { AppTranslationApi, + TranslationMessages, TranslationRef, + TranslationResource, } from '@backstage/core-plugin-api/alpha'; import i18next, { type i18n } from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -import { TranslationMessages } from '../../../alpha'; // Internal import to avoid code duplication, this will lead to duplication in build output // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalTranslationResource } from '../../../../../core-plugin-api/src/translation/TranslationResource'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/translation/TranslationRef'; +const DEFAULT_LANGUAGE = 'en'; + /** @alpha */ export type ExperimentalI18n = { - supportedLanguages: string[]; fallbackLanguage?: string | string[]; - messages?: Array<{ - ref: TranslationRef; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ messages: TranslationMessages }> - >; - }>; + supportedLanguages?: string[]; + resources?: Array; }; +function removeNulls( + messages: Record, +): Record { + return Object.fromEntries( + Object.entries(messages).filter( + (e): e is [string, string] => e[1] !== null, + ), + ); +} + /** @alpha */ export class AppTranslationApiImpl implements AppTranslationApi { static create(options?: ExperimentalI18n) { @@ -49,8 +57,8 @@ export class AppTranslationApiImpl implements AppTranslationApi { i18n.use(LanguageDetector); i18n.init({ - fallbackLng: options?.fallbackLanguage || 'en', - supportedLngs: options?.supportedLanguages || ['en'], + fallbackLng: options?.fallbackLanguage || DEFAULT_LANGUAGE, + supportedLngs: options?.supportedLanguages || [DEFAULT_LANGUAGE], interpolation: { escapeValue: false, }, @@ -62,70 +70,52 @@ export class AppTranslationApiImpl implements AppTranslationApi { return new AppTranslationApiImpl(i18n, options); } - private readonly cache = new WeakSet(); - private readonly lazyCache = new WeakMap>(); + private readonly cache = new Set(); + private readonly lazyCache = new Map>(); getI18n() { return this.i18n; } initMessages(options?: ExperimentalI18n) { - if (options?.messages?.length) { - options.messages.forEach(appMessage => { - if (appMessage.messages) { - this.addResources(appMessage.ref, appMessage.messages); - } - - if (appMessage.lazyMessages) { - this.addLazyResources(appMessage.ref, appMessage.lazyMessages); - } - }); + for (const resource of options?.resources || []) { + if (resource.$$type === '@backstage/TranslationResource') { + this.addLazyResources(resource); + } else if (resource.$$type === '@backstage/TranslationMessages') { + // Overrides for default messages, created with createTranslationMessages and installed via app + this.addMessages(resource); + } } } - addResourcesByRef>( - translationRef: TranslationRef, - ): void { - this.addResources(translationRef); - this.addLazyResources(translationRef); + addResource(translationRef: TranslationRef): void { + const internalRef = toInternalTranslationRef(translationRef); + const defaultResource = internalRef.getDefaultResource(); + if (defaultResource) { + this.addLazyResources(defaultResource); + } } - addResources>( - translationRef: TranslationRef, - initResources?: Record< - string, - TranslationMessages> - >, - ) { - const internalRef = toInternalTranslationRef(translationRef); - const resources = initResources || internalRef.getResources(); - if (!resources || this.cache.has(internalRef)) { + addMessages(messages: TranslationMessages) { + if (this.cache.has(messages.id)) { return; } - this.cache.add(internalRef); - Object.entries(resources).forEach(([language, messages]) => { - this.i18n.addResourceBundle( - language, - internalRef.id, - messages, - true, - false, - ); - }); + this.cache.add(messages.id); + this.i18n.addResourceBundle( + DEFAULT_LANGUAGE, + messages.id, + removeNulls(messages.messages), + true, + false, + ); } - addLazyResources>( - translationRef: TranslationRef, - initResources?: Record< - string, - () => Promise<{ messages: TranslationMessages }> - >, - ) { - let cache = this.lazyCache.get(translationRef); + addLazyResources(resource: TranslationResource) { + let cache = this.lazyCache.get(resource.id); if (!cache) { cache = new Set(); - this.lazyCache.set(translationRef, cache); + this.lazyCache.set(resource.id, cache); } const { @@ -140,9 +130,8 @@ export class AppTranslationApiImpl implements AppTranslationApi { return; } - const internalRef = toInternalTranslationRef(translationRef); - const namespace = internalRef.id; - const lazyResources = initResources || internalRef.getLazyResources(); + const internalResource = toInternalTranslationResource(resource); + const namespace = internalResource.id; Promise.allSettled((options.supportedLngs || []).map(addLanguage)).then( results => { @@ -165,7 +154,9 @@ export class AppTranslationApiImpl implements AppTranslationApi { loadBackend = reloadResources([language], [namespace]); } - const loadLazyResources = lazyResources?.[language]; + const loadLazyResources = internalResource.resources.find( + entry => entry.language === language, + )?.loader; if (!loadLazyResources) { await loadBackend; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 3d4b9cf8c5..ea51723d21 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -27,7 +27,11 @@ import { FeatureFlag, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { + TranslationMessages, + TranslationRef, + TranslationResource, +} from '@backstage/core-plugin-api/alpha'; /** * Props for the `BootErrorPage` component of {@link AppComponents}. @@ -178,16 +182,6 @@ export type AppRouteBinder = < >, ) => void; -/** - * TODO: To be remove when TranslationMessages in packages/core-app-api/src/app/TranslationResource.ts - * come to be public - * - * @ignore - * */ -type TranslationMessages = T extends TranslationRef - ? Partial - : never; - /** * The options accepted by {@link createSpecializedApp}. * @@ -290,21 +284,11 @@ export type AppOptions = { */ bindRoutes?(context: { bind: AppRouteBinder }): void; - /** - * TODO: Change to ExperimentalI18n type when packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts - * become to public - */ - __experimentalI18n?: { - supportedLanguages: string[]; + // TODO: Change to ExperimentalI18n type when packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts + __experimentalTranslations?: { fallbackLanguage?: string | string[]; - messages?: Array<{ - ref: TranslationRef; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ messages: TranslationMessages }> - >; - }>; + supportedLanguages?: string[]; + resources?: Array; }; }; diff --git a/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts index b94e5add87..44f85c0c89 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts @@ -22,9 +22,7 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; export type AppTranslationApi = { getI18n(): i18n; - addResourcesByRef>( - translationRef: TranslationRef, - ): void; + addResource(resource: TranslationRef): void; }; /** diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/core-plugin-api/src/translation/useTranslationRef.ts index 605a220914..a11ab9c25b 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.ts +++ b/packages/core-plugin-api/src/translation/useTranslationRef.ts @@ -29,13 +29,12 @@ export interface TranslationOptions { export const useTranslationRef = < TMessages extends { [key in string]: string }, >( - translationRef: TranslationRef, + translationRef: TranslationRef, ) => { - const appTranslationApi = useApi(appTranslationApiRef); - - appTranslationApi.addResourcesByRef(translationRef); + const translationApi = useApi(appTranslationApiRef); const internalRef = toInternalTranslationRef(translationRef); + translationApi.addResource(translationRef); const { t } = useTranslation(internalRef.id, { useSuspense: process.env.NODE_ENV !== 'test', From 4ba6201070154376f819a252bd2664f379ce0976 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 12:30:54 +0200 Subject: [PATCH 14/32] core-plugin-api: fix TranslationResource loader result type Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/src/translation/TranslationResource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-plugin-api/src/translation/TranslationResource.ts b/packages/core-plugin-api/src/translation/TranslationResource.ts index d10d1b80e3..50d10546cd 100644 --- a/packages/core-plugin-api/src/translation/TranslationResource.ts +++ b/packages/core-plugin-api/src/translation/TranslationResource.ts @@ -31,7 +31,7 @@ export interface InternalTranslationResource version: 'v1'; resources: Array<{ language: string; - loader(): Promise<{ messages: TranslationMessages }>; + loader(): Promise<{ messages: { [key in string]: string | null } }>; }>; } From 251c19438a4b24d50675717f37f42eba3ce2e76a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 12:43:27 +0200 Subject: [PATCH 15/32] core-app,plugin-api: update API reports + fixes Signed-off-by: Patrik Oldsberg --- packages/core-app-api/alpha-api-report.md | 63 +------- packages/core-app-api/api-report.md | 18 +-- .../core-app-api/src/app/AppManager.test.tsx | 4 +- packages/core-app-api/src/app/AppManager.tsx | 2 +- packages/core-app-api/src/app/types.ts | 1 - packages/core-plugin-api/alpha-api-report.md | 152 +++++++++++++++--- 6 files changed, 144 insertions(+), 96 deletions(-) diff --git a/packages/core-app-api/alpha-api-report.md b/packages/core-app-api/alpha-api-report.md index f35adfdbaa..9b6ffea91e 100644 --- a/packages/core-app-api/alpha-api-report.md +++ b/packages/core-app-api/alpha-api-report.md @@ -5,32 +5,18 @@ ```ts import { AppTranslationApi } from '@backstage/core-plugin-api/alpha'; import { i18n } from 'i18next'; +import { TranslationMessages } from '@backstage/core-plugin-api/alpha'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { TranslationResource } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) export class AppTranslationApiImpl implements AppTranslationApi { // (undocumented) - addLazyResources>( - translationRef: TranslationRef, - initResources?: Record< - string, - () => Promise<{ - messages: TranslationMessages; - }> - >, - ): void; + addLazyResources(resource: TranslationResource): void; // (undocumented) - addResources>( - translationRef: TranslationRef, - initResources?: Record< - string, - TranslationMessages> - >, - ): void; + addMessages(messages: TranslationMessages): void; // (undocumented) - addResourcesByRef>( - translationRef: TranslationRef, - ): void; + addResource(translationRef: TranslationRef): void; // (undocumented) static create(options?: ExperimentalI18n): AppTranslationApiImpl; // (undocumented) @@ -39,47 +25,12 @@ export class AppTranslationApiImpl implements AppTranslationApi { initMessages(options?: ExperimentalI18n): void; } -// @alpha (undocumented) -export function createTranslationResource(options: { - ref: T; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ - messages: TranslationMessages; - }> - >; -}): { - ref: T; - messages?: Record> | undefined; - lazyMessages: Record< - string, - () => Promise<{ - messages: TranslationMessages; - }> - >; -}; - // @alpha (undocumented) export type ExperimentalI18n = { - supportedLanguages: string[]; fallbackLanguage?: string | string[]; - messages?: Array<{ - ref: TranslationRef; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ - messages: TranslationMessages; - }> - >; - }>; + supportedLanguages?: string[]; + resources?: Array; }; -// @alpha (undocumented) -export type TranslationMessages = T extends TranslationRef - ? Partial - : never; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index e53d2eed83..1b5a5b4798 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -64,7 +64,8 @@ import { SessionState } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { TranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { TranslationResource } from '@backstage/core-plugin-api/alpha'; // @public export class AlertApiForwarder implements AlertApi { @@ -221,19 +222,10 @@ export type AppOptions = { themes: (Partial & Omit)[]; configLoader?: AppConfigLoader; bindRoutes?(context: { bind: AppRouteBinder }): void; - __experimentalI18n?: { - supportedLanguages: string[]; + __experimentalTranslations?: { fallbackLanguage?: string | string[]; - messages?: Array<{ - ref: TranslationRef; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ - messages: TranslationMessages; - }> - >; - }>; + supportedLanguages?: string[]; + resources?: Array; }; }; diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 1b68cfd6ca..2eddcfc3ae 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -260,7 +260,7 @@ describe('Integration Test', () => { expect(screen.getByText('extLink4: ')).toBeInTheDocument(); }); - it('runs success with __experimentalI18n', async () => { + it('runs success with __experimentalTranslations', async () => { const app = new AppManager({ apis: [noOpAnalyticsApi], defaultApis: [], @@ -275,7 +275,7 @@ describe('Integration Test', () => { extRouteRef2: plugin2RouteRef, }); }, - __experimentalI18n: { + __experimentalTranslations: { supportedLanguages: ['en'], }, }); diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 0bd495c03b..bd6a1ba3d0 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -179,7 +179,7 @@ export class AppManager implements BackstageApp { this.bindRoutes = options.bindRoutes; this.apiFactoryRegistry = new ApiFactoryRegistry(); this.appTranslationApi = AppTranslationApiImpl.create( - options.__experimentalI18n, + options.__experimentalTranslations, ); } diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index ea51723d21..0adb93cfd1 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -29,7 +29,6 @@ import { import { AppConfig } from '@backstage/config'; import { TranslationMessages, - TranslationRef, TranslationResource, } from '@backstage/core-plugin-api/alpha'; diff --git a/packages/core-plugin-api/alpha-api-report.md b/packages/core-plugin-api/alpha-api-report.md index 119f5c570e..f186c36b7f 100644 --- a/packages/core-plugin-api/alpha-api-report.md +++ b/packages/core-plugin-api/alpha-api-report.md @@ -7,27 +7,64 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { i18n } from 'i18next'; import { ReactNode } from 'react'; +import { TranslationMessages as TranslationMessages_2 } from '@backstage/core-plugin-api/alpha'; +import { TranslationRef as TranslationRef_2 } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) export type AppTranslationApi = { getI18n(): i18n; - addResourcesByRef>( - translationRef: TranslationRef, - ): void; + addResource(resource: TranslationRef): void; }; // @alpha (undocumented) export const appTranslationApiRef: ApiRef; -// @alpha (undocumented) -export function createTranslationRef< +// @alpha +export function createTranslationMessages< + TId extends string, TMessages extends { [key in string]: string; }, - TId extends string = string, + TFull extends boolean, >( - config: TranslationRefOptions, -): TranslationRef; + options: TranslationMessagesOptions, +): TranslationMessages; + +// @alpha (undocumented) +export function createTranslationRef< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TTranslations extends { + [language in string]: () => Promise<{ + default: { + [key in keyof TMessages]: string | null; + }; + }>; + }, +>( + config: TranslationRefOptions, +): TranslationRef; + +// @alpha (undocumented) +export function createTranslationResource< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TTranslations extends { + [language in string]: () => Promise<{ + default: + | TranslationMessages_2 + | { + [key in keyof TMessages]: string | null; + }; + }>; + }, +>( + options: TranslationResourceOptions, +): TranslationResource; // @alpha export interface PluginOptionsProviderProps { @@ -40,17 +77,56 @@ export interface PluginOptionsProviderProps { // @alpha export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element; -// @alpha (undocumented) -export interface TranslationOptions {} - -// @alpha (undocumented) -export interface TranslationRef< +// @alpha +export interface TranslationMessages< + TId extends string = string, TMessages extends { [key in string]: string; } = { [key in string]: string; }, + TFull extends boolean = boolean, +> { + // (undocumented) + $$type: '@backstage/TranslationMessages'; + full: TFull; + id: TId; + messages: TMessages; +} + +// @alpha +export interface TranslationMessagesOptions< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TFull extends boolean, +> { + // (undocumented) + full?: TFull; + // (undocumented) + messages: false extends TFull + ? { + [key in keyof TMessages]?: string | null; + } + : { + [key in keyof TMessages]: string | null; + }; + // (undocumented) + ref: TranslationRef_2; +} + +// @alpha (undocumented) +export interface TranslationOptions {} + +// @alpha (undocumented) +export interface TranslationRef< TId extends string = string, + TMessages extends { + [key in string]: string; + } = { + [key in string]: string; + }, > { // (undocumented) $$type: '@backstage/TranslationRef'; @@ -62,24 +138,54 @@ export interface TranslationRef< // @alpha (undocumented) export interface TranslationRefOptions< + TId extends string, TMessages extends { [key in string]: string; }, - TId extends string = string, + TTranslations extends { + [language in string]: () => Promise<{ + default: { + [key in keyof TMessages]: string | null; + }; + }>; + }, > { // (undocumented) id: TId; // (undocumented) - lazyResources?: Record< - string, - () => Promise<{ - messages: TMessages; - }> - >; - // (undocumented) messages: TMessages; // (undocumented) - resources?: Record; + translations?: TTranslations; +} + +// @alpha (undocumented) +export interface TranslationResource { + // (undocumented) + $$type: '@backstage/TranslationResource'; + // (undocumented) + id: TId; +} + +// @alpha (undocumented) +export interface TranslationResourceOptions< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TTranslations extends { + [language in string]: () => Promise<{ + default: + | TranslationMessages_2 + | { + [key in keyof TMessages]: string | null; + }; + }>; + }, +> { + // (undocumented) + ref: TranslationRef_2; + // (undocumented) + translations: TTranslations; } // @alpha @@ -93,7 +199,7 @@ export const useTranslationRef: < [x: string]: string; }, >( - translationRef: TranslationRef, + translationRef: TranslationRef, ) => ( key: TKey, options?: TranslationOptions, From 3dc4e9d8b672a0b413552bcd4f6c195e69a89bf2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 17:56:01 +0200 Subject: [PATCH 16/32] user-settings: API report update Signed-off-by: Patrik Oldsberg --- plugins/user-settings/alpha-api-report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/user-settings/alpha-api-report.md b/plugins/user-settings/alpha-api-report.md index 673763745f..4c6ac6ef39 100644 --- a/plugins/user-settings/alpha-api-report.md +++ b/plugins/user-settings/alpha-api-report.md @@ -7,6 +7,7 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) export const userSettingsTranslationRef: TranslationRef< + 'user-settings', { readonly language: 'Language'; readonly change_the_language: 'Change the language'; @@ -21,8 +22,7 @@ export const userSettingsTranslationRef: TranslationRef< readonly select_theme_custom: 'Select {{custom}}'; readonly lng: '{{language}}'; readonly select_lng: 'Select language {{language}}'; - }, - 'user-settings' + } >; // (No @packageDocumentation comment for this package) From 5a1c6a35f5d8ffcc946ddb34beb2a9cd2e1cae88 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 17:57:30 +0200 Subject: [PATCH 17/32] core-app-api: stop exporting AppTranslationApi implementation Signed-off-by: Patrik Oldsberg --- packages/core-app-api/alpha-api-report.md | 36 ----------------------- packages/core-app-api/package.json | 4 --- packages/core-app-api/src/alpha.ts | 16 ---------- 3 files changed, 56 deletions(-) delete mode 100644 packages/core-app-api/alpha-api-report.md delete mode 100644 packages/core-app-api/src/alpha.ts diff --git a/packages/core-app-api/alpha-api-report.md b/packages/core-app-api/alpha-api-report.md deleted file mode 100644 index 9b6ffea91e..0000000000 --- a/packages/core-app-api/alpha-api-report.md +++ /dev/null @@ -1,36 +0,0 @@ -## API Report File for "@backstage/core-app-api" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { AppTranslationApi } from '@backstage/core-plugin-api/alpha'; -import { i18n } from 'i18next'; -import { TranslationMessages } from '@backstage/core-plugin-api/alpha'; -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; -import { TranslationResource } from '@backstage/core-plugin-api/alpha'; - -// @alpha (undocumented) -export class AppTranslationApiImpl implements AppTranslationApi { - // (undocumented) - addLazyResources(resource: TranslationResource): void; - // (undocumented) - addMessages(messages: TranslationMessages): void; - // (undocumented) - addResource(translationRef: TranslationRef): void; - // (undocumented) - static create(options?: ExperimentalI18n): AppTranslationApiImpl; - // (undocumented) - getI18n(): i18n; - // (undocumented) - initMessages(options?: ExperimentalI18n): void; -} - -// @alpha (undocumented) -export type ExperimentalI18n = { - fallbackLanguage?: string | string[]; - supportedLanguages?: string[]; - resources?: Array; -}; - -// (No @packageDocumentation comment for this package) -``` diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 0509af9bd4..c63f64c1ce 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -7,14 +7,10 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/packages/core-app-api/src/alpha.ts b/packages/core-app-api/src/alpha.ts deleted file mode 100644 index 41037a539c..0000000000 --- a/packages/core-app-api/src/alpha.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 * from './apis/implementations/AppTranslationApi'; From 8a5b04308b331a706b22b882819f1a1b2b8a545f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 17:58:34 +0200 Subject: [PATCH 18/32] core-plugin-api: fix useTranslationRef test Signed-off-by: Patrik Oldsberg --- .../core-plugin-api/src/translation/useTranslationRef.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.test.ts b/packages/core-plugin-api/src/translation/useTranslationRef.test.ts index 372d72eb23..54e4440928 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.test.ts +++ b/packages/core-plugin-api/src/translation/useTranslationRef.test.ts @@ -51,7 +51,7 @@ describe('useTranslationRef', () => { }; (useApi as jest.Mock).mockReturnValue({ - addResourcesByRef: jest.fn(), + addResource: jest.fn(), }); (useTranslation as jest.Mock).mockReturnValue(i18nMock); From f22665e65f13f99da08bd4932b899c129c6b9bf3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 18:32:52 +0200 Subject: [PATCH 19/32] adr: update API report Signed-off-by: Patrik Oldsberg --- plugins/adr/alpha-api-report.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/adr/alpha-api-report.md b/plugins/adr/alpha-api-report.md index 1d2b3f313d..7ce9f6f055 100644 --- a/plugins/adr/alpha-api-report.md +++ b/plugins/adr/alpha-api-report.md @@ -6,11 +6,14 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) -export const adrTranslationRef: TranslationRef<{ - readonly content_header_title: 'Architecture Decision Records'; - readonly failed_to_fetch: 'Failed to fetch ADRs'; - readonly no_adrs: 'No ADRs found'; -}>; +export const adrTranslationRef: TranslationRef< + 'adr', + { + readonly content_header_title: 'Architecture Decision Records'; + readonly failed_to_fetch: 'Failed to fetch ADRs'; + readonly no_adrs: 'No ADRs found'; + } +>; // (No @packageDocumentation comment for this package) ``` From 191dfe98b04f31027cc55d7670ee64606768b362 Mon Sep 17 00:00:00 2001 From: Hitoshi Kamezaki Date: Wed, 13 Sep 2023 12:57:32 +0900 Subject: [PATCH 20/32] add cognitive-search plugin yaml for marketplace Signed-off-by: Hitoshi Kamezaki --- .../plugins/search-backend-module-cognitive-search.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 microsite/data/plugins/search-backend-module-cognitive-search.yaml diff --git a/microsite/data/plugins/search-backend-module-cognitive-search.yaml b/microsite/data/plugins/search-backend-module-cognitive-search.yaml new file mode 100644 index 0000000000..128e5d9167 --- /dev/null +++ b/microsite/data/plugins/search-backend-module-cognitive-search.yaml @@ -0,0 +1,9 @@ +--- +title: Search extension for Azure Cognitive Search +author: AP Communications +authorUrl: https://www.ap-com.co.jp/en/ +category: Search +description: Search extension plugin to create backstage search indexes into Azure Cognitive Search. +documentation: https://github.com/ap-communications/platt-backstage-plugin/tree/main/plugins/search-backend-module-cognitive-search +npmPackageName: '@platt/plugin-search-backend-module-cognitive-search' +addedDate: '2023-09-13' From b2fbeed5403b7030cf44192db8158f1110dfe1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Wed, 13 Sep 2023 16:51:08 +0200 Subject: [PATCH 21/32] Allow using FetchApi in graphiql api setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .changeset/chilled-beds-exist.md | 5 +++++ plugins/graphiql/api-report.md | 3 +++ .../graphiql/src/lib/api/GraphQLEndpoints.ts | 17 +++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .changeset/chilled-beds-exist.md diff --git a/.changeset/chilled-beds-exist.md b/.changeset/chilled-beds-exist.md new file mode 100644 index 0000000000..2023f6fb71 --- /dev/null +++ b/.changeset/chilled-beds-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-graphiql': patch +--- + +Add support for using the FetchApi diff --git a/plugins/graphiql/api-report.md b/plugins/graphiql/api-report.md index 788551476a..cbecc37006 100644 --- a/plugins/graphiql/api-report.md +++ b/plugins/graphiql/api-report.md @@ -8,6 +8,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; @@ -23,6 +24,7 @@ export type EndpointConfig = { headers?: { [name in string]: string; }; + fetchApi?: FetchApi; }; // @public (undocumented) @@ -31,6 +33,7 @@ export type GithubEndpointConfig = { title: string; url?: string; errorApi?: ErrorApi; + fetchApi?: FetchApi; githubAuthApi: OAuthApi; }; diff --git a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts index 52dc40fd81..4235d5f7cf 100644 --- a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts +++ b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts @@ -15,7 +15,7 @@ */ import { GraphQLBrowseApi, GraphQLEndpoint } from './types'; -import { ErrorApi, OAuthApi } from '@backstage/core-plugin-api'; +import { ErrorApi, FetchApi, OAuthApi } from '@backstage/core-plugin-api'; /** * Helper for generic http endpoints @@ -31,6 +31,10 @@ export type EndpointConfig = { method?: 'POST'; // Defaults to setting Content-Type to application/json headers?: { [name in string]: string }; + /** + * Fetch API to use instead of browser fetch() + */ + fetchApi?: FetchApi; }; /** @public */ @@ -45,6 +49,10 @@ export type GithubEndpointConfig = { * Errors will be posted to the ErrorApi if it is provided. */ errorApi?: ErrorApi; + /** + * Fetch API to use instead of browser fetch() + */ + fetchApi?: FetchApi; /** * GitHub Auth API used to authenticate requests. */ @@ -55,7 +63,7 @@ export type GithubEndpointConfig = { export class GraphQLEndpoints implements GraphQLBrowseApi { // Create a support static create(config: EndpointConfig): GraphQLEndpoint { - const { id, title, url, method = 'POST' } = config; + const { id, title, url, method = 'POST', fetchApi } = config; return { id, title, @@ -66,7 +74,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi { ...config.headers, ...options.headers, }; - const res = await fetch(await url, { + const res = await (fetchApi?.fetch ?? fetch)(await url, { method, headers, body, @@ -88,6 +96,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi { title, url = 'https://api.github.com/graphql', errorApi, + fetchApi, githubAuthApi, } = config; type ResponseBody = { @@ -101,7 +110,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi { let retried = false; const doRequest = async (): Promise => { - const res = await fetch(url, { + const res = await (fetchApi?.fetch ?? fetch)(url, { method: 'POST', headers: { 'Content-Type': 'application/json', From 9480c58e0254f9267122dde06f9aee54e757c1d5 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Wed, 13 Sep 2023 13:11:41 -0400 Subject: [PATCH 22/32] mock global-agent/bootstrap in tests Signed-off-by: Jamie Klassen --- packages/cli/src/commands/versions/bump.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 84019b55ee..2129bbaae9 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -20,6 +20,8 @@ import { Command } from 'commander'; import { resolve as resolvePath } from 'path'; import { paths } from '../../lib/paths'; import * as runObj from '../../lib/run'; + +jest.mock('global-agent/bootstrap', () => {}); import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; import { setupRequestMockHandlers, From 6d99b16b287a14c3122f2b6594ef2f4204a1c78a Mon Sep 17 00:00:00 2001 From: Yobannys Cabrera Gonzalez Date: Tue, 5 Sep 2023 15:29:57 -0400 Subject: [PATCH 23/32] Adding `next_run_start_at` to the merge conflict, when scheduler updates Signed-off-by: Yobannys Cabrera Gonzalez --- .changeset/real-turkeys-report.md | 5 +++++ packages/backend-tasks/src/tasks/TaskWorker.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/real-turkeys-report.md diff --git a/.changeset/real-turkeys-report.md b/.changeset/real-turkeys-report.md new file mode 100644 index 0000000000..7e13f3a937 --- /dev/null +++ b/.changeset/real-turkeys-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Fixed bug in backend TaskWorker, adding `next_run_start_at` as a merge conflict when scheduler changes diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index a03d1ec058..f1acb4ed19 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -199,7 +199,7 @@ export class TaskWorker { next_run_start_at: startAt, }) .onConflict('id') - .merge(['settings_json']); + .merge(['settings_json', 'next_run_start_at']); } /** From 8fd91547cd0b9545c2de2552f753fc9846ea01c6 Mon Sep 17 00:00:00 2001 From: Yobannys Cabrera Gonzalez Date: Tue, 12 Sep 2023 09:27:05 -0400 Subject: [PATCH 24/32] Adding the least between current and previous `next_run_start_at` to merge conflict when scheduler updates Signed-off-by: Yobannys Cabrera Gonzalez --- .changeset/dry-yaks-type.md | 5 +++ .../src/tasks/TaskWorker.test.ts | 37 +++++++++++++++++++ .../backend-tasks/src/tasks/TaskWorker.ts | 15 +++++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/dry-yaks-type.md diff --git a/.changeset/dry-yaks-type.md b/.changeset/dry-yaks-type.md new file mode 100644 index 0000000000..33d053a154 --- /dev/null +++ b/.changeset/dry-yaks-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Fixed bug in backend TaskWorker, 'next_run_start_at' will be always the least between schedule changes. diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 70be2bf415..38bb2f7e99 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -332,4 +332,41 @@ describe('TaskWorker', () => { expect(fn1.mock.calls.length).toBeGreaterThan(before); }, ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is always the min between schedule changes, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV2 = { + version: 2, + cadence: '*/15 * * * *', + initialDelayDuration: 'PT2M', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(settings); + + const settings2 = { + ...settings, + cadence: '*/2 * * * *', + initialDelayDuration: 'PT1M', + }; + await worker.persistTask(settings2); + + const row2 = (await knex(DB_TASKS_TABLE))[0]; + + const settings3 = { ...settings }; + await worker.persistTask(settings3); + + const row3 = (await knex(DB_TASKS_TABLE))[0]; + + expect(row3.next_run_start_at).toBe(row2.next_run_start_at); + }, + ); }); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index f1acb4ed19..6ede00c590 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -190,6 +190,11 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} configured to run at: ${startAt}`); + const start_at_previuos = this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .select('next_run_start_at') + .first(); + // It's OK if the task already exists; if it does, just replace its // settings with the new value and start the loop as usual. await this.knex(DB_TASKS_TABLE) @@ -199,7 +204,15 @@ export class TaskWorker { next_run_start_at: startAt, }) .onConflict('id') - .merge(['settings_json', 'next_run_start_at']); + .merge({ + settings_json: JSON.stringify(settings), + next_run_start_at: this.knex.raw('CASE WHEN ? < ? THEN ? ELSE ? END', [ + start_at_previuos, + startAt, + start_at_previuos, + startAt, + ]), + }); } /** From bf431f81c42f3e424f8bf09ea04d05c51ca82756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 14 Sep 2023 09:23:19 +0200 Subject: [PATCH 25/32] fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/dry-yaks-type.md | 5 ++- .changeset/real-turkeys-report.md | 5 --- .../src/tasks/TaskWorker.test.ts | 11 ++++-- .../backend-tasks/src/tasks/TaskWorker.ts | 39 ++++++++++++------- 4 files changed, 36 insertions(+), 24 deletions(-) delete mode 100644 .changeset/real-turkeys-report.md diff --git a/.changeset/dry-yaks-type.md b/.changeset/dry-yaks-type.md index 33d053a154..3e9cdef474 100644 --- a/.changeset/dry-yaks-type.md +++ b/.changeset/dry-yaks-type.md @@ -2,4 +2,7 @@ '@backstage/backend-tasks': patch --- -Fixed bug in backend TaskWorker, 'next_run_start_at' will be always the least between schedule changes. +When starting a task that existed before, with a faster schedule than it +previously had, the task will now correctly obey the faster schedule +immediately. Before this fix, the new schedule was only obeyed after the next +pending (according to the old schedule) run had completed. diff --git a/.changeset/real-turkeys-report.md b/.changeset/real-turkeys-report.md deleted file mode 100644 index 7e13f3a937..0000000000 --- a/.changeset/real-turkeys-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-tasks': patch ---- - -Fixed bug in backend TaskWorker, adding `next_run_start_at` as a merge conflict when scheduler changes diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 38bb2f7e99..71d02274e2 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -330,6 +330,8 @@ describe('TaskWorker', () => { const before = fn1.mock.calls.length; await promise2; expect(fn1.mock.calls.length).toBeGreaterThan(before); + + await knex.destroy(); }, ); @@ -351,6 +353,7 @@ describe('TaskWorker', () => { const worker = new TaskWorker('task99', fn, knex, logger); await worker.persistTask(settings); + const row1 = (await knex(DB_TASKS_TABLE))[0]; const settings2 = { ...settings, @@ -358,15 +361,17 @@ describe('TaskWorker', () => { initialDelayDuration: 'PT1M', }; await worker.persistTask(settings2); - const row2 = (await knex(DB_TASKS_TABLE))[0]; + expect(row2.next_run_start_at).not.toStrictEqual(row1.next_run_start_at); + const settings3 = { ...settings }; await worker.persistTask(settings3); - const row3 = (await knex(DB_TASKS_TABLE))[0]; - expect(row3.next_run_start_at).toBe(row2.next_run_start_at); + expect(row3.next_run_start_at).toStrictEqual(row2.next_run_start_at); + + await knex.destroy(); }, ); }); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 6ede00c590..c1007b4964 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -190,29 +190,38 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} configured to run at: ${startAt}`); - const start_at_previuos = this.knex(DB_TASKS_TABLE) - .where('id', '=', this.taskId) - .select('next_run_start_at') - .first(); - // It's OK if the task already exists; if it does, just replace its // settings with the new value and start the loop as usual. + const settingsJson = JSON.stringify(settings); await this.knex(DB_TASKS_TABLE) .insert({ id: this.taskId, - settings_json: JSON.stringify(settings), + settings_json: settingsJson, next_run_start_at: startAt, }) .onConflict('id') - .merge({ - settings_json: JSON.stringify(settings), - next_run_start_at: this.knex.raw('CASE WHEN ? < ? THEN ? ELSE ? END', [ - start_at_previuos, - startAt, - start_at_previuos, - startAt, - ]), - }); + .merge( + this.knex.client.config.client.includes('mysql') + ? { + settings_json: settingsJson, + next_run_start_at: this.knex.raw( + `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`, + [startAt, 'next_run_start_at', startAt, 'next_run_start_at'], + ), + } + : { + settings_json: this.knex.ref('excluded.settings_json'), + next_run_start_at: this.knex.raw( + `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`, + [ + 'excluded.next_run_start_at', + `${DB_TASKS_TABLE}.next_run_start_at`, + 'excluded.next_run_start_at', + `${DB_TASKS_TABLE}.next_run_start_at`, + ], + ), + }, + ); } /** From e68002b7cb8214ae9ded26d49d49d0cec8fca61e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Sep 2023 11:02:11 +0200 Subject: [PATCH 26/32] core-plugin-api: remove stray NoInfer Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/src/translation/TranslationRef.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core-plugin-api/src/translation/TranslationRef.ts b/packages/core-plugin-api/src/translation/TranslationRef.ts index 91b5d7850d..748473f92d 100644 --- a/packages/core-plugin-api/src/translation/TranslationRef.ts +++ b/packages/core-plugin-api/src/translation/TranslationRef.ts @@ -46,8 +46,6 @@ export interface InternalTranslationRef< getDefaultResource(): TranslationResource | undefined; } -export type NoInfer = T extends infer S ? S : never; - /** @alpha */ export interface TranslationRefOptions< TId extends string, From 6f8f78918c8cd5e54a6775ae5976b68c85f375f1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Sep 2023 11:36:43 +0200 Subject: [PATCH 27/32] changeset: exit prerelease Signed-off-by: Patrik Oldsberg --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 6c1673b0be..d824680e8f 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.86", From c8029af89c50207a6c5bcfadd001734ae24f3807 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Sep 2023 11:49:35 +0200 Subject: [PATCH 28/32] kubernetes-backend: avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../src/service/KubernetesProxy.test.ts | 16 ++++------------ .../src/service/__fixtures__/mock-ca.crt | 1 + 2 files changed, 5 insertions(+), 12 deletions(-) create mode 100644 plugins/kubernetes-backend/src/service/__fixtures__/mock-ca.crt diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index aaadbfae0e..6c8953d0a3 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import 'buffer'; +import 'buffer'; +import { resolve as resolvePath } from 'path'; import { errorHandler, getVoidLogger } from '@backstage/backend-common'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { NotFoundError } from '@backstage/errors'; @@ -44,7 +45,6 @@ import { KubernetesProxy, } from './KubernetesProxy'; import fetch from 'cross-fetch'; -import mockFs from 'mock-fs'; import type { Request } from 'express'; @@ -680,15 +680,7 @@ describe('KubernetesProxy', () => { httpsRequest.mockClear(); }); describe('should pass the exact response from Kubernetes using the CA file', () => { - afterEach(() => { - mockFs.restore(); - }); - it('should trust contents of specified caFile', async () => { - mockFs({ - '/path/to/ca.crt': 'MOCKCA', - }); - const apiResponse = { kind: 'APIVersions', versions: ['v1'], @@ -706,7 +698,7 @@ describe('KubernetesProxy', () => { url: 'https://localhost:9999', serviceAccountToken: '', authProvider: 'serviceAccount', - caFile: '/path/to/ca.crt', + caFile: resolvePath(__dirname, '__fixtures__/mock-ca.crt'), }, ] as ClusterDetails[]); @@ -736,7 +728,7 @@ describe('KubernetesProxy', () => { expect(httpsRequest).toHaveBeenCalledTimes(1); const [[{ ca }]] = httpsRequest.mock.calls; - expect(ca).toEqual('MOCKCA'); + expect(ca).toMatch('MOCKCA'); }); }); }); diff --git a/plugins/kubernetes-backend/src/service/__fixtures__/mock-ca.crt b/plugins/kubernetes-backend/src/service/__fixtures__/mock-ca.crt new file mode 100644 index 0000000000..96cc8f4b38 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/__fixtures__/mock-ca.crt @@ -0,0 +1 @@ +MOCKCA From c3c1fd3a17653b677082193df268bbdf3194912c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 14 Sep 2023 13:33:21 +0200 Subject: [PATCH 29/32] fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/old-windows-argue.md | 5 +++++ plugins/catalog-import/api-report.md | 2 +- .../StepPrepareCreatePullRequest/PreparePullRequestForm.tsx | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changeset/old-windows-argue.md diff --git a/.changeset/old-windows-argue.md b/.changeset/old-windows-argue.md new file mode 100644 index 0000000000..f687d1b08c --- /dev/null +++ b/.changeset/old-windows-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': minor +--- + +Slight change to the `PreparePullRequestFormProps`, because of an update to `react-hook-form`. diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index f507967d37..7397bfad1d 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -238,7 +238,7 @@ export type PreparePullRequestFormProps< UseFormReturn, 'formState' | 'register' | 'control' | 'setValue' > & { - values: UnpackNestedValue; + values: TFieldValues; }, ) => React_2.ReactNode; }; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx index 079ed57022..f0d84f13a4 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { FormProvider, SubmitHandler, - UnpackNestedValue, useForm, UseFormProps, UseFormReturn, @@ -39,7 +38,7 @@ export type PreparePullRequestFormProps< UseFormReturn, 'formState' | 'register' | 'control' | 'setValue' > & { - values: UnpackNestedValue; + values: TFieldValues; }, ) => React.ReactNode; }; From 117027636b1045f6267e3f94c950752a2fd0d5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 14 Sep 2023 13:57:03 +0200 Subject: [PATCH 30/32] fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/nine-taxis-study.md | 5 +++++ packages/core-components/src/layout/Sidebar/Items.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/nine-taxis-study.md diff --git a/.changeset/nine-taxis-study.md b/.changeset/nine-taxis-study.md new file mode 100644 index 0000000000..693534c2be --- /dev/null +++ b/.changeset/nine-taxis-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Minor internal tweak to handle `classnames` update diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 715e0d4af7..c40d45f8b4 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -351,7 +351,7 @@ export const WorkaroundNavLink = React.forwardRef< aria-current={ariaCurrent} style={{ ...style, ...(isActive ? activeStyle : undefined) }} className={classnames([ - className, + typeof className !== 'function' ? className : undefined, isActive ? activeClassName : undefined, ])} /> From d8c61e81da283344b251b6292934941191d8adeb Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Thu, 14 Sep 2023 09:30:16 -0400 Subject: [PATCH 31/32] explanatory comment and move the module mock to the same section as the other module mocks. Signed-off-by: Jamie Klassen --- packages/cli/src/commands/versions/bump.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 2129bbaae9..7af6c6160b 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -20,8 +20,6 @@ import { Command } from 'commander'; import { resolve as resolvePath } from 'path'; import { paths } from '../../lib/paths'; import * as runObj from '../../lib/run'; - -jest.mock('global-agent/bootstrap', () => {}); import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; import { setupRequestMockHandlers, @@ -33,6 +31,9 @@ import { rest } from 'msw'; import { NotFoundError } from '@backstage/errors'; import { Lockfile } from '../../lib/versioning/Lockfile'; +// Avoid mutating the global http(s) agent used in other tests +jest.mock('global-agent/bootstrap', () => {}); + // Remove log coloring to simplify log matching jest.mock('chalk', () => ({ red: (str: string) => str, From 01c4b9e92a6a6b3baaad1b280ee45604b3838ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Thu, 14 Sep 2023 16:36:33 +0200 Subject: [PATCH 32/32] Use the FetchApi for GraphiQL endpoints in the example app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- packages/app/src/apis.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index f348d24466..f2fda9882b 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -33,6 +33,7 @@ import { createApiFactory, discoveryApiRef, errorApiRef, + fetchApiRef, githubAuthApiRef, } from '@backstage/core-plugin-api'; import { AuthProxyDiscoveryApi } from './AuthProxyDiscoveryApi'; @@ -53,18 +54,24 @@ export const apis: AnyApiFactory[] = [ createApiFactory({ api: graphQlBrowseApiRef, - deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef }, - factory: ({ errorApi, githubAuthApi }) => + deps: { + errorApi: errorApiRef, + fetchApi: fetchApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ errorApi, fetchApi, githubAuthApi }) => GraphQLEndpoints.from([ GraphQLEndpoints.create({ id: 'gitlab', title: 'GitLab', url: 'https://gitlab.com/api/graphql', + fetchApi, }), GraphQLEndpoints.github({ id: 'github', title: 'GitHub', errorApi, + fetchApi, githubAuthApi, }), ]),