Merge branch 'backstage:master' into master
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Add the `FrontendFeature` type, which is the union of `BackstagePlugin` and `ExtensionOverrides`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
'@backstage/core-compat-api': patch
|
||||
---
|
||||
|
||||
Leverage the new `FrontendFeature` type to simplify interfaces
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
Removed `featureLoader` from `createApp`, `features` instead accepts both `FrontendFeature` and `CreateAppFeatureLoader`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Add a new git repository url picker for `gitea`. This `GiteaRepoPicker` can be used in a template to scaffold a project to be cloned using gitea.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Add permission check to catalog create and refresh button
|
||||
@@ -309,7 +309,7 @@ package, which is done as follows:
|
||||
.addRouter('', await app(appEnv));
|
||||
```
|
||||
3. Remove the `@backstage/plugin-app-backend` and the app package dependency
|
||||
(e.g. `app`) from `packages/backend/packages.json`. If you don't remove the
|
||||
(e.g. `app`) from `packages/backend/package.json`. If you don't remove the
|
||||
app package dependency the app will still be built and bundled with the
|
||||
backend.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Documentation on adding internationalization to the plugin
|
||||
|
||||
## Overview
|
||||
|
||||
The Backstage core function provides internationalization for plugins
|
||||
The Backstage core function provides internationalization for plugins. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys.
|
||||
|
||||
## For a plugin developer
|
||||
|
||||
@@ -46,6 +46,107 @@ return (
|
||||
);
|
||||
```
|
||||
|
||||
You will see how the initial dictionary structure and nesting gets converted into dot notation, so we encourage `camelCase` in key names and lean on the nesting structure to separate keys.
|
||||
|
||||
### Guidelines for `i18n` messages and keys
|
||||
|
||||
The API for `i18n` messages and keys can be pretty tricky to get right, as it's a pretty flexible API. We've put together some guidelines to help you get started that encourage good practices when thinking about translating plugins:
|
||||
|
||||
#### Key names
|
||||
|
||||
When defining messages it is recommended to use a nested structure that represents the semantic hierarchy in your translations. This allows for better organization and understanding of the structure. For example:
|
||||
|
||||
```ts
|
||||
export const myPluginTranslationRef = createTranslationRef({
|
||||
id: 'plugin.my-plugin',
|
||||
messages: {
|
||||
dashboardPage: {
|
||||
title: 'All your components',
|
||||
subtitle: 'Create new component',
|
||||
widgets: {
|
||||
weather: {
|
||||
title: 'Weather',
|
||||
description: 'Shows the weather',
|
||||
},
|
||||
calendar: {
|
||||
title: 'Calendar',
|
||||
description: 'Shows the calendar',
|
||||
},
|
||||
},
|
||||
},
|
||||
entityPage: {
|
||||
notFound: 'Entity not found',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Think about the semantic placement of content rather than the text content itself. Group related translations under a common prefix, and use nesting to represent relationships between different parts of your application. It's good to start grouping under extensions, page sections, or visual scopes and experiences.
|
||||
|
||||
Translations should avoid using their own text content as key where possible, as this can lead to confusion if the translation changes. Instead prefer to use keys that describe the location or usage of the text.
|
||||
|
||||
#### Common Key names
|
||||
|
||||
This list is intended to grow over time, but below are some examples of common key names and patterns that we encourage you to use where possible:
|
||||
|
||||
- `${page}.title`
|
||||
- `${page}.subtitle`
|
||||
- `${page}.description`
|
||||
|
||||
- `${page}.header.title`
|
||||
|
||||
#### Key reuse
|
||||
|
||||
Reusing the same key in multiple places is discouraged. This helps prevent ambiguity, and instead keeps the usage of each key as clear as possible. Consider creating duplicate keys that are grouped under a semantic section instead.
|
||||
|
||||
#### Flat keys
|
||||
|
||||
Avoid a flat key structure at the root level, as it can lead to naming conflicts and make the translation file harder to manage and change evolve over time. Instead, group translations under a common prefix.
|
||||
|
||||
```ts
|
||||
export const myPluginTranslationRef = createTranslationRef({
|
||||
id: 'plugin.my-plugin',
|
||||
messages: {
|
||||
// this is BAD
|
||||
title: 'My page',
|
||||
subtitle: 'My subtitle',
|
||||
// this is GOOD
|
||||
dashboardPage: {
|
||||
header: {
|
||||
title: 'All your components',
|
||||
subtitle: 'Create new component',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Plurals
|
||||
|
||||
The `i18next` library, which is used as the underlying implementation, has built-in support for pluralization. You can use this feature as is described in [the documentation](https://www.i18next.com/translation-function/plurals).
|
||||
|
||||
We encourage you to use this feature and avoid creating different key prefixes for pluralized content. For example:
|
||||
|
||||
```ts
|
||||
export const myPluginTranslationRef = createTranslationRef({
|
||||
id: 'plugin.my-plugin',
|
||||
messages: {
|
||||
dashboardPage: {
|
||||
title: 'All your components',
|
||||
subtitle: 'Create new component',
|
||||
cards: {
|
||||
title_one: 'You have one card',
|
||||
title_two: 'You have two cards',
|
||||
title_other: 'You have many cards ({{count}})',
|
||||
},
|
||||
},
|
||||
entityPage: {
|
||||
notFound: 'Entity not found',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## For an application developer overwrite plugin messages
|
||||
|
||||
In an app you can both override the default messages, as well as register translations for additional languages:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Scaffolder odo CLI actions
|
||||
author: Red Hat
|
||||
authorUrl: https://developers.redhat.com
|
||||
category: Scaffolder
|
||||
description: Collection of actions to run odo CLI commands. odo is a developer-focused CLI for container-based application development on Podman and Kubernetes.
|
||||
documentation: https://github.com/redhat-developer/backstage-odo-devfile-plugin/blob/main/packages/scaffolder-odo-actions-backend/README.md
|
||||
iconUrl: https://odo.dev/img/logo.png
|
||||
npmPackageName: '@redhat-developer/plugin-scaffolder-odo-actions'
|
||||
addedDate: '2023-12-08'
|
||||
@@ -4,10 +4,9 @@
|
||||
|
||||
```ts
|
||||
import { AnyRouteRefParams } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
@@ -21,7 +20,7 @@ export function compatWrapper(element: ReactNode): React_2.JSX.Element;
|
||||
// @public (undocumented)
|
||||
export function convertLegacyApp(
|
||||
rootElement: React_2.JSX.Element,
|
||||
): (ExtensionOverrides | BackstagePlugin)[];
|
||||
): FrontendFeature[];
|
||||
|
||||
// @public
|
||||
export function convertLegacyRouteRef<TParams extends AnyRouteRefParams>(
|
||||
|
||||
@@ -22,8 +22,7 @@ import React, {
|
||||
isValidElement,
|
||||
} from 'react';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
ExtensionOverrides,
|
||||
FrontendFeature,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
@@ -61,7 +60,7 @@ function selectChildren(
|
||||
/** @public */
|
||||
export function convertLegacyApp(
|
||||
rootElement: React.JSX.Element,
|
||||
): (ExtensionOverrides | BackstagePlugin)[] {
|
||||
): FrontendFeature[] {
|
||||
if (getComponentData(rootElement, 'core.type') === 'FlatRoutes') {
|
||||
return collectLegacyRoutes(rootElement);
|
||||
}
|
||||
|
||||
@@ -3,30 +3,34 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createApp(options?: {
|
||||
features?: (BackstagePlugin | ExtensionOverrides)[];
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{
|
||||
config: ConfigApi;
|
||||
}>;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
featureLoader?: (ctx: {
|
||||
config: ConfigApi;
|
||||
}) => Promise<(BackstagePlugin | ExtensionOverrides)[]>;
|
||||
}): {
|
||||
createRoot(): JSX_2.Element;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface CreateAppFeatureLoader {
|
||||
getLoaderName(): string;
|
||||
load(options: { config: ConfigApi }): Promise<{
|
||||
features: FrontendFeature[];
|
||||
}>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type CreateAppRouteBinder = <
|
||||
TExternalRoutes extends {
|
||||
@@ -45,7 +49,7 @@ export function createExtensionTree(options: { config: Config }): ExtensionTree;
|
||||
|
||||
// @public
|
||||
export function createSpecializedApp(options?: {
|
||||
features?: (BackstagePlugin | ExtensionOverrides)[];
|
||||
features?: FrontendFeature[];
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
}): {
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BackstagePlugin,
|
||||
ExtensionOverrides,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
FrontendFeature,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef';
|
||||
@@ -33,9 +32,7 @@ export interface RouteRefsById {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function collectRouteIds(
|
||||
features: (BackstagePlugin | ExtensionOverrides)[],
|
||||
): RouteRefsById {
|
||||
export function collectRouteIds(features: FrontendFeature[]): RouteRefsById {
|
||||
const routesById = new Map<string, RouteRef | SubRouteRef>();
|
||||
const externalRoutesById = new Map<string, ExternalRouteRef>();
|
||||
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
ExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Extension, FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { readAppExtensionsConfig } from './readAppExtensionsConfig';
|
||||
import { resolveAppTree } from './resolveAppTree';
|
||||
import { resolveAppNodeSpecs } from './resolveAppNodeSpecs';
|
||||
@@ -28,7 +24,7 @@ import { instantiateAppNodeTree } from './instantiateAppNodeTree';
|
||||
|
||||
/** @internal */
|
||||
export interface CreateAppTreeOptions {
|
||||
features: (BackstagePlugin | ExtensionOverrides)[];
|
||||
features: FrontendFeature[];
|
||||
builtinExtensions: Extension<unknown>[];
|
||||
config: Config;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
ExtensionOverrides,
|
||||
FrontendFeature,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
|
||||
@@ -30,7 +31,7 @@ import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/res
|
||||
|
||||
/** @internal */
|
||||
export function resolveAppNodeSpecs(options: {
|
||||
features: (BackstagePlugin | ExtensionOverrides)[];
|
||||
features: FrontendFeature[];
|
||||
builtinExtensions: Extension<unknown>[];
|
||||
parameters: Array<ExtensionParameters>;
|
||||
forbidden?: Set<string>;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
createThemeExtension,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { createApp } from './createApp';
|
||||
import { CreateAppFeatureLoader, createApp } from './createApp';
|
||||
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
@@ -99,6 +99,66 @@ describe('createApp', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should support feature loaders', async () => {
|
||||
const loader: CreateAppFeatureLoader = {
|
||||
getLoaderName() {
|
||||
return 'test-loader';
|
||||
},
|
||||
async load({ config }) {
|
||||
return {
|
||||
features: [
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createPageExtension({
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>{config.getString('key')}</div>,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const app = createApp({
|
||||
configLoader: async () => ({
|
||||
config: new MockConfigApi({ key: 'config-value' }),
|
||||
}),
|
||||
features: [loader],
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByText('config-value'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should propagate errors thrown by feature loaders', async () => {
|
||||
const loader: CreateAppFeatureLoader = {
|
||||
getLoaderName() {
|
||||
return 'test-loader';
|
||||
},
|
||||
async load() {
|
||||
throw new TypeError('boom');
|
||||
},
|
||||
};
|
||||
|
||||
const app = createApp({
|
||||
configLoader: async () => ({
|
||||
config: new MockConfigApi({}),
|
||||
}),
|
||||
features: [loader],
|
||||
});
|
||||
|
||||
await expect(
|
||||
renderWithEffects(app.createRoot()),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to read frontend features from loader 'test-loader', TypeError: boom"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should register feature flags', async () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: new MockConfigApi({}) }),
|
||||
|
||||
@@ -19,7 +19,6 @@ import { ConfigReader, Config } from '@backstage/config';
|
||||
import {
|
||||
AppTree,
|
||||
appTreeApiRef,
|
||||
BackstagePlugin,
|
||||
ComponentRef,
|
||||
componentsApiRef,
|
||||
coreExtensionData,
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
createThemeExtension,
|
||||
createTranslationExtension,
|
||||
ExtensionDataRef,
|
||||
ExtensionOverrides,
|
||||
FrontendFeature,
|
||||
RouteRef,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
@@ -102,6 +101,7 @@ import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiri
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
|
||||
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
|
||||
export const builtinExtensions = [
|
||||
Core,
|
||||
@@ -217,8 +217,8 @@ export function createExtensionTree(options: {
|
||||
}
|
||||
|
||||
function deduplicateFeatures(
|
||||
allFeatures: (BackstagePlugin | ExtensionOverrides)[],
|
||||
): (BackstagePlugin | ExtensionOverrides)[] {
|
||||
allFeatures: FrontendFeature[],
|
||||
): FrontendFeature[] {
|
||||
// Start by removing duplicates by reference
|
||||
const features = Array.from(new Set(allFeatures));
|
||||
|
||||
@@ -239,14 +239,30 @@ function deduplicateFeatures(
|
||||
.reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of dynamically loaded frontend features.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CreateAppFeatureLoader {
|
||||
/**
|
||||
* Returns name of this loader. suitable for showing to users.
|
||||
*/
|
||||
getLoaderName(): string;
|
||||
|
||||
/**
|
||||
* Loads a number of features dynamically.
|
||||
*/
|
||||
load(options: { config: ConfigApi }): Promise<{
|
||||
features: FrontendFeature[];
|
||||
}>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createApp(options?: {
|
||||
features?: (BackstagePlugin | ExtensionOverrides)[];
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{ config: ConfigApi }>;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
featureLoader?: (ctx: {
|
||||
config: ConfigApi;
|
||||
}) => Promise<(BackstagePlugin | ExtensionOverrides)[]>;
|
||||
}): {
|
||||
createRoot(): JSX.Element;
|
||||
} {
|
||||
@@ -258,15 +274,28 @@ export function createApp(options?: {
|
||||
);
|
||||
|
||||
const discoveredFeatures = getAvailableFeatures(config);
|
||||
const loadedFeatures = (await options?.featureLoader?.({ config })) ?? [];
|
||||
|
||||
const providedFeatures: FrontendFeature[] = [];
|
||||
for (const entry of options?.features ?? []) {
|
||||
if ('load' in entry) {
|
||||
try {
|
||||
const result = await entry.load({ config });
|
||||
providedFeatures.push(...result.features);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
providedFeatures.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const app = createSpecializedApp({
|
||||
config,
|
||||
features: [
|
||||
...discoveredFeatures,
|
||||
...loadedFeatures,
|
||||
...(options?.features ?? []),
|
||||
],
|
||||
features: [...discoveredFeatures, ...providedFeatures],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
}).createRoot();
|
||||
|
||||
@@ -288,10 +317,11 @@ export function createApp(options?: {
|
||||
/**
|
||||
* Synchronous version of {@link createApp}, expecting all features and
|
||||
* config to have been loaded already.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createSpecializedApp(options?: {
|
||||
features?: (BackstagePlugin | ExtensionOverrides)[];
|
||||
features?: FrontendFeature[];
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
}): { createRoot(): JSX.Element } {
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
ExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
|
||||
interface DiscoveryGlobal {
|
||||
modules: Array<{ name: string; export?: string; default: unknown }>;
|
||||
@@ -58,9 +55,7 @@ function readPackageDetectionConfig(config: Config) {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getAvailableFeatures(
|
||||
config: Config,
|
||||
): (BackstagePlugin | ExtensionOverrides)[] {
|
||||
export function getAvailableFeatures(config: Config): FrontendFeature[] {
|
||||
const discovered = (
|
||||
window as { '__@backstage/discovered__'?: DiscoveryGlobal }
|
||||
)['__@backstage/discovered__'];
|
||||
@@ -86,9 +81,7 @@ export function getAvailableFeatures(
|
||||
);
|
||||
}
|
||||
|
||||
function isBackstageFeature(
|
||||
obj: unknown,
|
||||
): obj is BackstagePlugin | ExtensionOverrides {
|
||||
function isBackstageFeature(obj: unknown): obj is FrontendFeature {
|
||||
if (obj !== null && typeof obj === 'object' && '$$type' in obj) {
|
||||
return (
|
||||
obj.$$type === '@backstage/BackstagePlugin' ||
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
createApp,
|
||||
createSpecializedApp,
|
||||
createExtensionTree,
|
||||
type CreateAppFeatureLoader,
|
||||
type ExtensionTreeNode,
|
||||
type ExtensionTree,
|
||||
} from './createApp';
|
||||
|
||||
@@ -863,6 +863,9 @@ export { FetchApi };
|
||||
|
||||
export { fetchApiRef };
|
||||
|
||||
// @public (undocumented)
|
||||
export type FrontendFeature = BackstagePlugin | ExtensionOverrides;
|
||||
|
||||
export { githubAuthApiRef };
|
||||
|
||||
export { gitlabAuthApiRef };
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
Extension,
|
||||
resolveExtensionDefinition,
|
||||
} from './resolveExtensionDefinition';
|
||||
import { FeatureFlagConfig } from './types';
|
||||
import { ExtensionOverrides, FeatureFlagConfig } from './types';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionOverridesOptions {
|
||||
@@ -27,11 +27,6 @@ export interface ExtensionOverridesOptions {
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionOverrides {
|
||||
readonly $$type: '@backstage/ExtensionOverrides';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalExtensionOverrides extends ExtensionOverrides {
|
||||
readonly version: 'v1';
|
||||
|
||||
@@ -18,13 +18,14 @@ import React from 'react';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
import { createPlugin, BackstagePlugin } from './createPlugin';
|
||||
import { createPlugin } from './createPlugin';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { createExtension } from './createExtension';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import { coreExtensionData } from './coreExtensionData';
|
||||
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
|
||||
import { createExtensionInput } from './createExtensionInput';
|
||||
import { BackstagePlugin } from './types';
|
||||
|
||||
const nameExtensionDataRef = createExtensionDataRef<string>('name');
|
||||
|
||||
|
||||
@@ -15,18 +15,16 @@
|
||||
*/
|
||||
|
||||
import { ExtensionDefinition } from './createExtension';
|
||||
import { ExternalRouteRef, RouteRef } from '../routing';
|
||||
import { FeatureFlagConfig } from './types';
|
||||
import {
|
||||
Extension,
|
||||
resolveExtensionDefinition,
|
||||
} from './resolveExtensionDefinition';
|
||||
|
||||
/** @public */
|
||||
export type AnyRoutes = { [name in string]: RouteRef };
|
||||
|
||||
/** @public */
|
||||
export type AnyExternalRoutes = { [name in string]: ExternalRouteRef };
|
||||
import {
|
||||
AnyExternalRoutes,
|
||||
AnyRoutes,
|
||||
BackstagePlugin,
|
||||
FeatureFlagConfig,
|
||||
} from './types';
|
||||
|
||||
/** @public */
|
||||
export interface PluginOptions<
|
||||
@@ -40,17 +38,6 @@ export interface PluginOptions<
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface BackstagePlugin<
|
||||
Routes extends AnyRoutes = AnyRoutes,
|
||||
ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
> {
|
||||
readonly $$type: '@backstage/BackstagePlugin';
|
||||
readonly id: string;
|
||||
readonly routes: Routes;
|
||||
readonly externalRoutes: ExternalRoutes;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface InternalBackstagePlugin<
|
||||
Routes extends AnyRoutes = AnyRoutes,
|
||||
|
||||
@@ -34,17 +34,17 @@ export {
|
||||
type ExtensionDataRef,
|
||||
type ConfigurableExtensionDataRef,
|
||||
} from './createExtensionDataRef';
|
||||
export {
|
||||
createPlugin,
|
||||
type BackstagePlugin,
|
||||
type PluginOptions,
|
||||
type AnyRoutes,
|
||||
type AnyExternalRoutes,
|
||||
} from './createPlugin';
|
||||
export { createPlugin, type PluginOptions } from './createPlugin';
|
||||
export {
|
||||
createExtensionOverrides,
|
||||
type ExtensionOverrides,
|
||||
type ExtensionOverridesOptions,
|
||||
} from './createExtensionOverrides';
|
||||
export { type Extension } from './resolveExtensionDefinition';
|
||||
export type { FeatureFlagConfig } from './types';
|
||||
export {
|
||||
type AnyRoutes,
|
||||
type AnyExternalRoutes,
|
||||
type BackstagePlugin,
|
||||
type ExtensionOverrides,
|
||||
type FeatureFlagConfig,
|
||||
type FrontendFeature,
|
||||
} from './types';
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExternalRouteRef, RouteRef } from '../routing';
|
||||
|
||||
/**
|
||||
* Feature flag configuration.
|
||||
*
|
||||
@@ -23,3 +25,28 @@ export type FeatureFlagConfig = {
|
||||
/** Feature flag name */
|
||||
name: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type AnyRoutes = { [name in string]: RouteRef };
|
||||
|
||||
/** @public */
|
||||
export type AnyExternalRoutes = { [name in string]: ExternalRouteRef };
|
||||
|
||||
/** @public */
|
||||
export interface BackstagePlugin<
|
||||
Routes extends AnyRoutes = AnyRoutes,
|
||||
ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
> {
|
||||
readonly $$type: '@backstage/BackstagePlugin';
|
||||
readonly id: string;
|
||||
readonly routes: Routes;
|
||||
readonly externalRoutes: ExternalRoutes;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionOverrides {
|
||||
readonly $$type: '@backstage/ExtensionOverrides';
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type FrontendFeature = BackstagePlugin | ExtensionOverrides;
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"@backstage/integration-react": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-permission-react": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-common": "workspace:^",
|
||||
"@backstage/plugin-search-common": "workspace:^",
|
||||
"@backstage/plugin-search-react": "workspace:^",
|
||||
@@ -83,7 +84,7 @@
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/plugin-permission-react": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
|
||||
@@ -33,6 +33,12 @@ import { RELATION_OWNED_BY } from '@backstage/catalog-model';
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { permissionApiRef } from '@backstage/plugin-permission-react';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
|
||||
const mockAuthorize = jest.fn();
|
||||
|
||||
const mockPermissionApi = { authorize: mockAuthorize };
|
||||
|
||||
describe('<AboutCard />', () => {
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
@@ -87,6 +93,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -143,6 +150,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -198,6 +206,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -240,6 +249,7 @@ describe('<AboutCard />', () => {
|
||||
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -276,6 +286,10 @@ describe('<AboutCard />', () => {
|
||||
},
|
||||
};
|
||||
|
||||
mockAuthorize.mockImplementation(async () => ({
|
||||
result: AuthorizeResult.ALLOW,
|
||||
}));
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
@@ -284,6 +298,7 @@ describe('<AboutCard />', () => {
|
||||
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, mockPermissionApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -308,6 +323,55 @@ describe('<AboutCard />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render refresh button if the permission is DENY', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://backstage.io/catalog-info.yaml',
|
||||
},
|
||||
name: 'software-deny',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
|
||||
mockAuthorize.mockImplementation(async () => ({
|
||||
result: AuthorizeResult.DENY,
|
||||
}));
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, mockPermissionApi],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTitle('Schedule entity refresh'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render refresh button if the location is not an url or file', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
@@ -330,6 +394,7 @@ describe('<AboutCard />', () => {
|
||||
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -384,6 +449,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -440,6 +506,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -493,6 +560,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -546,6 +614,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -592,6 +661,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -659,6 +729,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -707,6 +778,7 @@ describe('<AboutCard />', () => {
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
|
||||
@@ -60,6 +60,8 @@ import DocsIcon from '@material-ui/icons/Description';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha';
|
||||
|
||||
const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
|
||||
|
||||
@@ -108,6 +110,9 @@ export function AboutCard(props: AboutCardProps) {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const viewTechdocLink = useRouteRef(viewTechDocRouteRef);
|
||||
const templateRoute = useRouteRef(createFromTemplateRouteRef);
|
||||
const { allowed: canRefresh } = useEntityPermission(
|
||||
catalogEntityRefreshPermission,
|
||||
);
|
||||
|
||||
const entitySourceLocation = getEntitySourceLocation(
|
||||
entity,
|
||||
@@ -215,7 +220,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
title="About"
|
||||
action={
|
||||
<>
|
||||
{allowRefresh && (
|
||||
{allowRefresh && canRefresh && (
|
||||
<IconButton
|
||||
aria-label="Refresh"
|
||||
title="Schedule entity refresh"
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { mockBreakpoint } from '@backstage/core-components/testUtils';
|
||||
import {
|
||||
MockPermissionApi,
|
||||
MockStorageApi,
|
||||
TestApiProvider,
|
||||
renderInTestApp,
|
||||
@@ -46,6 +47,7 @@ import { CatalogTableRow } from '../CatalogTable';
|
||||
import { DefaultCatalogPage } from './DefaultCatalogPage';
|
||||
|
||||
import { CatalogTableColumnsFunc } from '../CatalogTable/types';
|
||||
import { permissionApiRef } from '@backstage/plugin-permission-react';
|
||||
|
||||
describe('DefaultCatalogPage', () => {
|
||||
const origReplaceState = window.history.replaceState;
|
||||
@@ -168,6 +170,7 @@ describe('DefaultCatalogPage', () => {
|
||||
[identityApiRef, identityApi],
|
||||
[storageApiRef, storageApi],
|
||||
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
|
||||
[permissionApiRef, new MockPermissionApi()],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -45,6 +45,8 @@ import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
import { CatalogTableColumnsFunc } from '../CatalogTable/types';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
|
||||
/** @internal */
|
||||
export type BaseCatalogPageProps = {
|
||||
@@ -60,15 +62,20 @@ export function BaseCatalogPage(props: BaseCatalogPageProps) {
|
||||
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
|
||||
const createComponentLink = useRouteRef(createComponentRouteRef);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const { allowed } = usePermission({
|
||||
permission: catalogEntityCreatePermission,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageWithHeader title={t('indexPage.title', { orgName })} themeId="home">
|
||||
<Content>
|
||||
<ContentHeader title="">
|
||||
<CreateButton
|
||||
title={t('indexPage.createButtonTitle')}
|
||||
to={createComponentLink && createComponentLink()}
|
||||
/>
|
||||
{allowed && (
|
||||
<CreateButton
|
||||
title={t('indexPage.createButtonTitle')}
|
||||
to={createComponentLink && createComponentLink()}
|
||||
/>
|
||||
)}
|
||||
<SupportButton>All your software catalog entities</SupportButton>
|
||||
</ContentHeader>
|
||||
<EntityListProvider pagination={pagination}>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
|
||||
@@ -54,6 +54,8 @@ import EditIcon from '@material-ui/icons/Edit';
|
||||
import EmailIcon from '@material-ui/icons/Email';
|
||||
import GroupIcon from '@material-ui/icons/Group';
|
||||
import { LinksGroup } from '../../Meta';
|
||||
import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha';
|
||||
|
||||
const CardTitle = (props: { title: string }) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
@@ -70,6 +72,9 @@ export const GroupProfileCard = (props: {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const { entity: group } = useEntity<GroupEntity>();
|
||||
const { allowed: canRefresh } = useEntityPermission(
|
||||
catalogEntityRefreshPermission,
|
||||
);
|
||||
|
||||
const refreshEntity = useCallback(async () => {
|
||||
await catalogApi.refreshEntity(stringifyEntityRef(group));
|
||||
@@ -127,7 +132,7 @@ export const GroupProfileCard = (props: {
|
||||
variant={props.variant}
|
||||
action={
|
||||
<>
|
||||
{allowRefresh && (
|
||||
{allowRefresh && canRefresh && (
|
||||
<IconButton
|
||||
aria-label="Refresh"
|
||||
title="Schedule entity refresh"
|
||||
|
||||
@@ -379,6 +379,7 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2<
|
||||
secretsKey: string;
|
||||
additionalScopes?:
|
||||
| {
|
||||
gitea?: string[] | undefined;
|
||||
gerrit?: string[] | undefined;
|
||||
github?: string[] | undefined;
|
||||
gitlab?: string[] | undefined;
|
||||
@@ -405,6 +406,7 @@ export const RepoUrlPickerFieldSchema: FieldSchema<
|
||||
secretsKey: string;
|
||||
additionalScopes?:
|
||||
| {
|
||||
gitea?: string[] | undefined;
|
||||
gerrit?: string[] | undefined;
|
||||
github?: string[] | undefined;
|
||||
gitlab?: string[] | undefined;
|
||||
|
||||
@@ -107,6 +107,7 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
...this.scmIntegrationsApi.bitbucketCloud.list(),
|
||||
...this.scmIntegrationsApi.bitbucketServer.list(),
|
||||
...this.scmIntegrationsApi.gerrit.list(),
|
||||
...this.scmIntegrationsApi.gitea.list(),
|
||||
...this.scmIntegrationsApi.github.list(),
|
||||
...this.scmIntegrationsApi.gitlab.list(),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { GiteaRepoPicker } from './GiteaRepoPicker';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
describe('GiteaRepoPicker', () => {
|
||||
describe('owner input field', () => {
|
||||
it('calls onChange when the owner input changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<GiteaRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'gitea.com' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const ownerInput = getAllByRole('textbox')[0];
|
||||
|
||||
fireEvent.change(ownerInput, { target: { value: 'test-owner' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ owner: 'test-owner' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import { Select, SelectItem } from '@backstage/core-components';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
|
||||
export const GiteaRepoPicker = (props: {
|
||||
allowedOwners?: string[];
|
||||
allowedRepos?: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { allowedOwners = [], state, onChange, rawErrors } = props;
|
||||
const ownerItems: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
const { owner } = state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
{allowedOwners?.length ? (
|
||||
<Select
|
||||
native
|
||||
label="Owner Available"
|
||||
onChange={selected =>
|
||||
onChange({
|
||||
owner: String(Array.isArray(selected) ? selected[0] : selected),
|
||||
})
|
||||
}
|
||||
disabled={allowedOwners.length === 1}
|
||||
selected={owner}
|
||||
items={ownerItems}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input
|
||||
id="ownerInput"
|
||||
onChange={e => onChange({ owner: e.target.value })}
|
||||
value={owner}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormHelperText>
|
||||
Gitea namespace where this repository will belong to. It can be the
|
||||
name of organization, group, subgroup, user, or the project.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@backstage/integration-react';
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { GithubRepoPicker } from './GithubRepoPicker';
|
||||
import { GiteaRepoPicker } from './GiteaRepoPicker';
|
||||
import { GitlabRepoPicker } from './GitlabRepoPicker';
|
||||
import { AzureRepoPicker } from './AzureRepoPicker';
|
||||
import { BitbucketRepoPicker } from './BitbucketRepoPicker';
|
||||
@@ -183,6 +184,15 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => {
|
||||
state={state}
|
||||
/>
|
||||
)}
|
||||
{hostType === 'gitea' && (
|
||||
<GiteaRepoPicker
|
||||
allowedOwners={allowedOwners}
|
||||
allowedRepos={allowedRepos}
|
||||
rawErrors={rawErrors}
|
||||
state={state}
|
||||
onChange={updateLocalState}
|
||||
/>
|
||||
)}
|
||||
{hostType === 'gitlab' && (
|
||||
<GitlabRepoPicker
|
||||
allowedOwners={allowedOwners}
|
||||
|
||||
@@ -51,6 +51,10 @@ export const RepoUrlPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
),
|
||||
additionalScopes: z
|
||||
.object({
|
||||
gitea: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Additional Gitea scopes to request'),
|
||||
gerrit: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
|
||||
@@ -64,6 +64,5 @@ export function parseRepoPickerUrl(
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
return { host, owner, repoName, organization, workspace, project };
|
||||
}
|
||||
|
||||
@@ -4158,6 +4158,7 @@ __metadata:
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
@@ -6049,6 +6050,7 @@ __metadata:
|
||||
"@backstage/integration-react": "workspace:^"
|
||||
"@backstage/plugin-catalog-common": "workspace:^"
|
||||
"@backstage/plugin-catalog-react": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
"@backstage/plugin-permission-react": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-common": "workspace:^"
|
||||
"@backstage/plugin-search-common": "workspace:^"
|
||||
@@ -8149,6 +8151,7 @@ __metadata:
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/dev-utils": "workspace:^"
|
||||
"@backstage/plugin-catalog": "workspace:^"
|
||||
"@backstage/plugin-catalog-common": "workspace:^"
|
||||
"@backstage/plugin-catalog-react": "workspace:^"
|
||||
"@backstage/plugin-permission-react": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user