Merge remote-tracking branch 'origin/master' into cost-insight-trending-line

This commit is contained in:
bogdannechyporenko
2022-11-17 16:48:17 +01:00
59 changed files with 1214 additions and 220 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-node': patch
---
Wait for indexer initialization before finalizing indexing.
+17
View File
@@ -0,0 +1,17 @@
---
'@backstage/core-app-api': patch
'@backstage/core-components': patch
'@backstage/core-plugin-api': patch
'@backstage/dev-utils': patch
'@backstage/test-utils': patch
'@backstage/types': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-config-schema': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-shortcuts': patch
'@backstage/plugin-user-settings': patch
---
Updated dependency `zen-observable` to `^0.9.0`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Implement Custom Field Explorer to view and play around with available installed custom field extensions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Added `integrations.github.apps.allowedInstallationOwners` to the configuration schema.
+1
View File
@@ -407,3 +407,4 @@ zod
Zolotusky
zoomable
zsh
Lainfiesta
+31
View File
@@ -171,6 +171,37 @@ Exported types can be marked with either `@public`, `@alpha` or `@beta` release
If a package does not have this configuration, then all exported types are considered stable, even if they are marked as `@alpha` or `@beta`.
#### Changes that are Not Considered Breaking
There are a few exceptions that are not considered breaking in the [versioning policy](https://backstage.io/docs/overview/versioning-policy#changes-that-are-not-considered-breaking),
primarily regarding Utility APIs and Backend Service interfaces (referred to "contracts" below) that are supplied by the
Backstage core packages.
Example of a non-breaking change to a contract which has a default
implementation, since consumers typically only interact with that contract as
callers of existing methods:
```diff
export interface MyService {
oldMethod(): void;
+ newMethod(): void;
}
```
Changes such as these must still be marked with `**BREAKING PRODUCERS**:` in the
changelog, to highlight them for those who might be implementing custom
implementations of those contracts or putting mocks of the contract in tests.
Example of a breaking change to a contract, which affects existing consumers and
therefore makes it NOT fall under these exceptions:
```diff
export interface MyService {
- oldMethod(): void;
+ oldMethod(): Promise<void>;
}
```
#### Type Contract Direction
An important distinction to make when looking at changes to an API Report is the direction of the contract of a changed type, that is, whether it's used as input or output from the user's point of view. In the next two sections we'll dive into the different directions of a type contract, and how it affects whether a change is breaking or not.
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

+62 -2
View File
@@ -258,9 +258,58 @@ Passport-supported authentication method.
## Custom ScmAuthApi Implementation
If you are using any custom authentication providers, like for example one for GitHub Enterprise, then you are likely to need a custom implementation of the [`ScmAuthApi`](https://backstage.io/docs/reference/integration-react.scmauthapi). It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed, and is used for example by the Scaffolder (Software Templates) and Catalog Import plugins.
The default `ScmAuthAPi` provides integrations for `github`, `gitlab`, `azure` and `bitbucket` and is created by the following code in `packages/app/src/apis.ts`:
To set up a custom `ScmAuthApi` implementation, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The following example shows an implementation that supports both public GitHub via `githubAuthApi` as well as a GitHub Enterprise installation hosted at `ghe.example.com` via `gheAuthApi`:
```ts
ScmAuth.createDefaultApiFactory();
```
If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/docs/reference/integration-react.scmauthapi). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins.
The first step is to remove the code that creates the default providers.
```diff
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
+ ScmAuth,
} from '@backstage/integration-react';
export const apis: AnyApiFactory[] = [
...
+ ScmAuth.createDefaultApiFactory(),
...
];
```
Then replace it with something like this, which will create an `ApiFactory` with only a github provider.
```ts
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmAuthApiRef,
deps: {
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
),
});
```
If you use any custom authentication integrations, a new provider can be added to the `ApiFactory`.
The first step is to create a new authentication ref, which follows the naming convention of `xxxAuthApiRef`. The example below is for a new GitHub enterprise integration which can be defined either inside the app itself if it's only used for this purpose or inside a common internal package for APIs, such as `@internal/apis`:
```ts
const gheAuthApiRef: ApiRef<OAuthApi & ProfileInfoApi & SessionApi> =
createApiRef({
id: 'internal.auth.ghe',
});
```
The new ref is then used to add a new provider to the ApiFactory:
```ts
createApiFactory({
@@ -278,3 +327,14 @@ createApiFactory({
),
});
```
Finally, you also need to add and configure another provider to the `auth-backend` using the provider ID, which in this example is `ghe`:
```ts
import { providers } from '@backstage/plugin-auth-backend';
// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts
providerFactories: {
ghe: providers.github.create(),
},
```
@@ -181,3 +181,102 @@ const CustomFieldExtension = scaffolderPlugin.provide(
})
);
```
## Previewing Custom Field Extensions
You can preview custom field extensions you write in the Backstage UI using the Custom Field Explorer
(accessible via the `/create/edit` route by default):
![Custom Field Explorer](../../assets/software-templates/custom-field-explorer.png)
In order to make your new custom field extension available in the explorer you will have to define a
JSON schema that describes the input/output types on your field like in the following example:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
export const MyCustomExtensionWithOptionsSchema = {
uiOptions: {
type: 'object',
properties: {
focused: {
description: 'Whether to focus this field',
type: 'boolean',
},
},
},
returnValue: { type: 'string' },
};
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
```tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts
...
import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';
export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
);
```
We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema
and the provided `makeFieldSchemaFromZod` helper utility function to generate both the JSON schema
and type for your field props to preventing having to duplicate the definitions:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
...
import { z } from 'zod';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
focused: z
.boolean()
.optional()
.describe('Whether to focus this field'),
}),
);
export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;
type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: MyCustomExtensionWithOptionsProps) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
+1
View File
@@ -34,6 +34,7 @@ guide to do a repository-based installation.
- [Package manager](https://nodejs.org/en/download/package-manager/)
- [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md)
- `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install)
- You will need to use Yarn classic to create a new project, but it can then be [migrated to Yarn 3](../tutorials/yarn-migration.md)
- `docker` [installation](https://docs.docker.com/engine/install/)
- `git` [installation](https://github.com/git-guides/install-git)
- If the system is not directly accessible over your network the following ports
+20 -1
View File
@@ -99,7 +99,7 @@ This versioning is completely decoupled from the Backstage release versioning,
meaning you might for example have `@backstage/core-plugin-api` version `3.1.4`
be part of the `1.12` Backstage release.
Following versioning policy applies to all packages:
The following versioning policy applies to all packages:
- Breaking changes are noted in the changelog, and documentation is updated.
- Breaking changes are prefixed with `**BREAKING**: ` in the changelog.
@@ -120,6 +120,25 @@ For packages at version `1.0.0` or above, the following policy also applies:
changes without a deprecation period, but the changes must still adhere to
semver.
### Changes that are Not Considered Breaking
There are a few changes that would typically be considered breaking changes, but
that we make exceptions for. This is both to be able to evolve the project more
rapidly, also because the alternative ends up having a bigger impact on users.
For all Utility APIs and Backend Services that _have_ a built-in implementation,
we only consider the API stability for consumers of those interfaces. This means
that it is not considered a breaking change to break the contract for producers
of the interface.
Changes that fall under the above rule, must be marked with
`**BREAKING PRODUCERS**:` in the changelog.
For any case of dependency injection, it is not considered a breaking change to
add a dependency on a Utility API or Backend Service that is provided by the
framework. This includes any dependency that is provided by the
`@backstage/app-defaults` and `@backstage/backend-defaults` packages.
### Release Stages
The release stages(`@alpha`, `@beta` `@public`) refers to the
@@ -0,0 +1,16 @@
---
title: The Linux Foundation launches its Introduction to Backstage course
description: The new Linux Foundation course is available for free on edX and is designed to help individuals understand how to map Backstage to their organization needs
author: Jorge Lainfiesta, Roadie
authorURL: https://www.linkedin.com/in/jrlainfiesta/
---
Backstage continues to grow in [popularity and maturity](https://roadie.io/blog/backstage-consolidating-its-role-in-the-cloud-native-ecosystem/), with industry leaders not only adopting the framework but actively participating in the community through contributions and commercial offerings. One of the most recent contributions comes from the Linux Foundation, [launching an introductory course](https://training.linuxfoundation.org/blog/23107/) aimed at DevOps engineers or professionals working on Developer Productivity or Developer Experience. The course is available for free on edX, with a paid option for the certified track.
[![Introduction to Backstage: Developer Portals Made Easy (LFS142x): Enroll!](assets/22-11-15/Introduction-to-Backstage-Developer-Portals-Made-Easy-2-768x432.png)](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy)
<!--truncate-->
The course, [Introduction to Backstage: Developer Portals Made Easy (LFS142x)](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy), starts discussing the benefits of adopting a Developer Portal. Then, it dives into what is Backstage, its main features (Catalog, Scaffolder, TechDocs), and how to map its capabilities to your organization. At last, it provides tips on how to connect with the community to ease up your adoption journey.
The course author, [Jorge Lainfiesta](https://www.linkedin.com/in/jrlainfiesta/), is a Technical Marketing Manager at [Roadie](https://roadie.io). Jorge has a background in software engineering (PayPal) and digital communication (UCLA). Hes been working around Backstage since it was open sourced by Spotify and co-hosts community initiatives like the Backstage Users Unconference.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+1 -1
View File
@@ -80,7 +80,7 @@
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"devDependencies": {
"@backstage/test-utils": "workspace:^",
+1 -1
View File
@@ -39,7 +39,7 @@
"@types/prop-types": "^15.7.3",
"prop-types": "^15.7.2",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15",
"zen-observable": "^0.9.0",
"zod": "^3.11.6"
},
"peerDependencies": {
+1 -1
View File
@@ -67,7 +67,7 @@
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.6",
"remark-gfm": "^3.0.1",
"zen-observable": "^0.8.15",
"zen-observable": "^0.9.0",
"zod": "^3.11.6"
},
"peerDependencies": {
+1 -1
View File
@@ -38,7 +38,7 @@
"@backstage/version-bridge": "workspace:^",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+1 -1
View File
@@ -47,7 +47,7 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+8
View File
@@ -189,6 +189,14 @@ export interface Config {
* @visibility secret
*/
clientSecret: string;
/**
* List of installation owners allowed to be used by this GitHub app. The GitHub UI does not provide a way to list the installations.
* However you can list the installations with the GitHub API. You can find the list of installations here:
* https://api.github.com/app/installations
* The relevant documentation for this is here.
* https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples
*/
allowedInstallationOwners?: string[];
}>;
}>;
+1 -1
View File
@@ -45,7 +45,7 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"cross-fetch": "^3.1.5",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+1 -1
View File
@@ -35,7 +35,7 @@
"@backstage/cli": "workspace:^",
"@types/zen-observable": "^0.8.0",
"luxon": "^3.0.0",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"files": [
"dist"
+1 -1
View File
@@ -54,7 +54,7 @@
"qs": "^6.9.4",
"react-use": "^17.2.4",
"yaml": "^2.0.0",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+1 -1
View File
@@ -51,7 +51,7 @@
"lodash": "^4.17.21",
"react-helmet": "6.1.0",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"jsonschema": "^1.2.6",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
+1 -1
View File
@@ -77,7 +77,7 @@
"vm2": "^3.9.11",
"winston": "^3.2.1",
"yaml": "^2.0.0",
"zen-observable": "^0.8.15",
"zen-observable": "^0.9.0",
"zod": "^3.11.6"
},
"devDependencies": {
+151 -62
View File
@@ -35,6 +35,7 @@ import { TaskStep } from '@backstage/plugin-scaffolder-common';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { UIOptionsType } from '@rjsf/utils';
import { UiSchema } from '@rjsf/utils';
import { z } from 'zod';
// @alpha
export function createNextScaffolderFieldExtension<
@@ -57,6 +58,12 @@ export function createScaffolderLayout<TInputProps = unknown>(
options: LayoutOptions,
): Extension<LayoutComponent<TInputProps>>;
// @public
export type CustomFieldExtensionSchema = {
returnValue: JSONSchema7;
uiOptions?: JSONSchema7;
};
// @public
export type CustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
@@ -75,36 +82,52 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent<
// @public
export const EntityPickerFieldExtension: FieldExtensionComponent<
string,
EntityPickerUiOptions
{
defaultKind?: string | undefined;
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
}
>;
// @public (undocumented)
export const EntityPickerFieldSchema: FieldSchema<
string,
{
defaultKind?: string | undefined;
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
}
>;
// @public
export interface EntityPickerUiOptions {
// (undocumented)
allowArbitraryValues?: boolean;
// (undocumented)
allowedKinds?: string[];
// (undocumented)
defaultKind?: string;
// (undocumented)
defaultNamespace?: string | false;
}
export type EntityPickerUiOptions =
typeof EntityPickerFieldSchema.uiOptionsType;
// @public
export const EntityTagsPickerFieldExtension: FieldExtensionComponent<
string[],
EntityTagsPickerUiOptions
{
showCounts?: boolean | undefined;
kinds?: string[] | undefined;
helperText?: string | undefined;
}
>;
// @public (undocumented)
export const EntityTagsPickerFieldSchema: FieldSchema<
string[],
{
showCounts?: boolean | undefined;
kinds?: string[] | undefined;
helperText?: string | undefined;
}
>;
// @public
export interface EntityTagsPickerUiOptions {
// (undocumented)
helperText?: string;
// (undocumented)
kinds?: string[];
// (undocumented)
showCounts?: boolean;
}
export type EntityTagsPickerUiOptions =
typeof EntityTagsPickerFieldSchema.uiOptionsType;
// @public
export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
@@ -130,8 +153,19 @@ export type FieldExtensionOptions<
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
// @public
export interface FieldSchema<TReturn, TUiOptions> {
// (undocumented)
readonly schema: CustomFieldExtensionSchema;
// (undocumented)
readonly type: FieldExtensionComponentProps<TReturn, TUiOptions>;
// (undocumented)
readonly uiOptionsType: TUiOptions;
}
// @public
export type LayoutComponent<_TInputProps> = () => null;
@@ -169,6 +203,20 @@ export type LogEvent = {
taskId: string;
};
// @public
export function makeFieldSchemaFromZod<
TReturnSchema extends z.ZodType,
TUiOptionsSchema extends z.ZodType = z.ZodType<any, any, {}>,
>(
returnSchema: TReturnSchema,
uiOptionsSchema?: TUiOptionsSchema,
): FieldSchema<
TReturnSchema extends z.ZodType<any, any, infer IReturn> ? IReturn : never,
TUiOptionsSchema extends z.ZodType<any, any, infer IUiOptions>
? IUiOptions
: never
>;
// @alpha
export type NextCustomFieldValidator<TFieldReturnValue> = (
data: TFieldReturnValue,
@@ -199,6 +247,7 @@ export type NextFieldExtensionOptions<
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
// @alpha (undocumented)
@@ -228,36 +277,51 @@ export const nextSelectedTemplateRouteRef: SubRouteRef<
// @public
export const OwnedEntityPickerFieldExtension: FieldExtensionComponent<
string,
OwnedEntityPickerUiOptions
{
defaultKind?: string | undefined;
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
}
>;
// @public (undocumented)
export const OwnedEntityPickerFieldSchema: FieldSchema<
string,
{
defaultKind?: string | undefined;
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
}
>;
// @public
export interface OwnedEntityPickerUiOptions {
// (undocumented)
allowArbitraryValues?: boolean;
// (undocumented)
allowedKinds?: string[];
// (undocumented)
defaultKind?: string;
// (undocumented)
defaultNamespace?: string | false;
}
export type OwnedEntityPickerUiOptions =
typeof OwnedEntityPickerFieldSchema.uiOptionsType;
// @public
export const OwnerPickerFieldExtension: FieldExtensionComponent<
string,
OwnerPickerUiOptions
{
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
}
>;
// @public (undocumented)
export const OwnerPickerFieldSchema: FieldSchema<
string,
{
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
}
>;
// @public
export interface OwnerPickerUiOptions {
// (undocumented)
allowArbitraryValues?: boolean;
// (undocumented)
allowedKinds?: string[];
// (undocumented)
defaultNamespace?: string | false;
}
export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType;
// @public
export const repoPickerValidation: (
@@ -271,31 +335,56 @@ export const repoPickerValidation: (
// @public
export const RepoUrlPickerFieldExtension: FieldExtensionComponent<
string,
RepoUrlPickerUiOptions
{
allowedOwners?: string[] | undefined;
allowedOrganizations?: string[] | undefined;
allowedRepos?: string[] | undefined;
allowedHosts?: string[] | undefined;
requestUserCredentials?:
| {
additionalScopes?:
| {
azure?: string[] | undefined;
github?: string[] | undefined;
gitlab?: string[] | undefined;
bitbucket?: string[] | undefined;
gerrit?: string[] | undefined;
}
| undefined;
secretsKey: string;
}
| undefined;
}
>;
// @public (undocumented)
export const RepoUrlPickerFieldSchema: FieldSchema<
string,
{
allowedOwners?: string[] | undefined;
allowedOrganizations?: string[] | undefined;
allowedRepos?: string[] | undefined;
allowedHosts?: string[] | undefined;
requestUserCredentials?:
| {
additionalScopes?:
| {
azure?: string[] | undefined;
github?: string[] | undefined;
gitlab?: string[] | undefined;
bitbucket?: string[] | undefined;
gerrit?: string[] | undefined;
}
| undefined;
secretsKey: string;
}
| undefined;
}
>;
// @public
export interface RepoUrlPickerUiOptions {
// (undocumented)
allowedHosts?: string[];
// (undocumented)
allowedOrganizations?: string[];
// (undocumented)
allowedOwners?: string[];
// (undocumented)
allowedRepos?: string[];
// (undocumented)
requestUserCredentials?: {
secretsKey: string;
additionalScopes?: {
gerrit?: string[];
github?: string[];
gitlab?: string[];
bitbucket?: string[];
azure?: string[];
};
};
}
export type RepoUrlPickerUiOptions =
typeof RepoUrlPickerFieldSchema.uiOptionsType;
// @public (undocumented)
export const rootRouteRef: RouteRef<undefined>;
+3 -1
View File
@@ -74,7 +74,9 @@
"react-use": "^17.2.4",
"use-immer": "^0.7.0",
"yaml": "^2.0.0",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0",
"zod": "^3.11.6",
"zod-to-json-schema": "^3.18.1"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
@@ -0,0 +1,201 @@
/*
* 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 { StreamLanguage } from '@codemirror/language';
import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml';
import {
Button,
Card,
CardContent,
CardHeader,
FormControl,
IconButton,
InputLabel,
makeStyles,
MenuItem,
Select,
} from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import { withTheme } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import CodeMirror from '@uiw/react-codemirror';
import React, { useCallback, useMemo, useState } from 'react';
import yaml from 'yaml';
import { FieldExtensionOptions } from '../../extensions';
import * as fieldOverrides from '../MultistepJsonForm/FieldOverrides';
import { TemplateEditorForm } from './TemplateEditorForm';
const Form = withTheme(MuiTheme);
const useStyles = makeStyles(theme => ({
root: {
gridArea: 'pageContent',
display: 'grid',
gridTemplateAreas: `
"controls controls"
"fieldForm preview"
`,
gridTemplateRows: 'auto 1fr',
gridTemplateColumns: '1fr 1fr',
},
controls: {
gridArea: 'controls',
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
margin: theme.spacing(1),
},
fieldForm: {
gridArea: 'fieldForm',
},
preview: {
gridArea: 'preview',
},
}));
export const CustomFieldExplorer = ({
customFieldExtensions = [],
onClose,
}: {
customFieldExtensions?: FieldExtensionOptions<any, any>[];
onClose?: () => void;
}) => {
const classes = useStyles();
const fieldOptions = customFieldExtensions.filter(field => !!field.schema);
const [selectedField, setSelectedField] = useState(fieldOptions[0]);
const [fieldFormState, setFieldFormState] = useState({});
const [formState, setFormState] = useState({});
const [refreshKey, setRefreshKey] = useState(Date.now());
const sampleFieldTemplate = useMemo(
() =>
yaml.stringify({
parameters: [
{
title: `${selectedField.name} Example`,
properties: {
[selectedField.name]: {
type: selectedField.schema?.returnValue?.type,
'ui:field': selectedField.name,
'ui:options': fieldFormState,
},
},
},
],
}),
[fieldFormState, selectedField],
);
const fieldComponents = useMemo(() => {
return Object.fromEntries(
customFieldExtensions.map(({ name, component }) => [name, component]),
);
}, [customFieldExtensions]);
const handleSelectionChange = useCallback(
selection => {
setSelectedField(selection);
setFieldFormState({});
setFormState({});
},
[setFieldFormState, setFormState, setSelectedField],
);
const handleFieldConfigChange = useCallback(
state => {
setFieldFormState(state);
setFormState({});
// Force TemplateEditorForm to re-render since some fields
// may not be responsive to ui:option changes
setRefreshKey(Date.now());
},
[setFieldFormState, setRefreshKey],
);
return (
<main className={classes.root}>
<div className={classes.controls}>
<FormControl variant="outlined" size="small" fullWidth>
<InputLabel id="select-field-label">
Choose Custom Field Extension
</InputLabel>
<Select
value={selectedField}
label="Choose Custom Field Extension"
labelId="select-field-label"
onChange={e => handleSelectionChange(e.target.value)}
>
{fieldOptions.map((option, idx) => (
<MenuItem key={idx} value={option as any}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
<IconButton size="medium" onClick={onClose}>
<CloseIcon />
</IconButton>
</div>
<div className={classes.fieldForm}>
<Card>
<CardHeader title="Field Options" />
<CardContent>
<Form
showErrorList={false}
fields={{ ...fieldOverrides, ...fieldComponents }}
noHtml5Validate
formData={fieldFormState}
formContext={{ fieldFormState }}
onSubmit={e => handleFieldConfigChange(e.formData)}
schema={selectedField.schema?.uiOptions || {}}
>
<Button
variant="contained"
color="primary"
type="submit"
disabled={!selectedField.schema?.uiOptions}
>
Apply
</Button>
</Form>
</CardContent>
</Card>
</div>
<div className={classes.preview}>
<Card>
<CardHeader title="Example Template Spec" />
<CardContent>
<CodeMirror
readOnly
theme="dark"
height="100%"
extensions={[StreamLanguage.define(yamlSupport)]}
value={sampleFieldTemplate}
/>
</CardContent>
</Card>
<TemplateEditorForm
key={refreshKey}
content={sampleFieldTemplate}
contentIsSpec
fieldExtensions={customFieldExtensions}
data={formState}
onUpdate={setFormState}
setErrorText={() => null}
/>
</div>
</main>
);
};
@@ -44,7 +44,7 @@ const useStyles = makeStyles(theme => ({
interface EditorIntroProps {
style?: JSX.IntrinsicElements['div']['style'];
onSelect?: (option: 'local' | 'form') => void;
onSelect?: (option: 'local' | 'form' | 'field-explorer') => void;
}
export function TemplateEditorIntro(props: EditorIntroProps) {
@@ -104,6 +104,22 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
</Card>
);
const cardFieldExplorer = (
<Card className={classes.card} elevation={4}>
<CardActionArea onClick={() => props.onSelect?.('field-explorer')}>
<CardContent>
<Typography variant="h5" gutterBottom>
Custom Field Explorer
</Typography>
<Typography variant="body1">
View and play around with available installed custom field
extensions.
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
return (
<div style={props.style}>
<Typography variant="h6" className={classes.introText}>
@@ -121,6 +137,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
{supportsLoad && cardLoadLocal}
{cardFormEditor}
{!supportsLoad && cardLoadLocal}
{cardFieldExplorer}
</div>
</div>
);
@@ -19,6 +19,7 @@ import {
TemplateDirectoryAccess,
WebFileSystemAccess,
} from '../../lib/filesystem';
import { CustomFieldExplorer } from './CustomFieldExplorer';
import { TemplateEditorIntro } from './TemplateEditorIntro';
import { TemplateEditor } from './TemplateEditor';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
@@ -32,6 +33,9 @@ type Selection =
}
| {
type: 'form';
}
| {
type: 'field-explorer';
};
interface TemplateEditorPageProps {
@@ -62,6 +66,13 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
layouts={props.layouts}
/>
);
} else if (selection?.type === 'field-explorer') {
content = (
<CustomFieldExplorer
customFieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
/>
);
} else {
content = (
<Content>
@@ -73,6 +84,8 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
.catch(() => {});
} else if (option === 'form') {
setSelection({ type: 'form' });
} else if (option === 'field-explorer') {
setSelection({ type: 'field-explorer' });
}
}}
/>
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import React from 'react';
import { FieldExtensionComponentProps } from '../../../extensions';
import { EntityNamePickerProps } from './schema';
import { TextField } from '@material-ui/core';
export { EntityNamePickerSchema } from './schema';
/**
* EntityName Picker
*/
export const EntityNamePicker = (
props: FieldExtensionComponentProps<string>,
) => {
export const EntityNamePicker = (props: EntityNamePickerProps) => {
const {
onChange,
required,
@@ -0,0 +1,23 @@
/*
* 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 { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
const EntityNamePickerFieldSchema = makeFieldSchemaFromZod(z.string());
export const EntityNamePickerSchema = EntityNamePickerFieldSchema.schema;
export type EntityNamePickerProps = typeof EntityNamePickerFieldSchema.type;
@@ -23,20 +23,9 @@ import FormControl from '@material-ui/core/FormControl';
import Autocomplete from '@material-ui/lab/Autocomplete';
import React, { useCallback, useEffect } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { FieldExtensionComponentProps } from '../../../extensions';
import { EntityPickerProps } from './schema';
/**
* The input props that can be specified under `ui:options` for the
* `EntityPicker` field extension.
*
* @public
*/
export interface EntityPickerUiOptions {
allowedKinds?: string[];
defaultKind?: string;
allowArbitraryValues?: boolean;
defaultNamespace?: string | false;
}
export { EntityPickerSchema } from './schema';
/**
* The underlying component that is rendered in the form for the `EntityPicker`
@@ -44,9 +33,7 @@ export interface EntityPickerUiOptions {
*
* @public
*/
export const EntityPicker = (
props: FieldExtensionComponentProps<string, EntityPickerUiOptions>,
) => {
export const EntityPicker = (props: EntityPickerProps) => {
const {
onChange,
schema: { title = 'Entity', description = 'An entity from the catalog' },
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { EntityPickerUiOptions } from './EntityPicker';
export { EntityPickerFieldSchema, type EntityPickerUiOptions } from './schema';
@@ -0,0 +1,59 @@
/*
* 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 { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
/**
* @public
*/
export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
allowedKinds: z
.array(z.string())
.optional()
.describe('List of kinds of entities to derive options from'),
defaultKind: z
.string()
.optional()
.describe(
'The default entity kind. Options of this kind will not be prefixed.',
),
allowArbitraryValues: z
.boolean()
.optional()
.describe('Whether to allow arbitrary user input. Defaults to true'),
defaultNamespace: z
.union([z.string(), z.literal(false)])
.optional()
.describe(
'The default namespace. Options with this namespace will not be prefixed.',
),
}),
);
/**
* The input props that can be specified under `ui:options` for the
* `EntityPicker` field extension.
*
* @public
*/
export type EntityPickerUiOptions =
typeof EntityPickerFieldSchema.uiOptionsType;
export type EntityPickerProps = typeof EntityPickerFieldSchema.type;
export const EntityPickerSchema = EntityPickerFieldSchema.schema;
@@ -22,19 +22,9 @@ import { useApi } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { FormControl, TextField } from '@material-ui/core';
import { Autocomplete } from '@material-ui/lab';
import { FieldExtensionComponentProps } from '../../../extensions';
import { EntityTagsPickerProps } from './schema';
/**
* The input props that can be specified under `ui:options` for the
* `EntityTagsPicker` field extension.
*
* @public
*/
export interface EntityTagsPickerUiOptions {
kinds?: string[];
showCounts?: boolean;
helperText?: string;
}
export { EntityTagsPickerSchema } from './schema';
/**
* The underlying component that is rendered in the form for the `EntityTagsPicker`
@@ -42,9 +32,7 @@ export interface EntityTagsPickerUiOptions {
*
* @public
*/
export const EntityTagsPicker = (
props: FieldExtensionComponentProps<string[], EntityTagsPickerUiOptions>,
) => {
export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
const { formData, onChange, uiSchema } = props;
const catalogApi = useApi(catalogApiRef);
const [tagOptions, setTagOptions] = useState<string[]>([]);
@@ -13,4 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { EntityTagsPickerUiOptions } from './EntityTagsPicker';
export {
EntityTagsPickerFieldSchema,
type EntityTagsPickerUiOptions,
} from './schema';
@@ -0,0 +1,48 @@
/*
* 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 { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
/**
* @public
*/
export const EntityTagsPickerFieldSchema = makeFieldSchemaFromZod(
z.array(z.string()),
z.object({
kinds: z
.array(z.string())
.optional()
.describe('List of kinds of entities to derive tags from'),
showCounts: z
.boolean()
.optional()
.describe('Whether to show usage counts per tag'),
helperText: z.string().optional().describe('Helper text to display'),
}),
);
export const EntityTagsPickerSchema = EntityTagsPickerFieldSchema.schema;
export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.type;
/**
* The input props that can be specified under `ui:options` for the
* `EntityTagsPicker` field extension.
*
* @public
*/
export type EntityTagsPickerUiOptions =
typeof EntityTagsPickerFieldSchema.uiOptionsType;
@@ -26,20 +26,9 @@ import Autocomplete from '@material-ui/lab/Autocomplete';
import React, { useMemo } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { FieldExtensionComponentProps } from '../../../extensions';
import { OwnedEntityPickerProps } from './schema';
/**
* The input props that can be specified under `ui:options` for the
* `OwnedEntityPicker` field extension.
*
* @public
*/
export interface OwnedEntityPickerUiOptions {
allowedKinds?: string[];
defaultKind?: string;
allowArbitraryValues?: boolean;
defaultNamespace?: string | false;
}
export { OwnedEntityPickerSchema } from './schema';
/**
* The underlying component that is rendered in the form for the `OwnedEntityPicker`
@@ -47,9 +36,7 @@ export interface OwnedEntityPickerUiOptions {
*
* @public
*/
export const OwnedEntityPicker = (
props: FieldExtensionComponentProps<string, OwnedEntityPickerUiOptions>,
) => {
export const OwnedEntityPicker = (props: OwnedEntityPickerProps) => {
const {
onChange,
schema: { title = 'Entity', description = 'An entity from the catalog' },
@@ -13,4 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker';
export {
OwnedEntityPickerFieldSchema,
type OwnedEntityPickerUiOptions,
} from './schema';
@@ -0,0 +1,59 @@
/*
* 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 { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
/**
* @public
*/
export const OwnedEntityPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
allowedKinds: z
.array(z.string())
.optional()
.describe('List of kinds of entities to derive options from'),
defaultKind: z
.string()
.optional()
.describe(
'The default entity kind. Options of this kind will not be prefixed.',
),
allowArbitraryValues: z
.boolean()
.optional()
.describe('Whether to allow arbitrary user input. Defaults to true'),
defaultNamespace: z
.union([z.string(), z.literal(false)])
.optional()
.describe(
'The default namespace. Options with this namespace will not be prefixed.',
),
}),
);
/**
* The input props that can be specified under `ui:options` for the
* `OwnedEntityPicker` field extension.
*
* @public
*/
export type OwnedEntityPickerUiOptions =
typeof OwnedEntityPickerFieldSchema.uiOptionsType;
export type OwnedEntityPickerProps = typeof OwnedEntityPickerFieldSchema.type;
export const OwnedEntityPickerSchema = OwnedEntityPickerFieldSchema.schema;
@@ -15,19 +15,9 @@
*/
import React from 'react';
import { EntityPicker } from '../EntityPicker/EntityPicker';
import { FieldExtensionComponentProps } from '../../../extensions';
import { OwnerPickerProps } from './schema';
/**
* The input props that can be specified under `ui:options` for the
* `OwnerPicker` field extension.
*
* @public
*/
export interface OwnerPickerUiOptions {
allowedKinds?: string[];
allowArbitraryValues?: boolean;
defaultNamespace?: string | false;
}
export { OwnerPickerSchema } from './schema';
/**
* The underlying component that is rendered in the form for the `OwnerPicker`
@@ -35,9 +25,7 @@ export interface OwnerPickerUiOptions {
*
* @public
*/
export const OwnerPicker = (
props: FieldExtensionComponentProps<string, OwnerPickerUiOptions>,
) => {
export const OwnerPicker = (props: OwnerPickerProps) => {
const {
schema: { title = 'Owner', description = 'The owner of the component' },
uiSchema,
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { OwnerPickerUiOptions } from './OwnerPicker';
export { OwnerPickerFieldSchema, type OwnerPickerUiOptions } from './schema';
@@ -0,0 +1,55 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
/**
* @public
*/
export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
allowedKinds: z
.array(z.string())
.default(['Group', 'User'])
.optional()
.describe(
'List of kinds of entities to derive options from. Defaults to Group and User',
),
allowArbitraryValues: z
.boolean()
.optional()
.describe('Whether to allow arbitrary user input. Defaults to true'),
defaultNamespace: z
.union([z.string(), z.literal(false)])
.optional()
.describe(
'The default namespace. Options with this namespace will not be prefixed.',
),
}),
);
/**
* The input props that can be specified under `ui:options` for the
* `OwnerPicker` field extension.
*
* @public
*/
export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType;
export type OwnerPickerProps = typeof OwnerPickerFieldSchema.type;
export const OwnerPickerSchema = OwnerPickerFieldSchema.schema;
@@ -24,36 +24,15 @@ import { GitlabRepoPicker } from './GitlabRepoPicker';
import { AzureRepoPicker } from './AzureRepoPicker';
import { BitbucketRepoPicker } from './BitbucketRepoPicker';
import { GerritRepoPicker } from './GerritRepoPicker';
import { FieldExtensionComponentProps } from '../../../extensions';
import { RepoUrlPickerHost } from './RepoUrlPickerHost';
import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName';
import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils';
import { RepoUrlPickerProps } from './schema';
import { RepoUrlPickerState } from './types';
import useDebounce from 'react-use/lib/useDebounce';
import { useTemplateSecrets } from '../../secrets';
/**
* The input props that can be specified under `ui:options` for the
* `RepoUrlPicker` field extension.
*
* @public
*/
export interface RepoUrlPickerUiOptions {
allowedHosts?: string[];
allowedOrganizations?: string[];
allowedOwners?: string[];
allowedRepos?: string[];
requestUserCredentials?: {
secretsKey: string;
additionalScopes?: {
gerrit?: string[];
github?: string[];
gitlab?: string[];
bitbucket?: string[];
azure?: string[];
};
};
}
export { RepoUrlPickerSchema } from './schema';
/**
* The underlying component that is rendered in the form for the `RepoUrlPicker`
@@ -61,9 +40,7 @@ export interface RepoUrlPickerUiOptions {
*
* @public
*/
export const RepoUrlPicker = (
props: FieldExtensionComponentProps<string, RepoUrlPickerUiOptions>,
) => {
export const RepoUrlPicker = (props: RepoUrlPickerProps) => {
const { uiSchema, onChange, rawErrors, formData } = props;
const [state, setState] = useState<RepoUrlPickerState>(
parseRepoPickerUrl(formData),
@@ -13,5 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { RepoUrlPickerUiOptions } from './RepoUrlPicker';
export {
RepoUrlPickerFieldSchema,
type RepoUrlPickerUiOptions,
} from './schema';
export { repoPickerValidation } from './validation';
@@ -0,0 +1,95 @@
/*
* 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 { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
/**
* @public
*/
export const RepoUrlPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
allowedHosts: z
.array(z.string())
.optional()
.describe('List of allowed SCM platform hosts'),
allowedOrganizations: z
.array(z.string())
.optional()
.describe('List of allowed organizations in the given SCM platform'),
allowedOwners: z
.array(z.string())
.optional()
.describe('List of allowed owners in the given SCM platform'),
allowedRepos: z
.array(z.string())
.optional()
.describe('List of allowed repos in the given SCM platform'),
requestUserCredentials: z
.object({
secretsKey: z
.string()
.describe(
'Key used within the template secrets context to store the credential',
),
additionalScopes: z
.object({
gerrit: z
.array(z.string())
.optional()
.describe('Additional Gerrit scopes to request'),
github: z
.array(z.string())
.optional()
.describe('Additional GitHub scopes to request'),
gitlab: z
.array(z.string())
.optional()
.describe('Additional GitLab scopes to request'),
bitbucket: z
.array(z.string())
.optional()
.describe('Additional BitBucket scopes to request'),
azure: z
.array(z.string())
.optional()
.describe('Additional Azure scopes to request'),
})
.optional()
.describe('Additional permission scopes to request'),
})
.optional()
.describe(
'If defined will request user credentials to auth against the given SCM platform',
),
}),
);
/**
* The input props that can be specified under `ui:options` for the
* `RepoUrlPicker` field extension.
*
* @public
*/
export type RepoUrlPickerUiOptions =
typeof RepoUrlPickerFieldSchema.uiOptionsType;
export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type;
// NOTE: There is a bug with this failing validation in the custom field explorer due
// to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if
// requestUserCredentials is not defined
export const RepoUrlPickerSchema = RepoUrlPickerFieldSchema.schema;
@@ -18,3 +18,4 @@ export * from './OwnerPicker';
export * from './RepoUrlPicker';
export * from './OwnedEntityPicker';
export * from './EntityTagsPicker';
export { type FieldSchema, makeFieldSchemaFromZod } from './utils';
@@ -0,0 +1,62 @@
/*
* 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 { JSONSchema7 } from 'json-schema';
import { z } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
import {
CustomFieldExtensionSchema,
FieldExtensionComponentProps,
} from '../../extensions';
/**
* @public
* FieldSchema encapsulates a JSONSchema7 along with the
* matching FieldExtensionComponentProps type for a field extension.
*/
export interface FieldSchema<TReturn, TUiOptions> {
readonly schema: CustomFieldExtensionSchema;
readonly type: FieldExtensionComponentProps<TReturn, TUiOptions>;
readonly uiOptionsType: TUiOptions;
}
/**
* @public
* Utility function to convert zod return and UI options schemas to a
* CustomFieldExtensionSchema with FieldExtensionComponentProps type inference
*/
export function makeFieldSchemaFromZod<
TReturnSchema extends z.ZodType,
TUiOptionsSchema extends z.ZodType = z.ZodType<any, any, {}>,
>(
returnSchema: TReturnSchema,
uiOptionsSchema?: TUiOptionsSchema,
): FieldSchema<
TReturnSchema extends z.ZodType<any, any, infer IReturn> ? IReturn : never,
TUiOptionsSchema extends z.ZodType<any, any, infer IUiOptions>
? IUiOptions
: never
> {
return {
schema: {
returnValue: zodToJsonSchema(returnSchema) as JSONSchema7,
uiOptions: uiOptionsSchema
? (zodToJsonSchema(uiOptionsSchema) as JSONSchema7)
: undefined,
},
type: null as any,
uiOptionsType: null as any,
};
}
+30 -6
View File
@@ -13,40 +13,64 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker';
import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker';
import {
EntityPicker,
EntityPickerSchema,
} from '../components/fields/EntityPicker/EntityPicker';
import {
EntityNamePicker,
EntityNamePickerSchema,
} from '../components/fields/EntityNamePicker/EntityNamePicker';
import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation';
import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker';
import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker';
import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker';
import {
EntityTagsPicker,
EntityTagsPickerSchema,
} from '../components/fields/EntityTagsPicker/EntityTagsPicker';
import {
OwnerPicker,
OwnerPickerSchema,
} from '../components/fields/OwnerPicker/OwnerPicker';
import {
RepoUrlPicker,
RepoUrlPickerSchema,
} from '../components/fields/RepoUrlPicker/RepoUrlPicker';
import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation';
import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker';
import {
OwnedEntityPicker,
OwnedEntityPickerSchema,
} from '../components/fields/OwnedEntityPicker/OwnedEntityPicker';
export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [
{
component: EntityPicker,
name: 'EntityPicker',
schema: EntityPickerSchema,
},
{
component: EntityNamePicker,
name: 'EntityNamePicker',
validation: entityNamePickerValidation,
schema: EntityNamePickerSchema,
},
{
component: EntityTagsPicker,
name: 'EntityTagsPicker',
schema: EntityTagsPickerSchema,
},
{
component: RepoUrlPicker,
name: 'RepoUrlPicker',
validation: repoPickerValidation,
schema: RepoUrlPickerSchema,
},
{
component: OwnerPicker,
name: 'OwnerPicker',
schema: OwnerPickerSchema,
},
{
component: OwnedEntityPicker,
name: 'OwnedEntityPicker',
schema: OwnedEntityPickerSchema,
},
];
@@ -16,6 +16,7 @@
import React from 'react';
import {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
@@ -103,6 +104,7 @@ attachComponentData(
);
export type {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
+14 -2
View File
@@ -15,14 +15,14 @@
*/
import { ApiHolder } from '@backstage/core-plugin-api';
import { FieldValidation, FieldProps } from '@rjsf/core';
import { PropsWithChildren } from 'react';
import {
UIOptionsType,
FieldProps as FieldPropsV5,
UiSchema as UiSchemaV5,
FieldValidation as FieldValidationV5,
} from '@rjsf/utils';
import { PropsWithChildren } from 'react';
import { JSONSchema7 } from 'json-schema';
/**
* Field validation type for Custom Field Extensions.
@@ -35,6 +35,16 @@ export type CustomFieldValidator<TFieldReturnValue> = (
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
/**
* Type for the Custom Field Extension schema.
*
* @public
*/
export type CustomFieldExtensionSchema = {
returnValue: JSONSchema7;
uiOptions?: JSONSchema7;
};
/**
* Type for the Custom Field Extension with the
* name and components and validation function.
@@ -50,6 +60,7 @@ export type FieldExtensionOptions<
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
/**
@@ -107,4 +118,5 @@ export type NextFieldExtensionOptions<
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
+1
View File
@@ -43,6 +43,7 @@ export {
ScaffolderFieldExtensions,
} from './extensions';
export type {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
+30 -6
View File
@@ -16,12 +16,24 @@
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { scaffolderApiRef, ScaffolderClient } from './api';
import { EntityPicker } from './components/fields/EntityPicker/EntityPicker';
import {
EntityPicker,
EntityPickerSchema,
} from './components/fields/EntityPicker/EntityPicker';
import { entityNamePickerValidation } from './components/fields/EntityNamePicker';
import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker';
import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker';
import {
EntityNamePicker,
EntityNamePickerSchema,
} from './components/fields/EntityNamePicker/EntityNamePicker';
import {
OwnerPicker,
OwnerPickerSchema,
} from './components/fields/OwnerPicker/OwnerPicker';
import { repoPickerValidation } from './components/fields/RepoUrlPicker';
import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker';
import {
RepoUrlPicker,
RepoUrlPickerSchema,
} from './components/fields/RepoUrlPicker/RepoUrlPicker';
import { createScaffolderFieldExtension } from './extensions';
import {
nextRouteRef,
@@ -37,8 +49,14 @@ import {
fetchApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker';
import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker';
import {
OwnedEntityPicker,
OwnedEntityPickerSchema,
} from './components/fields/OwnedEntityPicker/OwnedEntityPicker';
import {
EntityTagsPicker,
EntityTagsPickerSchema,
} from './components/fields/EntityTagsPicker/EntityTagsPicker';
/**
* The main plugin export for the scaffolder.
@@ -82,6 +100,7 @@ export const EntityPickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
component: EntityPicker,
name: 'EntityPicker',
schema: EntityPickerSchema,
}),
);
@@ -95,6 +114,7 @@ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide(
component: EntityNamePicker,
name: 'EntityNamePicker',
validation: entityNamePickerValidation,
schema: EntityNamePickerSchema,
}),
);
@@ -109,6 +129,7 @@ export const RepoUrlPickerFieldExtension = scaffolderPlugin.provide(
component: RepoUrlPicker,
name: 'RepoUrlPicker',
validation: repoPickerValidation,
schema: RepoUrlPickerSchema,
}),
);
@@ -121,6 +142,7 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
component: OwnerPicker,
name: 'OwnerPicker',
schema: OwnerPickerSchema,
}),
);
@@ -146,6 +168,7 @@ export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
component: OwnedEntityPicker,
name: 'OwnedEntityPicker',
schema: OwnedEntityPickerSchema,
}),
);
@@ -157,6 +180,7 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
component: EntityTagsPicker,
name: 'EntityTagsPicker',
schema: EntityTagsPickerSchema,
}),
);
@@ -111,6 +111,12 @@ export abstract class BatchSearchEngineIndexer extends Writable {
*/
async _final(done: (error?: Error | null) => void) {
try {
const maybeError = await this.initialized;
if (maybeError) {
done(maybeError);
return;
}
// Index any remaining documents.
if (this.currentBatch.length) {
await this.index(this.currentBatch);
+1 -1
View File
@@ -34,7 +34,7 @@
"react-hook-form": "^7.12.2",
"react-use": "^17.2.4",
"uuid": "^8.3.2",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
+1 -1
View File
@@ -43,7 +43,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@types/react": "^16.13.1 || ^17.0.0",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
+4 -15
View File
@@ -7875,28 +7875,17 @@ __metadata:
linkType: hard
"loader-utils@npm:^1.2.3":
version: 1.4.1
resolution: "loader-utils@npm:1.4.1"
version: 1.4.2
resolution: "loader-utils@npm:1.4.2"
dependencies:
big.js: ^5.2.2
emojis-list: ^3.0.0
json5: ^1.0.1
checksum: ea0b648cba0194e04a90aab6270619f0e35be009e33a443d9e642e93056cd49e6ca4c9678bd1c777a2392551bc5f4d0f24a87f5040608da1274aa84c6eebb502
checksum: eb6fb622efc0ffd1abdf68a2022f9eac62bef8ec599cf8adb75e94d1d338381780be6278534170e99edc03380a6d29bc7eb1563c89ce17c5fed3a0b17f1ad804
languageName: node
linkType: hard
"loader-utils@npm:^2.0.0":
version: 2.0.2
resolution: "loader-utils@npm:2.0.2"
dependencies:
big.js: ^5.2.2
emojis-list: ^3.0.0
json5: ^2.1.2
checksum: 9078d1ed47cadc57f4c6ddbdb2add324ee7da544cea41de3b7f1128e8108fcd41cd3443a85b7ee8d7d8ac439148aa221922774efe4cf87506d4fb054d5889303
languageName: node
linkType: hard
"loader-utils@npm:^2.0.3":
"loader-utils@npm:^2.0.0, loader-utils@npm:^2.0.3":
version: 2.0.4
resolution: "loader-utils@npm:2.0.4"
dependencies:
+23 -14
View File
@@ -3269,7 +3269,7 @@ __metadata:
react-router-dom-stable: "npm:react-router-dom@^6.3.0"
react-router-stable: "npm:react-router@^6.3.0"
react-use: ^17.2.4
zen-observable: ^0.8.15
zen-observable: ^0.9.0
zod: ^3.11.6
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
@@ -3390,7 +3390,7 @@ __metadata:
react-virtualized-auto-sizer: ^1.0.6
react-window: ^1.8.6
remark-gfm: ^3.0.1
zen-observable: ^0.8.15
zen-observable: ^0.9.0
zod: ^3.11.6
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
@@ -3422,7 +3422,7 @@ __metadata:
history: ^5.0.0
msw: ^0.48.0
prop-types: ^15.7.2
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0
@@ -3477,7 +3477,7 @@ __metadata:
"@testing-library/user-event": ^14.0.0
"@types/node": ^16.0.0
react-use: ^17.2.4
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0
@@ -4712,7 +4712,7 @@ __metadata:
react-test-renderer: ^16.13.1
react-use: ^17.2.4
yaml: ^2.0.0
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0
@@ -4752,7 +4752,7 @@ __metadata:
lodash: ^4.17.21
react-helmet: 6.1.0
react-use: ^17.2.4
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0
@@ -5020,7 +5020,7 @@ __metadata:
jsonschema: ^1.2.6
msw: ^0.48.0
react-use: ^17.2.4
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
languageName: unknown
@@ -6571,7 +6571,7 @@ __metadata:
vm2: ^3.9.11
winston: ^3.2.1
yaml: ^2.0.0
zen-observable: ^0.8.15
zen-observable: ^0.9.0
zod: ^3.11.6
languageName: unknown
linkType: soft
@@ -6645,7 +6645,9 @@ __metadata:
react-use: ^17.2.4
use-immer: ^0.7.0
yaml: ^2.0.0
zen-observable: ^0.8.15
zen-observable: ^0.9.0
zod: ^3.11.6
zod-to-json-schema: ^3.18.1
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0
@@ -6880,7 +6882,7 @@ __metadata:
react-hook-form: ^7.12.2
react-use: ^17.2.4
uuid: ^8.3.2
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-router: 6.0.0-beta.0 || ^6.3.0
@@ -7474,7 +7476,7 @@ __metadata:
cross-fetch: ^3.1.5
msw: ^0.48.0
react-use: ^17.2.4
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-router: 6.0.0-beta.0 || ^6.3.0
@@ -7600,7 +7602,7 @@ __metadata:
"@types/node": ^16.11.26
cross-fetch: ^3.1.5
msw: ^0.48.0
zen-observable: ^0.8.15
zen-observable: ^0.9.0
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0
@@ -7626,7 +7628,7 @@ __metadata:
"@backstage/cli": "workspace:^"
"@types/zen-observable": ^0.8.0
luxon: ^3.0.0
zen-observable: ^0.8.15
zen-observable: ^0.9.0
languageName: unknown
linkType: soft
@@ -20884,7 +20886,7 @@ __metadata:
react-router-dom: ^6.3.0
react-use: ^17.2.4
start-server-and-test: ^1.10.11
zen-observable: ^0.8.15
zen-observable: ^0.9.0
languageName: unknown
linkType: soft
@@ -37625,6 +37627,13 @@ __metadata:
languageName: node
linkType: hard
"zen-observable@npm:^0.9.0":
version: 0.9.0
resolution: "zen-observable@npm:0.9.0"
checksum: d770639f7cb47c89e442b0b87e6955079b9d0f3f7da220f081c3a478ee7b837ee792be4ae8764f2d49fe0afaaa3fbcdc7a88042c3ed471df9ae551de90e82ea2
languageName: node
linkType: hard
"zenscroll@npm:^4.0.2":
version: 4.0.2
resolution: "zenscroll@npm:4.0.2"