Merge branch 'master' of https://github.com/backstage/backstage into feat/allow-inspect-override

Signed-off-by: jrwpatterson <jrwpatterson@gmail.com>
This commit is contained in:
jrwpatterson
2023-09-15 06:55:37 +10:00
49 changed files with 1094 additions and 543 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-graphiql': patch
---
Add support for using the FetchApi
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/backend-tasks': patch
---
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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Minor internal tweak to handle `classnames` update
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-import': minor
---
Slight change to the `PreparePullRequestFormProps`, because of an update to `react-hook-form`.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"mode": "pre",
"mode": "exit",
"tag": "next",
"initialVersions": {
"example-app": "0.2.86",
+1 -1
View File
@@ -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
@@ -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'
@@ -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'
+9 -2
View File
@@ -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,
}),
]),
@@ -330,6 +330,48 @@ describe('TaskWorker', () => {
const before = fn1.mock.calls.length;
await promise2;
expect(fn1.mock.calls.length).toBeGreaterThan(before);
await knex.destroy();
},
);
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<void>(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 row1 = (await knex<DbTasksRow>(DB_TASKS_TABLE))[0];
const settings2 = {
...settings,
cadence: '*/2 * * * *',
initialDelayDuration: 'PT1M',
};
await worker.persistTask(settings2);
const row2 = (await knex<DbTasksRow>(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<DbTasksRow>(DB_TASKS_TABLE))[0];
expect(row3.next_run_start_at).toStrictEqual(row2.next_run_start_at);
await knex.destroy();
},
);
});
+24 -2
View File
@@ -192,14 +192,36 @@ export class TaskWorker {
// 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<DbTasksRow>(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']);
.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`,
],
),
},
);
}
/**
@@ -31,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,
-85
View File
@@ -1,85 +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 { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
export class AppTranslationApiImpl implements AppTranslationApi {
// (undocumented)
addLazyResources<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
initResources?: Record<
string,
() => Promise<{
messages: TranslationMessages<TranslationRef>;
}>
>,
): void;
// (undocumented)
addResources<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
initResources?: Record<
string,
TranslationMessages<TranslationRef<Messages>>
>,
): void;
// (undocumented)
addResourcesByRef<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
): void;
// (undocumented)
static create(options?: ExperimentalI18n): AppTranslationApiImpl;
// (undocumented)
getI18n(): i18n;
// (undocumented)
initMessages(options?: ExperimentalI18n): void;
}
// @alpha (undocumented)
export function createTranslationResource<T extends TranslationRef>(options: {
ref: T;
messages?: Record<string, TranslationMessages<T>>;
lazyMessages: Record<
string,
() => Promise<{
messages: TranslationMessages<T>;
}>
>;
}): {
ref: T;
messages?: Record<string, TranslationMessages<T>> | undefined;
lazyMessages: Record<
string,
() => Promise<{
messages: TranslationMessages<T>;
}>
>;
};
// @alpha (undocumented)
export type ExperimentalI18n = {
supportedLanguages: string[];
fallbackLanguage?: string | string[];
messages?: Array<{
ref: TranslationRef;
messages?: Record<string, TranslationMessages<TranslationRef>>;
lazyMessages: Record<
string,
() => Promise<{
messages: TranslationMessages<TranslationRef>;
}>
>;
}>;
};
// @alpha (undocumented)
export type TranslationMessages<T> = T extends TranslationRef<infer R>
? Partial<R>
: never;
// (No @packageDocumentation comment for this package)
```
+5 -13
View File
@@ -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<AppTheme> & Omit<AppTheme, 'theme'>)[];
configLoader?: AppConfigLoader;
bindRoutes?(context: { bind: AppRouteBinder }): void;
__experimentalI18n?: {
supportedLanguages: string[];
__experimentalTranslations?: {
fallbackLanguage?: string | string[];
messages?: Array<{
ref: TranslationRef;
messages?: Record<string, TranslationMessages<TranslationRef>>;
lazyMessages: Record<
string,
() => Promise<{
messages: TranslationMessages<TranslationRef>;
}>
>;
}>;
supportedLanguages?: string[];
resources?: Array<TranslationMessages | TranslationResource>;
};
};
-4
View File
@@ -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"
]
@@ -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');
});
});
@@ -16,28 +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<string, TranslationMessages<TranslationRef>>;
lazyMessages: Record<
string,
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
>;
}>;
supportedLanguages?: string[];
resources?: Array<TranslationMessages | TranslationResource>;
};
function removeNulls(
messages: Record<string, string | null>,
): Record<string, string> {
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) {
@@ -46,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,
},
@@ -59,69 +70,52 @@ export class AppTranslationApiImpl implements AppTranslationApi {
return new AppTranslationApiImpl(i18n, options);
}
private readonly cache = new WeakSet<TranslationRef>();
private readonly lazyCache = new WeakMap<TranslationRef, Set<string>>();
private readonly cache = new Set<string>();
private readonly lazyCache = new Map<string, Set<string>>();
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<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
): 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<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
initResources?: Record<
string,
TranslationMessages<TranslationRef<Messages>>
>,
) {
const resources = initResources || translationRef.getResources();
if (!resources || this.cache.has(translationRef)) {
addMessages(messages: TranslationMessages) {
if (this.cache.has(messages.id)) {
return;
}
this.cache.add(translationRef);
Object.entries(resources).forEach(([language, messages]) => {
this.i18n.addResourceBundle(
language,
translationRef.getId(),
messages,
true,
false,
);
});
this.cache.add(messages.id);
this.i18n.addResourceBundle(
DEFAULT_LANGUAGE,
messages.id,
removeNulls(messages.messages),
true,
false,
);
}
addLazyResources<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
initResources?: Record<
string,
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
>,
) {
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 {
@@ -136,8 +130,8 @@ export class AppTranslationApiImpl implements AppTranslationApi {
return;
}
const namespace = translationRef.getId();
const lazyResources = initResources || translationRef.getLazyResources();
const internalResource = toInternalTranslationResource(resource);
const namespace = internalResource.id;
Promise.allSettled((options.supportedLngs || []).map(addLanguage)).then(
results => {
@@ -160,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;
@@ -260,7 +260,7 @@ describe('Integration Test', () => {
expect(screen.getByText('extLink4: <none>')).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'],
},
});
+1 -1
View File
@@ -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,
);
}
+8 -25
View File
@@ -27,7 +27,10 @@ import {
FeatureFlag,
} from '@backstage/core-plugin-api';
import { AppConfig } from '@backstage/config';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
import {
TranslationMessages,
TranslationResource,
} from '@backstage/core-plugin-api/alpha';
/**
* Props for the `BootErrorPage` component of {@link AppComponents}.
@@ -178,16 +181,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> = T extends TranslationRef<infer R>
? Partial<R>
: never;
/**
* The options accepted by {@link createSpecializedApp}.
*
@@ -290,21 +283,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<string, TranslationMessages<TranslationRef>>;
lazyMessages: Record<
string,
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
>;
}>;
supportedLanguages?: string[];
resources?: Array<TranslationMessages | TranslationResource>;
};
};
@@ -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,
])}
/>
+142 -61
View File
@@ -7,24 +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<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
): void;
addResource(resource: TranslationRef): void;
};
// @alpha (undocumented)
export const appTranslationApiRef: ApiRef<AppTranslationApi>;
// @alpha (undocumented)
export const createTranslationRef: <
Messages extends Record<keyof Messages, string> = {},
// @alpha
export function createTranslationMessages<
TId extends string,
TMessages extends {
[key in string]: string;
},
TFull extends boolean,
>(
config: TranslationRefConfig<Messages>,
) => TranslationRef<Messages>;
options: TranslationMessagesOptions<TId, TMessages, TFull>,
): TranslationMessages<TId, TMessages, TFull>;
// @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<TId, TMessages, TTranslations>,
): TranslationRef<TId, TMessages>;
// @alpha (undocumented)
export function createTranslationResource<
TId extends string,
TMessages extends {
[key in string]: string;
},
TTranslations extends {
[language in string]: () => Promise<{
default:
| TranslationMessages_2<TId>
| {
[key in keyof TMessages]: string | null;
};
}>;
},
>(
options: TranslationResourceOptions<TId, TMessages, TTranslations>,
): TranslationResource<TId>;
// @alpha
export interface PluginOptionsProviderProps {
@@ -37,76 +77,115 @@ export interface PluginOptionsProviderProps {
// @alpha
export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element;
// @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<TId, TMessages>;
}
// @alpha (undocumented)
export type TranslationOptions<
Messages extends Record<keyof Messages, string> = Record<string, string>,
> = Messages;
export interface TranslationOptions {}
// @alpha (undocumented)
export interface TranslationRef<
Messages extends Record<keyof Messages, string> = Record<string, string>,
TId extends string = string,
TMessages extends {
[key in string]: string;
} = {
[key in string]: string;
},
> {
// (undocumented)
getDefaultMessages(): Messages;
$$type: '@backstage/TranslationRef';
// (undocumented)
getId(): string;
id: TId;
// (undocumented)
getLazyResources():
| Record<
string,
() => Promise<{
messages: Messages;
}>
>
| undefined;
// (undocumented)
getResources(): Record<string, Messages> | undefined;
T: TMessages;
}
// @alpha (undocumented)
export interface TranslationRefConfig<
Messages extends Record<keyof Messages, string>,
export interface TranslationRefOptions<
TId extends string,
TMessages extends {
[key in string]: string;
},
TTranslations extends {
[language in string]: () => Promise<{
default: {
[key in keyof TMessages]: string | null;
};
}>;
},
> {
// (undocumented)
id: string;
id: TId;
// (undocumented)
lazyResources?: Record<
string,
() => Promise<{
messages: Messages;
}>
>;
messages: TMessages;
// (undocumented)
messages: Messages;
// (undocumented)
resources?: Record<string, Messages>;
translations?: TTranslations;
}
// @alpha (undocumented)
export class TranslationRefImpl<Messages extends Record<keyof Messages, string>>
implements TranslationRef<Messages>
{
export interface TranslationResource<TId extends string = string> {
// (undocumented)
static create<Messages extends Record<keyof Messages, string>>(
config: TranslationRefConfig<Messages>,
): TranslationRefImpl<Messages>;
$$type: '@backstage/TranslationResource';
// (undocumented)
getDefaultMessages(): Messages;
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<TId>
| {
[key in keyof TMessages]: string | null;
};
}>;
},
> {
// (undocumented)
getId(): string;
ref: TranslationRef_2<TId, TMessages>;
// (undocumented)
getLazyResources():
| Record<
string,
() => Promise<{
messages: Messages;
}>
>
| undefined;
// (undocumented)
getResources(): Record<string, Messages> | undefined;
// (undocumented)
toString(): string;
translations: TTranslations;
}
// @alpha
@@ -116,13 +195,15 @@ export function usePluginOptions<
// @alpha (undocumented)
export const useTranslationRef: <
Messages extends Record<keyof Messages, string>,
TMessages extends {
[x: string]: string;
},
>(
translationRef: TranslationRef<Messages>,
) => <Tkey extends keyof Messages>(
key: Tkey,
translationRef: TranslationRef<string, TMessages>,
) => <TKey extends keyof TMessages & string>(
key: TKey,
options?: TranslationOptions,
) => Messages[Tkey];
) => TMessages[TKey];
// (No @packageDocumentation comment for this package)
```
@@ -22,14 +22,12 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api';
export type AppTranslationApi = {
getI18n(): i18n;
addResourcesByRef<Messages extends Record<string, string>>(
translationRef: TranslationRef<Messages>,
): void;
addResource(resource: TranslationRef): void;
};
/**
* @alpha
*/
export const appTranslationApiRef: ApiRef<AppTranslationApi> = createApiRef({
id: 'core.apptranslation',
id: 'core.translation',
});
@@ -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',
},
});
});
});
@@ -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<TId, TMessages>;
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<TId, TMessages, TFull>,
): TranslationMessages<TId, TMessages, TFull> {
return {
$$type: '@backstage/TranslationMessages',
id: options.ref.id,
full: Boolean(options.full) as TFull,
messages: options.messages as TMessages,
};
}
@@ -13,48 +13,61 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TranslationRefImpl, createTranslationRef } from './TranslationRef';
import {
createTranslationRef,
toInternalTranslationRef,
} from './TranslationRef';
import { toInternalTranslationResource } from './TranslationResource';
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',
const ref = createTranslationRef({
id: 'test',
messages: { key: 'value' },
};
});
const translationRef = createTranslationRef(config);
const internalRef = toInternalTranslationRef(ref);
expect(translationRef.getId()).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 = TranslationRefImpl.create(config);
const internalRef = toInternalTranslationRef(ref);
const lazyResources = translationRef.getLazyResources();
expect(internalRef.$$type).toBe('@backstage/TranslationRef');
expect(internalRef.version).toBe('v1');
expect(internalRef.id).toBe('test');
expect(internalRef.getDefaultMessages()).toEqual({ key: 'value' });
const messages = await lazyResources?.en();
expect(messages).toEqual({ messages: { key: 'value' } });
const internalResource = toInternalTranslationResource(
internalRef.getDefaultResource()!,
);
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',
},
});
});
});
@@ -14,48 +14,131 @@
* limitations under the License.
*/
import { TranslationRef, TranslationRefConfig } from './types';
import {
createTranslationResource,
TranslationResource,
} from './TranslationResource';
/** @alpha */
export class TranslationRefImpl<Messages extends Record<keyof Messages, string>>
implements TranslationRef<Messages>
{
static create<Messages extends Record<keyof Messages, string>>(
config: TranslationRefConfig<Messages>,
) {
return new TranslationRefImpl(config);
}
export interface TranslationRef<
TId extends string = string,
TMessages extends { [key in string]: string } = { [key in string]: string },
> {
$$type: '@backstage/TranslationRef';
getId() {
return this.config.id;
}
id: TId;
getDefaultMessages(): Messages {
return this.config.messages;
}
T: TMessages;
}
getLazyResources():
| Record<string, () => Promise<{ messages: Messages }>>
| undefined {
return this.config.lazyResources;
}
/** @internal */
type AnyMessages = { [key in string]: string };
getResources(): Record<string, Messages> | undefined {
return this.config.resources;
}
/** @internal */
export interface InternalTranslationRef<
TId extends string = string,
TMessages extends { [key in string]: string } = { [key in string]: string },
> extends TranslationRef<TId, TMessages> {
version: 'v1';
toString() {
return `TranslationRef(${this.getId()})`;
}
getDefaultMessages(): AnyMessages;
private constructor(
private readonly config: TranslationRefConfig<Messages>,
) {}
getDefaultResource(): TranslationResource | undefined;
}
/** @alpha */
export const createTranslationRef = <
Messages extends Record<keyof Messages, string> = {},
export interface TranslationRefOptions<
TId extends string,
TMessages extends { [key in string]: string },
TTranslations extends {
[language in string]: () => Promise<{
default: { [key in keyof TMessages]: string | null };
}>;
},
> {
id: TId;
messages: TMessages;
translations?: TTranslations;
}
/** @internal */
class TranslationRefImpl<
TId extends string,
TMessages extends { [key in string]: string },
> implements InternalTranslationRef<TId, TMessages>
{
#id: TId;
#messages: TMessages;
#resources: TranslationResource | undefined;
constructor(options: TranslationRefOptions<TId, TMessages, any>) {
this.#id = options.id;
this.#messages = options.messages;
}
$$type = '@backstage/TranslationRef' as const;
version = 'v1' as const;
get id(): TId {
return this.#id;
}
get T(): never {
throw new Error('Not implemented');
}
getDefaultMessages(): AnyMessages {
return this.#messages;
}
setDefaultResource(resources: TranslationResource): void {
this.#resources = resources;
}
getDefaultResource(): TranslationResource | undefined {
return this.#resources;
}
toString() {
return `TranslationRef{id=${this.id}}`;
}
}
/** @alpha */
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: TranslationRefConfig<Messages>,
): TranslationRef<Messages> => TranslationRefImpl.create(config);
config: TranslationRefOptions<TId, TMessages, TTranslations>,
): TranslationRef<TId, TMessages> {
const ref = new TranslationRefImpl(config);
if (config.translations) {
ref.setDefaultResource(
createTranslationResource({
ref,
translations: config.translations as any,
}),
);
}
return ref;
}
/** @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;
}
@@ -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',
},
});
});
});
@@ -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<TId extends string = string> {
$$type: '@backstage/TranslationResource';
id: TId;
}
/** @internal */
export interface InternalTranslationResource<TId extends string = string>
extends TranslationResource<TId> {
version: 'v1';
resources: Array<{
language: string;
loader(): Promise<{ messages: { [key in string]: string | null } }>;
}>;
}
/** @internal */
export function toInternalTranslationResource<TId extends string>(
resource: TranslationResource<TId>,
): InternalTranslationResource<TId> {
const r = resource as InternalTranslationResource<TId>;
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<TId>
| { [key in keyof TMessages]: string | null };
}>;
},
> {
ref: TranslationRef<TId, TMessages>;
translations: TTranslations;
}
/** @alpha */
export function createTranslationResource<
TId extends string,
TMessages extends { [key in string]: string },
TTranslations extends {
[language in string]: () => Promise<{
default:
| TranslationMessages<TId>
| { [key in keyof TMessages]: string | null };
}>;
},
>(
options: TranslationResourceOptions<TId, TMessages, TTranslations>,
): TranslationResource<TId> {
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<TId>;
}
@@ -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',
},
});
@@ -0,0 +1,5 @@
{
"one": "ett",
"two": "två",
"three": "tre"
}
@@ -0,0 +1,4 @@
{
"apple": "apfel",
"orange": "apfelsine"
}
@@ -13,5 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './apis/implementations/AppTranslationApi';
export * from './app/TranslationResource';
import { createTranslationMessages } from '../TranslationMessages';
import { fruitsTranslationRef } from './refs';
export default createTranslationMessages({
ref: fruitsTranslationRef,
messages: {
apple: 'äpple',
orange: 'apelsin',
},
});
@@ -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> = T extends TranslationRef<infer R>
? Partial<R>
: never;
export const countingTranslationRef = createTranslationRef({
id: 'counting',
messages: {
one: 'one',
two: 'two',
three: 'three',
},
});
/** @alpha */
export function createTranslationResource<T extends TranslationRef>(options: {
ref: T;
messages?: Record<string, TranslationMessages<T>>;
lazyMessages: Record<
string,
() => Promise<{ messages: TranslationMessages<T> }>
>;
}) {
return options;
}
export const fruitsTranslationRef = createTranslationRef({
id: 'fruits',
messages: {
apple: 'apple',
orange: 'orange',
},
translations: {
de: () => import('./fruits-de.json'),
},
});
@@ -14,6 +14,22 @@
* limitations under the License.
*/
export * from './TranslationRef';
export * from './types';
export * from './useTranslationRef';
export {
type TranslationMessages,
type TranslationMessagesOptions,
createTranslationMessages,
} from './TranslationMessages';
export {
type TranslationResource,
type TranslationResourceOptions,
createTranslationResource,
} from './TranslationResource';
export {
type TranslationRef,
type TranslationRefOptions,
createTranslationRef,
} from './TranslationRef';
export {
type TranslationOptions,
useTranslationRef,
} from './useTranslationRef';
@@ -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<keyof Messages, string>,
> {
id: string;
messages: Messages;
lazyResources?: Record<string, () => Promise<{ messages: Messages }>>;
resources?: Record<string, Messages>;
}
/** @alpha */
export interface TranslationRef<
Messages extends Record<keyof Messages, string> = Record<string, string>,
> {
getId(): string;
getDefaultMessages(): Messages;
getResources(): Record<string, Messages> | undefined;
getLazyResources():
| Record<string, () => Promise<{ messages: Messages }>>
| undefined;
}
/** @alpha */
export type TranslationOptions<
Messages extends Record<keyof Messages, string> = Record<string, string>,
> = Messages;
@@ -51,7 +51,7 @@ describe('useTranslationRef', () => {
};
(useApi as jest.Mock).mockReturnValue({
addResourcesByRef: jest.fn(),
addResource: jest.fn(),
});
(useTranslation as jest.Mock).mockReturnValue(i18nMock);
@@ -16,26 +16,34 @@
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<keyof Messages, string>,
TMessages extends { [key in string]: string },
>(
translationRef: TranslationRef<Messages>,
translationRef: TranslationRef<string, TMessages>,
) => {
const appTranslationApi = useApi(appTranslationApiRef);
const translationApi = useApi(appTranslationApiRef);
appTranslationApi.addResourcesByRef(translationRef);
const internalRef = toInternalTranslationRef(translationRef);
translationApi.addResource(translationRef);
const { t } = useTranslation(translationRef.getId());
const { t } = useTranslation(internalRef.id, {
useSuspense: process.env.NODE_ENV !== 'test',
});
const defaulteMessage = translationRef.getDefaultMessages();
const defaultMessages = internalRef.getDefaultMessages();
return <Tkey extends keyof Messages>(
key: Tkey,
return <TKey extends keyof TMessages & string>(
key: TKey,
options?: TranslationOptions,
): Messages[Tkey] => t(key as string, defaulteMessage[key], options);
): TMessages[TKey] => t(key, defaultMessages[key], options);
};
+8 -5
View File
@@ -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)
```
+1 -1
View File
@@ -238,7 +238,7 @@ export type PreparePullRequestFormProps<
UseFormReturn<TFieldValues>,
'formState' | 'register' | 'control' | 'setValue'
> & {
values: UnpackNestedValue<TFieldValues>;
values: TFieldValues;
},
) => React_2.ReactNode;
};
@@ -18,7 +18,6 @@ import React from 'react';
import {
FormProvider,
SubmitHandler,
UnpackNestedValue,
useForm,
UseFormProps,
UseFormReturn,
@@ -39,7 +38,7 @@ export type PreparePullRequestFormProps<
UseFormReturn<TFieldValues>,
'formState' | 'register' | 'control' | 'setValue'
> & {
values: UnpackNestedValue<TFieldValues>;
values: TFieldValues;
},
) => React.ReactNode;
};
+3
View File
@@ -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;
};
@@ -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<any> => {
const res = await fetch(url, {
const res = await (fetchApi?.fetch ?? fetch)(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -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');
});
});
});
@@ -0,0 +1 @@
MOCKCA
+18 -15
View File
@@ -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<
'user-settings',
{
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}}';
}
>;
// (No @packageDocumentation comment for this package)
```
@@ -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('<UserSettingsThemeToggle />', () => {
it('toggles the theme select button', async () => {
const themeApi = apiRegistry.get(appThemeApiRef);
// todo: general test provider
const messages: Record<string, string> =
userSettingsTranslationRef.getDefaultMessages();
const useTranslationRefMock = jest
.fn()
.mockReturnValue((key: string) => messages[key]);
(useTranslationRef as jest.Mock).mockImplementation(useTranslationRefMock);
const rendered = await renderWithEffects(
wrapInTestApp(
+1 -1
View File
@@ -33,5 +33,5 @@ export const userSettingsTranslationRef = createTranslationRef({
select_theme_custom: 'Select {{custom}}',
lng: '{{language}}',
select_lng: 'Select language {{language}}',
},
} as const,
});
+6 -6
View File
@@ -21802,9 +21802,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
@@ -37373,11 +37373,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