Merge remote-tracking branch 'origin/master' into retry-and-recovery-doc

This commit is contained in:
Bogdan Nechyporenko
2025-02-13 21:54:20 +01:00
613 changed files with 10084 additions and 4655 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-auth-backend-module-github-provider': patch
---
Fixed a bug where the requested scope was ignored when refreshing sessions for a GitHub OAuth App. This would lead to access tokens being returned that didn't have the requested scope, and in turn errors when trying to use these tokens.
As part of this fix all existing sessions are being revoked in order to ensure that they receive the correct scope.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Added support for experimental form decorators when dry-running templates in the template editor.
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-catalog-backend-module-backstage-openapi': patch
'@backstage/plugin-catalog-backend-module-bitbucket-server': patch
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
'@backstage/plugin-catalog-backend-module-msgraph': patch
'@backstage/plugin-catalog-backend-module-github': patch
'@backstage/plugin-catalog-backend-module-azure': patch
'@backstage/plugin-catalog-backend-module-ldap': patch
'@backstage/plugin-catalog-backend-module-aws': patch
---
Minor doc string update
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Improved performance when applying conditional decisions to entities within the permission framework.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-backend': patch
---
Fixed issue `syncEntityDocs` that would cause the `/sync` endpoint to be continuously called if the request fails
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': minor
---
**BREAKING**: The `remoteUrl` output is no longer required, it can be empty only when using the new `createWhenEmpty` boolean flag.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-backend': minor
'@backstage/plugin-scaffolder-node': minor
---
Added new `createTemplateFilter`, `createTemplateGlobalFunction`, `createTemplateGlobalValue` for template extensions.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Revert client side paginated catalog table to using built in Material Table toolbar component
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Add check to make sure that the `--link` option for the `start` command is a valid workspace.
+18
View File
@@ -0,0 +1,18 @@
---
'@backstage/cli': minor
---
The `new` command is now powered by a new template system that allows you to define your own templates in a declarative way, as well as import existing templates from external sources. See the [CLI templates documentation](https://backstage.io/docs/tooling/cli/templates) for more information.
The following flags for the `new` command have been deprecated and will be removed in a future release:
- `--license=<license>`: Configure the global `license` instead.
- `--no-private`: Configure the global `private` instead.
- `--baseVersion=<version>`: Configure the global `version` instead.
- `--npmRegistry=<url>`: Configure the global `publishRegistry` instead.
- `--scope=<scope>`: Configure the global `namePrefix` and/or `namePluginInfix` instead.
As part of this change the template IDs and their options have changed. The following backwards compatibility mappings for the `--select` and `--option` flags are enabled when using the default set of templates, but they will also be removed in the future:
- `--select=plugin` is mapped to `--select=frontend-plugin` instead.
- `--option=id=<id>` is mapped to `--option=pluginId=<id>` instead.
+22
View File
@@ -223,6 +223,7 @@
"eleven-mice-sleep",
"fair-mangos-sleep",
"fair-rocks-dream",
"fast-rabbits-unite",
"few-ducks-cross",
"few-shrimps-kiss",
"fifty-humans-repair",
@@ -230,20 +231,29 @@
"fifty-trains-attend",
"fifty-turtles-count",
"fluffy-carrots-wink",
"fresh-bears-thank",
"gentle-actors-sleep",
"gold-cameras-battle",
"gold-rabbits-change",
"gorgeous-shoes-stare",
"green-jokes-provide",
"grumpy-crews-sneeze",
"happy-windows-shop",
"heavy-bobcats-search",
"heavy-olives-thank",
"hip-hotels-type",
"hot-mice-cry",
"hot-planes-hear",
"hungry-chefs-relax",
"hungry-cycles-hide",
"hungry-mirrors-sniff",
"hungry-rings-live",
"large-pans-brake",
"late-cycles-teach",
"little-mangos-reply",
"loud-baboons-relate",
"loud-walls-build",
"lucky-seas-live",
"many-goats-flash",
"metal-worms-live",
"neat-singers-rhyme",
@@ -260,7 +270,10 @@
"orange-brooms-lick",
"plenty-bobcats-drum",
"polite-apricots-exercise",
"polite-pumas-joke",
"pretty-apricots-poke",
"pretty-worms-walk",
"proud-dolls-behave",
"proud-dryers-act",
"proud-hornets-cheer",
"purple-years-cross",
@@ -276,15 +289,24 @@
"slow-bananas-beam",
"smart-ligers-sniff",
"soft-planets-mate",
"soft-seahorses-relate",
"tall-scissors-sip",
"tame-hats-shout",
"thirty-dancers-cheat",
"three-glasses-sell",
"tidy-forks-pay",
"tidy-oranges-allow",
"tiny-spoons-repeat",
"twelve-eyes-stare",
"two-schools-deny",
"warm-masks-ring",
"wet-bees-heal",
"wise-apes-juggle",
"wise-cycles-sparkle",
"witty-clocks-thank",
"witty-ducks-cross",
"witty-pets-march",
"yellow-rules-vanish",
"young-plants-report"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Support file globbing in fs:delete
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-notifications': patch
'@backstage/plugin-signals': patch
---
Added support for the new frontend system via `/alpha` export.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
Fix spacing props not being applied for custom values.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-node': patch
---
Added `scopeAlreadyGranted` property to `OAuthAuthenticatorRefreshInput`, signaling to the provider whether the requested scope has already been granted when persisting session scope.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Replaced dependency `node-libs-browser` with `node-stdlib-browser`
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/frontend-app-api': patch
---
Add support for defining multiple attachment points for extensions and blueprints.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-unprocessed-entities': patch
---
Added alpha support for the New Frontend System
+31
View File
@@ -0,0 +1,31 @@
---
'@backstage/backend-app-api': patch
---
Added a configuration to permit backend plugin failures on startup:
```yaml
backend:
...
startup:
plugins:
plugin-x:
onPluginBootFailure: continue
```
This configuration permits `plugin-x` to fail on startup. Omitting the
`onPluginBootFailure` configuration matches the previous behavior, wherein any
individual plugin failure aborts backend startup.
The default can also be changed, so that continuing on failure is the default
unless otherwise specified:
```yaml
backend:
startup:
default:
onPluginBootFailure: continue
plugins:
catalog:
onPluginBootFailure: abort
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': minor
---
Updated core CSS tokens and fixing the Button component accordingly.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder': patch
---
Added conditional rendering of `oneOf` output schemas on the Installed Actions page for scaffolder actions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Use template title for ongoing task page header
@@ -157,6 +157,7 @@ Gitiles
gitlab
GitLab
globals
globbing
Gource
Grafana
graphql
@@ -328,6 +329,7 @@ preconfigured
prepack
Preprarer
productional
projectSlug
Protobuf
proxied
proxying
@@ -425,6 +427,7 @@ subheaders
subkey
subpath
subroutes
substring
subtree
superfences
Superfences
@@ -3,8 +3,6 @@ on:
push:
branches:
- master
paths:
- '.github/workflows/sync_version-packages.yml'
concurrency:
group: sync-version-packages
+1 -1
View File
@@ -6,7 +6,7 @@
"build": "npm run build:css && next build",
"build:css": "node scripts/build-css.js",
"lint": "next lint",
"start": "next dev"
"start": "npm run build:css && next dev"
},
"dependencies": {
"@codemirror/lang-sass": "^6.0.2",
-6
View File
@@ -9,11 +9,6 @@ const source1 = path.join(__dirname, `${source}/core.css`);
const destination1 = path.join(__dirname, `${destination}/core.css`);
const source2 = path.join(__dirname, `${source}/components.css`);
const destination2 = path.join(__dirname, `${destination}/components.css`);
const source3 = path.join(
__dirname,
`../../packages/canon/.storybook/themes/backstage.css`,
);
const destination3 = path.join(__dirname, `${destination}/backstage.css`);
// Function to bundle and copy the CSS file
const bundleAndCopyFile = async (source, destination) => {
@@ -34,7 +29,6 @@ const bundleAndCopyFile = async (source, destination) => {
Promise.all([
bundleAndCopyFile(source1, destination1),
bundleAndCopyFile(source2, destination2),
bundleAndCopyFile(source3, destination3),
])
.then(() => {
// Add an empty line after all operations are complete - It looks better in the terminal :)
@@ -11,6 +11,7 @@ import {
ButtonResponsive,
} from '@/snippets/button';
import { buttonVariants } from '@/snippets/_snippets';
import { buttonPropDefs } from '../../../../../../packages/canon/src/components/Button/Button.props';
# Button
@@ -58,34 +59,7 @@ A button component that can be used to trigger actions.
## API reference
<PropsTable
data={{
size: {
type: ['small', 'medium'],
responsive: true,
},
variant: {
type: ['primary', 'secondary', 'tertiary'],
responsive: true,
},
disabled: {
type: 'boolean',
responsive: false,
},
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
<PropsTable data={buttonPropDefs} />
## Examples
@@ -186,7 +186,7 @@ are prefixed with `fg` to make it easier to identify.
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-text-primary</Chip>
<Chip head>--canon-fg-primary</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of `--canon-bg-app` or `--canon-bg-elevated`.
@@ -194,7 +194,7 @@ are prefixed with `fg` to make it easier to identify.
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-text-secondary</Chip>
<Chip head>--canon-fg-secondary</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of `--canon-bg-app` or `--canon-bg-elevated`.
@@ -280,7 +280,7 @@ low contrast to help as a separator with the different background colors.
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-border-focus</Chip>
<Chip head>--canon-border-pressed</Chip>
</Table.Cell>
<Table.Cell>
Used when the component is interactive and focused.
@@ -10,7 +10,7 @@
}
.icon path {
fill: var(--canon-fg-text-primary);
fill: var(--canon-fg-primary);
}
.content {
@@ -6,8 +6,9 @@ import { icons } from '../../../../packages/canon';
// Define a more specific type for the data object
type PropData = {
type: string | string[];
responsive: boolean;
values?: string | string[];
responsive?: boolean;
default?: string;
};
// Modify the PropsTable component to use the new type
@@ -16,17 +17,34 @@ export const PropsTable = <T extends Record<string, PropData>>({
}: {
data: T;
}) => {
const completeData = {
...data,
children: {
values: 'ReactNode',
responsive: false,
},
className: {
values: 'string',
responsive: false,
},
style: {
values: 'CSSProperties',
responsive: false,
},
};
return (
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
<Table.HeaderCell>Responsive</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
{Object.keys(data).map(n => (
{Object.keys(completeData).map(n => (
<Table.Row key={n}>
<Table.Cell>
<Chip head>{n}</Chip>
@@ -35,17 +53,22 @@ export const PropsTable = <T extends Record<string, PropData>>({
<div
style={{ display: 'flex', flexWrap: 'wrap', gap: '0.375rem' }}
>
{data[n].type === 'icon' ? (
{completeData[n].values === 'icon' ? (
Object.keys(icons).map(icon => <Chip key={icon}>{icon}</Chip>)
) : Array.isArray(data[n].type) ? (
data[n].type.map(t => <Chip key={t}>{t}</Chip>)
) : Array.isArray(completeData[n].values) ? (
completeData[n].values.map(t => <Chip key={t}>{t}</Chip>)
) : (
<Chip>{data[n].type}</Chip>
<Chip>{completeData[n].values}</Chip>
)}
</div>
</Table.Cell>
<Table.Cell>
<Chip>{data[n].responsive ? 'Yes' : 'No'}</Chip>
<Chip>
{completeData[n].default ? completeData[n].default : '-'}
</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{completeData[n].responsive ? 'Yes' : 'No'}</Chip>
</Table.Cell>
</Table.Row>
))}
-3
View File
@@ -11,9 +11,6 @@ export const ButtonPreview = () => {
<Button iconStart="cloud" variant="secondary">
Button
</Button>
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Flex>
);
};
+2 -2
View File
@@ -61,12 +61,12 @@ export class ImmediateEntityProvider implements EntityProvider {
this.entityValidator = entitySchemaValidator();
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.getProviderName} */
getProviderName() {
return `ImmediateEntityProvider`;
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.connect} */
async connect(connection: EntityProviderConnection) {
this.connection = connection;
}
+3 -3
View File
@@ -6,7 +6,7 @@ import {
import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-backend';
} from '@backstage/plugin-catalog-node';
import { Logger } from 'winston';
/**
@@ -41,12 +41,12 @@ import { Logger } from 'winston';
export class LoadTestingEntityProvider implements EntityProvider {
constructor(private readonly options: LoadTestingEntityProviderOptions) {}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.getProviderName} */
getProviderName() {
return `LoadTestingEntityProvider`;
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.connect} */
async connect(connection: EntityProviderConnection) {
const delayStartup = this.options.delayStartup ?? 10_000;
const logger = this.options.logger.child({
+3 -2
View File
@@ -239,7 +239,8 @@ privateKey: |
-----END RSA PRIVATE KEY-----
```
> [!WARNING]
> Sensitive information, such as private keys, should not be hard coded.
:::warning
Sensitive information, such as private keys, should not be hard coded.
:::
We recommend that this entire file should be a secret and stored as such in a secure storage solution like Vault, to ensure they are neither exposed nor misused. This example key part only shows the format on how to use the yaml | syntax to make sure that the key is valid.
+22 -119
View File
@@ -7,20 +7,17 @@ description: Installing Kubernetes plugin into Backstage
The Kubernetes feature is a plugin to Backstage, and it is exposed as a tab when
viewing entities in the software catalog.
If you haven't setup Backstage already, read the
[Getting Started](../../getting-started/index.md) guide.
If you haven't set up Backstage already, read the [Getting Started](../../getting-started/index.md) guide.
## Adding the Kubernetes frontend plugin
The first step is to add the Kubernetes frontend plugin to your Backstage
application.
The first step is to add the Kubernetes frontend plugin to your Backstage application.
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-kubernetes
```
Once the package has been installed, you need to import the plugin in your app
by adding the "Kubernetes" tab to the respective catalog pages.
Once the package has been installed, you need to import the plugin in your app by adding the "Kubernetes" tab to the respective catalog pages.
```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
/* highlight-add-next-line */
@@ -40,73 +37,17 @@ const serviceEntityPage = (
);
```
**Notes:**
:::note Note
- The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
That's it! But now, we need the Kubernetes Backend plugin for the frontend to
work.
:::
That's it! But now, we need the Kubernetes Backend plugin for the frontend to work.
## Adding Kubernetes Backend plugin
Navigate to `packages/backend` of your Backstage app, and install the
`@backstage/plugin-kubernetes-backend` package.
```bash title="From your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
```
Create a file called `kubernetes.ts` inside `packages/backend/src/plugins/` and
add the following:
```ts title="packages/backend/src/plugins/kubernetes.ts"
import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { CatalogClient } from '@backstage/catalog-client';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
const { router } = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
catalogApi,
discovery: env.discovery,
permissions: env.permissions,
}).build();
return router;
}
```
And import the plugin to `packages/backend/src/index.ts`. There are three lines
of code you'll need to add, and they should be added near similar code in your
existing Backstage backend.
```typescript title="packages/backend/src/index.ts"
// ..
/* highlight-add-next-line */
import kubernetes from './plugins/kubernetes';
async function main() {
// ...
/* highlight-add-next-line */
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
// ...
/* highlight-add-next-line */
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
}
```
That's it! The Kubernetes frontend and backend have now been added to your
Backstage app.
### New Backend System
To get the Kubernetes plugin install using the New Backend System you will need to do the following:
Run this command to add the package:
First, we need to add the backend package:
```bash title="From your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
@@ -126,6 +67,9 @@ backend.add(import('@backstage/plugin-kubernetes-backend'));
backend.start();
```
That's it! The Kubernetes frontend and backend have now been added to your
Backstage app.
### Custom cluster discovery
If either existing
@@ -133,22 +77,18 @@ If either existing
don't work for your use-case, it is possible to implement a custom
[KubernetesClustersSupplier](https://backstage.io/docs/reference/plugin-kubernetes-backend.kubernetesclusterssupplier).
Change the following in `packages/backend/src/plugins/kubernetes.ts`:
Here's a very simplified example:
```ts title="packages/backend/src/plugins/kubernetes.ts"
import {
/* highlight-add-next-line */
ClusterDetails,
KubernetesBuilder,
/* highlight-add-next-line */
KubernetesClustersSupplier,
} from '@backstage/plugin-kubernetes-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
/* highlight-add-next-line */
```ts title="packages/backend/src/index.ts"
import { createBackend } from '@backstage/backend-defaults';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { Duration } from 'luxon';
import {
ClusterDetails,
KubernetesClustersSupplier,
kubernetesClusterSupplierExtensionPoint,
} from '@backstage/plugin-kubernetes-node';
/* highlight-add-start */
export class CustomClustersSupplier implements KubernetesClustersSupplier {
constructor(private clusterDetails: ClusterDetails[] = []) {}
@@ -170,43 +110,6 @@ export class CustomClustersSupplier implements KubernetesClustersSupplier {
return this.clusterDetails;
}
}
/* highlight-add-end */
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
/* highlight-remove-next-line */
const { router } = await KubernetesBuilder.createBuilder({
/* highlight-add-next-line */
const builder = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
/* highlight-remove-next-line */
}).build();
/* highlight-add-start */
});
builder.setClusterSupplier(
CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })),
);
const { router } = await builder.build();
/* highlight-add-end */
// ..
return router;
}
```
### New Backend System Custom cluster discovery
To use Custom cluster discovery with the New Backend System you'll need to create a module and add it to your backend. Here's a very simplified example:
```ts title="packages/backend/src/index.ts"
import { createBackend } from '@backstage/backend-defaults';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { Duration } from 'luxon';
import { kubernetesClusterSupplierExtensionPoint } from '@backstage/plugin-kubernetes-node';
import { CustomClustersSupplier } from './path/to/class';
const backend = createBackend();
@@ -236,7 +139,7 @@ backend.start();
:::note Note
This example assumes the `CustomClustersSupplier` class is the same from the [previous example](#custom-cluster-discovery)
This example uses items from the `@backstage/plugin-kubernetes-node` and `luxon` packages, you'll need to add those for this example to work as is.
:::
@@ -17,7 +17,7 @@ Initial support for pagination of the `CatalogIndexPage` was added in v1.21.0 of
## Initially Selected Filter
By default the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
By default, the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
```tsx title="packages/app/src/App.tsx"
<Route
@@ -30,7 +30,7 @@ Possible options are: owned, starred, or all
## Initially Selected Kind
By default the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
By default, the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage initialKind="domain" />} />
@@ -70,9 +70,7 @@ The columns you see in the `CatalogIndexPage` were selected to be a good startin
Suppose we want to add a new User Email column to the `User` kind in the Catalog. We can do this by overriding the `columns` that we pass into the `CatalogIndexPage` component in our `App.tsx`. First, we need to match the entity kind that we want to override, and then define the columns to show:
```tsx title="packages/app/src/App.tsx"
{
/* highlight-add-start */
}
{/* prettier-ignore */ /* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
@@ -84,9 +82,7 @@ const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{
/* highlight-add-end */
}
{/* prettier-ignore */ /* highlight-add-end */}
```
Then, we can implement the `createUserEmailColumn` function and add it to the list of columns. `field` is used to access the data from the entity, while `render` lets us customize how we display the data:
@@ -107,7 +103,6 @@ const createUserEmailColumn = (): TableColumn<CatalogTableRow> => ({
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
@@ -391,13 +386,9 @@ export const EntitySecurityTierPicker = () => {
Now we can add the component to `CatalogIndexPage`:
```tsx title="packages/app/src/App.tsx"
{
/* highlight-add-start */
}
{/* prettier-ignore */ /* highlight-add-start */}
import { DefaultFilters } from '@backstage/plugin-catalog-react';
{
/* highlight-add-end */
}
{/* prettier-ignore */ /* highlight-add-end */}
const routes = (
<FlatRoutes>
@@ -495,8 +495,8 @@ want to have an isomorphic package that houses these types. Within the Backstage
main repo the package naming pattern of `<plugin>-common` is used for isomorphic
packages, and you may choose to adopt this pattern as well.
You can generate an isomorphic plugin package by running:`yarn new --select plugin-common`
or you can run `yarn new` and then select "plugin-common" from the list of options
You can generate an isomorphic plugin package by running: `yarn new` and then
select "plugin-common" from the list of options
There's at this point no existing templates for generating isomorphic plugins
using the `@backstage/cli`. Perhaps the simplest way to get started right now is
@@ -495,8 +495,8 @@ want to have an isomorphic package that houses these types. Within the Backstage
main repo the package naming pattern of `<plugin>-common` is used for isomorphic
packages, and you may choose to adopt this pattern as well.
You can generate an isomorphic plugin package by running:`yarn new --select plugin-common`
or you can run `yarn new` and then select "plugin-common" from the list of options
You can generate an isomorphic plugin package by running: `yarn new` and then
selecting "plugin-common" from the list of options
There's at this point no existing templates for generating isomorphic plugins
using the `@backstage/cli`. Perhaps the simplest way to get started right now is
@@ -520,7 +520,7 @@ validate entities of our new kind. Just like with the definition package, you
can find inspiration in for example the existing
[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-scaffolder-entity-model/src/processor/ScaffolderEntitiesProcessor.ts).
The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new --select backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`.
The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new` and select `backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`.
We also provide a high-level example of what a catalog process for a custom
entity might look like:
@@ -72,7 +72,7 @@ putting all extensions like this in a backend module package of their own in the
`plugins` folder of your Backstage repo:
```sh
yarn new --select backend-module --option id=catalog
yarn new --select backend-module --option pluginId=catalog
```
The class will have this basic structure:
@@ -650,7 +650,7 @@ putting all extensions like this in a backend module package of their own in the
`plugins` folder of your Backstage repo:
```sh
yarn new --select backend-module --option id=catalog
yarn new --select backend-module --option pluginId=catalog
```
The class will have this basic structure:
@@ -0,0 +1,502 @@
---
id: template-extensions
title: Template Extensions
description: Template extensions system
---
Backstage templating is powered by [Nunjucks][]. The basics:
# Template Filters
The [filter][] is a critical mechanism for the rendering of Nunjucks templates,
providing a means of transforming values in a familiar [piped][] fashion.
Template filters are functions that help you transform data, extract specific
information, and perform various operations in Scaffolder Templates.
## Built-in
Backstage provides out of the box the following set of "built-in" template
filters (to create your own custom filters, look to the section [Custom Filter](#custom-filter) hereafter):
### parseRepoUrl
The `parseRepoUrl` filter parses a repository URL into its constituent parts:
`owner`, repository name (`repo`), etc.
**Usage Example:**
```yaml
- id: log
name: Parse Repo URL
action: debug:log
input:
message: ${{ parameters.repoUrl | parseRepoUrl }}
```
- **Input**: `github.com?repo=backstage&owner=backstage`
- **Output**: "RepoSpec" (see [parseRepoUrl][])
### parseEntityRef
The `parseEntityRef` filter allows you to extract different parts of
an entity reference, such as the `kind`, `namespace`, and `name`.
**Usage example**
1. Without context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: ${{ parameters.owner | parseEntityRef }}
```
- **Input**: `group:techdocs`
- **Output**: [CompoundEntityRef][]
1. With context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: ${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
```
- **Input**: `techdocs`
- **Output**: [CompoundEntityRef][]
### pick
The `pick` filter allows you to select a specific property (e.g. `kind`, `namespace`, `name`) from an object.
**Usage Example**
```yaml
- id: log
name: Pick
action: debug:log
input:
message: ${{ parameters.owner | parseEntityRef | pick('name') }}
```
- **Input**: `{ kind: 'Group', namespace: 'default', name: 'techdocs' }`
- **Output**: `techdocs`
### projectSlug
The `projectSlug` filter generates a project slug from a repository URL.
**Usage Example**
```yaml
- id: log
name: Project Slug
action: debug:log
input:
message: ${{ parameters.repoUrl | projectSlug }}
```
- **Input**: `github.com?repo=backstage&owner=backstage`
- **Output**: `backstage/backstage`
# Template Globals
In addition to its powerful filtering functionality, the Nunjucks engine allows
access from the template expression context to specified globally-accessible
references. Backstage propagates this capability via the scaffolder backend
plugin, which we shall soon see in action.
# Customizing the templating environment
Custom plugins make it possible to install your own template extensions, which
may be any combination of filters, global functions and global values. With the
new backend you would use a scaffolder plugin module for this; later we will
demonstrate the analogous approach with the old backend.
## Streamlining Template Extension Module Creation with the Backstage CLI
The creation of a "template environment customization" module in Backstage can
be accelerated using the Backstage CLI.
Start by using the `yarn backstage-cli new` command to generate a scaffolder module. This command sets up the necessary boilerplate code, providing a smooth start:
```
$ yarn backstage-cli new
? What do you want to create?
> backend-module - A new backend module that extends an existing backend plugin with additional features
backend-plugin - A new backend plugin
plugin - A new frontend plugin
node-library - A new node-library package, exporting shared functionality for backend plugins and modules
plugin-common - A new isomorphic common plugin package
plugin-node - A new Node.js library plugin package
plugin-react - A new web library plugin package
scaffolder-module - An module exporting custom actions for @backstage/plugin-scaffolder-backend
```
When prompted, select the option to generate a backend module.
Since we want to extend the Scaffolder backend, enter `scaffolder` when prompted for the plugin to extend.
Next, enter a name for your module (relative to the generated `scaffolder-backend-module-` prefix),
and the CLI will generate the required files and directory structure.
## Writing your Module
Once the CLI has generated the essential structure for your new scaffolder
module, it's time to implement our template extensions. Here we'll demonstrate
how to create each of the supported extension types.
`src/module.ts` is where the magic happens. First we prepare to utilize the
associated (_**alpha** phase_) API extension point by adding:
```ts
import { scaffolderTemplatingExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
```
Considering the generated code, you may observe that everything rests on the
`createBackendModule` call, which after providing some minimal metadata to
establish context, specifies a `register` callback whose sole responsibility
here is to call, in turn, `registerInit` against the
`BackendModuleRegistrationPoints` argument it receives. Modify this call to
make the `scaffolderTemplatingExtensionPoint` available to the specified `init`
function:
```ts
register(reg) {
reg.registerInit({
deps: {
...,
templating: scaffolderTemplatingExtensionPoint,
},
async init({
...,
templating
}) {
...
};
});
};
```
Now we're ready to extend the scaffolder templating engine. For our purposes
here we'll drop everything in `module.ts`; use your own judgment as to the
organization of your real-world plugin modules.
### Custom Filter
In this contrived example we add a filter to test whether the incoming string
value contains (at least) a specified number of occurrences of a given
substring. We can easily define this by adding code to our `init` callback:
```ts
async init({
...,
templating,
}) {
...
templating.addTemplateFilters({
containsOccurrences: (arg: string, substring: string, times: number) => {
let pos = 0;
let count = 0;
while (pos < arg.length) {
pos = arg.indexOf(substring, pos);
if (pos < 0) {
break;
}
count++;
}
return count === times;
},
});
},
```
This demonstrates the bare minimum: a TypeScript `Record` of named template
filter implementations to register. However, by adopting an alternate structure
we can document our filter with additional metadata; to utilize this capability
we begin by adding a new import:
```ts
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
```
Then, update your `init` implementation to specify an array rather than an
object/record:
```ts
async init({
...,
templating,
}) {
...
templating.addTemplateFilters([
createTemplateFilter({
id: 'containsOccurrences',
description: 'determine whether filter input contains a substring N times',
filter: (arg: string, substring: string, times: number) => {
let pos = 0;
let count = 0;
while (pos < arg.length) {
pos = arg.indexOf(substring, pos);
if (pos < 0) {
break;
}
count++;
}
return count === times;
},
}),
]);
},
```
With this we have added a `description` to our filter, which helps a template
author to understand the filter's purpose.
#### Schema
To enhance our filter documentation further, we will specify its `schema`
using a callback against the [Zod][] schema declaration library:
```ts
createTemplateFilter({
id: 'containsOccurrences',
description: 'determine whether filter input contains a substring N times',
schema: z =>
z.function(
z.tuple([
z.string().describe('input'),
z.string().describe('substring whose occurrences to find'),
z.number().describe('number of occurrences to check for'),
]),
z.boolean(),
),
...,
}),
```
Because a filter is, in fact, a function, its schema is defined by generating a
[Zod function schema][zod-fn] against the parameter supplied to our schema
callback. A filter function is required to have at least one argument; in this
example, we have two additional arguments. But what if we modify our filter's
implementation function to make `times` optional? Code:
```ts
createTemplateFilter({
id: 'containsOccurrences',
...,
filter: (arg: string, substring: string, times?: number) => {
if (times === undefined) {
// note that, in real life, simply calling this function directly with Nunjucks would suffice rather than implementing a filter:
return arg.includes(substring);
}
// original implementation follows
...
},
}),
```
In this case we should modify our `schema`:
```ts
createTemplateFilter({
...,
schema: z =>
z.function(
z.tuple([
z.string().describe('input'),
z.string().describe('substring whose occurrences to find'),
z
.number()
.describe('number of occurrences to check for')
.optional(),
]),
z.boolean(),
),
...,
}),
```
#### Filter Example Documentation
Our filter documentation may benefit from examples which we specify thus:
```ts
createTemplateFilter({
...,
examples: [
{
description: 'Basic Usage',
example: `\
- name: Contains Occurrences
action: debug:log
input:
message: \${{ parameters.projectName | containsOccurrences('-', 2) }}
`,
notes: `\
- **Input**: \`foo-bar-baz\`
- **Output**: \`true\`
`,
},
{
description: 'Omitting Optional Parameter',
example: `\
- name: Contains baz
action: debug:log
input:
message: \${{ parameters.projectName | containsOccurrences('baz') | dump }}
`,
notes: `\
- **Input**: \`foo-bar\`
- **Output**: \`false\`
`,
},
],
}),
```
### Custom Global Function
In case your template needs access to a value generated from a function not
appropriately modeled as a filter, Nunjucks supports the direct invocation of
[global functions][global-fn]. We might, for example, add to `init`:
```ts
async init({
...,
templating,
}) {
...
templating.addTemplateGlobals({
now: () => new Date().toISOString(),
});
},
```
Here we have implemented a simple mechanism to obtain a timestamp (note that
because we can only pass JSON-compatible--or `undefined`--values we have chosen
to model a date/time as an ISO string) using a globally available function.
Again we have the option to make our global function self-documenting. Import:
```ts
import {
...,
createTemplateGlobalFunction,
} from '@backstage/plugin-scaffolder-node/alpha';
```
Then modify:
```ts
...
templating.addTemplateGlobals([
createTemplateGlobalFunction({
id: 'now',
description:
'obtain an ISO representation of the current date and time',
fn: () => new Date().toISOString(),
}),
]);
```
#### Schema
Declaring a global function schema is quite like the schema declaration for a
template filter:
```ts
createTemplateGlobal({
...,
schema: z => z.function().args().returns(z.string()),
...,
}),
```
#### Template Global Function Example Documentation
Again, this works in the same way as filter examples:
```ts
createTemplateGlobal({
...,
examples: [
{
description: 'Obtain the current date/time',
example: `\
- name: Log Timestamp
action: debug:log
input:
message: Current date/time: \${{ now() }}
`,
// optional `notes` omitted from this example
},
],
...,
}),
```
### Custom Global Value
Alternatively, your template may need access to a simple JSON value, which can
be registered in this manner:
```ts
async init({
...,
templating,
}) {
...
templating.addTemplateGlobals({
...,
preferredMetasyntacticIdentifier: 'foo',
});
},
```
Or the documenting form:
```ts
async init({
...,
templating,
}) {
...
templating.addTemplateGlobals([
...,
createTemplateGlobalValue({
id: 'preferredMetasyntacticVariable',
value: 'foo',
description:
'This description is as contrived as the global value it documents',
}),
]);
},
```
## Register Template Extensions with the Legacy Backend System
Users of the original Backstage backend can register template extensions by
specifying options to the scaffolder backend plugin's `createRouter` function
(customarily called in `packages/backend/src/plugins/scaffolder.ts`):
- `additionalTemplateFilters` - either of:
- object mapping filter name to implementation function, or
- array of documented template filters as returned by the
utility function `createTemplateFilter`
- `additionalTemplateGlobals` - either of:
- object mapping global name to value or function, or
- array of documented global functions and values as returned by the utility
functions `createTemplateGlobalFunction` and `createTemplateGlobalValue`
[nunjucks]: https://mozilla.github.io/nunjucks
[filter]: https://mozilla.github.io/nunjucks/templating.html#filters
[global-fn]: https://mozilla.github.io/nunjucks/templating.html#global-functions
[parseRepoUrl]: https://backstage.io/docs/reference/plugin-scaffolder-node.parserepourl
[CompoundEntityRef]: https://backstage.io/docs/reference/catalog-model.compoundentityref
[Zod]: https://zod.dev/
[zod-fn]: https://zod.dev/?id=functions
[piped]: https://en.wikipedia.org/wiki/Pipeline_(Unix)#Pipelines_in_command_line_interfaces
@@ -631,7 +631,7 @@ output:
## The templating syntax
You might have noticed variables wrapped in `${{ }}` in the examples. These are
You might have noticed expressions wrapped in `${{ }}` in the examples. These are
template strings for linking and gluing the different parts of the template
together. All the form inputs from the `parameters` section will be available by
using this template syntax (for example, `${{ parameters.firstName }}` inserts
@@ -704,219 +704,16 @@ You can read more about all the `inputs` and `outputs` defined in the actions in
code part of the `JSONSchema`, or you can read more about our
[built in actions](./builtin-actions.md).
## Built in Filters
### More about expressions
Template filters are functions that help you transform data, extract specific information,
and perform various operations in Scaffolder Templates.
The `${{ }}` constructs in your template are evaluated using the
powerful [Nunjucks templating engine](https://mozilla.github.io/nunjucks/).
To learn more about basic Nunjucks templating please see
[templating documentation](https://mozilla.github.io/nunjucks/templating.html).
This section introduces the built-in filters provided by Backstage and offers examples of
how to use them in the Scaffolder templates. It's important to mention that Backstage also leverages the
native filters from the Nunjucks library. For a complete list of these native filters and their usage,
refer to the [Nunjucks documentation](https://mozilla.github.io/nunjucks/templating.html#builtin-filters).
To create your own custom filters, look to the section [Custom Filters and Globals](#custom-filters-and-globals) hereafter.
### parseRepoUrl
The `parseRepoUrl` filter parse a repository URL into
its components, such as `owner`, repository `name`, and more.
**Usage Example:**
```yaml
- id: log
name: Parse Repo URL
action: debug:log
input:
extra: ${{ parameters.repoUrl | parseRepoUrl }}
```
- **Input**: `github.com?repo=backstage&org=backstage`
- **Output**: [RepoSpec](https://github.com/backstage/backstage/blob/v1.17.2/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts#L39)
### parseEntityRef
The `parseEntityRef` filter allows you to extract different parts of
an entity reference, such as the `kind`, `namespace`, and `name`.
**Usage example**
1. Without context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
extra: ${{ parameters.owner | parseEntityRef }}
```
- **Input**: `group:techdocs`
- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23)
2. With context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
extra: ${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
```
- **Input**: `techdocs`
- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23)
### pick
This `pick` filter allows you to select specific properties (`kind`, `namespace`, `name`) from an object.
**Usage Example**
```yaml
- id: log
name: Pick
action: debug:log
input:
extra: ${{ parameters.owner | parseEntityRef | pick('name') }}
```
- **Input**: `{ kind: 'Group', namespace: 'default', name: 'techdocs' }`
- **Output**: `techdocs`
### projectSlug
The `projectSlug` filter generates a project slug from a repository URL
**Usage Example**
```yaml
- id: log
name: Project Slug
action: debug:log
input:
extra: ${{ parameters.repoUrl | projectSlug }}
```
- **Input**: `github.com?repo=backstage&org=backstage`
- **Output**: `backstage/backstage`
## Custom Filters and Globals
You may wish to extend the filters and globals with your own custom ones. For example `${{ myGlobal | myFilter | myOtherFilter }}` or `${{ myFunctionGlobal(1,2) | myFilter }}`.
This can be achieved using the `additionalTemplateFilters` and `additionalTemplateGlobals` properties respectively.
These properties accept a `Record`
```ts title="plugins/scaffolder-backend/src/service/router.ts"
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
```
where the first parameter is the identifier of the filter or global and the second is a `TemplateFilter` or a `TemplateGlobal` respectively.
A `TemplateFilter` is a function which will be called using the previous `JsonValue` objects and may return a `JsonValue` object.
A `TemplateGlobal` can either be a function which will be called using the passed `JsonValue` objects and may return a `JsonValue` object or it can be a `JsonValue` object itself.
```ts title="plugins/scaffolder-node/src/types.ts"
export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
export type TemplateGlobal =
| ((...args: JsonValue[]) => JsonValue | undefined)
| JsonValue;
```
**Usage Example**
Given you want to have the following filters and globals available in you template:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: test
title: Test
spec:
owner: user:guest
type: service
steps:
- id: debug1
name: debug1
action: debug:log
input:
message: ${{ myGlobal | myFilter | myOtherFilter }}
- id: debug2
name: debug2
action: debug:log
input:
message: ${{ myFunctionGlobal(1,2) | myFilter }}
```
You will have to create a new [`BackendModule`](../../backend-system/architecture/06-modules.md) using the `scaffolderTemplatingExtensionPoint`.
Here is a very simplified example of how to do that:
```ts title="packages/backend-next/src/index.ts"
/* highlight-add-start */
import { scaffolderTemplatingExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
/* highlight-add-end */
/* highlight-add-start */
const scaffolderModuleCustomFilters = createBackendModule({
pluginId: 'scaffolder', // name of the plugin that the module is targeting
moduleId: 'custom-filters',
register(env) {
env.registerInit({
deps: {
scaffolder: scaffolderTemplatingExtensionPoint,
// ... and other dependencies as needed
},
async init({ scaffolder /* ..., other dependencies */ }) {
scaffolder.addTemplateGlobals({
myGlobal: () => 'myGlobal',
myFunctionGlobal: (...args: JsonValue[]) => args[0] + args[1],
});
scaffolder.addTemplateFilters({
myFilter: () => 'the value is this now',
myOtherFilter: (...args: JsonValue[]) => args.join(''),
});
},
});
},
});
/* highlight-add-end */
const backend = createBackend();
backend.add(import('@backstage/plugin-scaffolder-backend'));
/* highlight-add-next-line */
backend.add(scaffolderModuleCustomFilters);
```
If you still use the legacy backend system, then you will use the `createRouter()` function of the `Scaffolder plugin`
```ts title="packages/backend/src/plugins/scaffolder.ts"
export default async function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
...
return await createRouter({
logger,
config,
additionalTemplateFilters: {
<YOUR_FILTERS>
},
additionalTemplateGlobals: {
<YOUR_GLOBALS>
},
});
}
```
Note that additional template global functions are currently not supported in `fetch:template` (see #25445).
Information about Backstage's built-in Nunjucks extensions, as well as how to
create your own customizations, may be found at
[Template Extensions](./template-extensions.md).
## Template Editor
@@ -337,3 +337,21 @@ const routableExtension = createExtension({
},
});
```
## Multiple attachment points
For some cases it can be useful to attach extensions to multiple parents. An example of this are Scaffolder field extensions or TechDocs addons that are consumed by multiple extensions. Specifying multiple attachments is done by providing an array of attachment points to the `attachTo` property of the extension. Keep in mind that this increases the complexity of your extension tree and should only be done when necessary. The following example shows how to attach our example extension to multiple parents:
```tsx
const extension = createExtension({
name: 'my-extension',
attachTo: [
{ id: 'my-first-parent', input: 'content' },
{ id: 'my-second-parent', input: 'children' }, // The input names do not need to match
],
output: [coreExtensionData.reactElement],
factory() {
return [coreExtensionData.reactElement(<div>Hello World</div>)];
},
});
```
+1
View File
@@ -168,5 +168,6 @@ We recommend you read [Setting up authentication](./authentication.md) next.
If you want to read more about the database configuration, here are some helpful links:
- [Configuring Plugin Databases](../../tutorials/configuring-plugin-databases.md#privileges)
- [Manual Knex Rollback](../../tutorials/manual-knex-rollback.md)
- [Read more about Knex](http://knexjs.org/), the database wrapper that we use.
- [Install `pgAdmin` 4](https://www.pgadmin.org/), a helpful tool for querying your database.
@@ -160,3 +160,7 @@ export GLOBAL_AGENT_NO_PROXY=${NO_PROXY}
export YARN_HTTP_PROXY=${HTTP_PROXY} # optional
export YARN_HTTPS_PROXY=${HTTPS_PROXY} # optional
```
## Rollback migrations
In some cases you could need to downgrade Backstage instance due to some problem or maybe because you are using a test environment to validate the new version of Backstage. You can check the [Manual Rollback using Knex](../tutorials/manual-knex-rollback.md) guide to know how to rollback migrations using Knex.
+3 -1
View File
@@ -85,7 +85,7 @@ The parameters available are:
- **`scope`** _(optional)_:
`'global'` or `'local'`. Sets the scope of concurrency control.
_Note:_
:::note Note
- The path parameter follows the same rules as the search on Azure DevOps web interface. For more details visit the [official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops).
- To use branch parameters, it is necessary that the desired branch be added to the "Searchable branches" list within Azure DevOps Repositories. To do this, follow the instructions below:
@@ -97,6 +97,8 @@ _Note:_
5. In the window that appears, enter the name of the branch you want to add and click "Add".
6. The added branch will now appear in the "Searchable branches" list.
:::
It may take some time before the branch is indexed and searchable.
As this provider is not one of the default providers, you will first need to install
@@ -121,9 +121,14 @@ catalog:
workspace: workspace-name
```
> **Note:** It is possible but certainly not recommended to skip the provider ID level.
:::note Note
It is possible but certainly not recommended to skip the provider ID level.
> If you do so, `default` will be used as provider ID.
:::
- **`catalogPath`** _(optional)_:
Default: `/catalog-info.yaml`.
Path where to look for `catalog-info.yaml` files.
+2 -8
View File
@@ -10,18 +10,12 @@ Backstage repository.
## Creating a Backend Plugin
A new, bare-bones backend plugin package can be created by issuing the following
command in your Backstage repository root:
command in your Backstage repository root and selecting `backend-plugin`:
```sh
yarn new --select backend-plugin
yarn new
```
Please also see the `--help` flag for the `new` command for some
further options that are available, notably the `--scope` and `--no-private`
flags that control naming and publishing of the newly created package. Your repo
root `package.json` will probably also have some default values already set up
for these.
You will be asked to supply a name for the plugin. This is an identifier that
will be part of the NPM package name, so make it short and containing only
lowercase characters separated by dashes, for example `carmen`, if it's a
+3 -1
View File
@@ -15,9 +15,11 @@ invoking the
from the root of your project.
```bash
yarn new --select plugin
yarn new
```
And then select `frontend-plugin`.
![Example of output when creating a new plugin](../assets/getting-started/create-plugin_output.png)
This will create a new Backstage Plugin based on the ID that was provided. It
@@ -21,7 +21,8 @@ should have a separate package in a folder, which represents your plugin.
Example:
```
$ yarn new --select plugin
$ yarn new
# Select `frontend-plugin`
> ? Enter an ID for the plugin [required] my-plugin
> ? Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional]
File diff suppressed because it is too large Load Diff
+8 -10
View File
@@ -258,30 +258,28 @@ it is possible to pre-select what you want to create using the `--select` flag,
and provide options using `--option`, for example:
```bash
backstage-cli new --select plugin --option id=foo
backstage-cli new --select plugin --option pluginId=foo
```
This command is typically added as script in the root `package.json` to be
executed with `yarn new`, using options that are appropriate for the organization
that owns the app repo. For example you may have it set up like this:
executed with `yarn new`. For example you may have it set up like this:
```json
{
"scripts": {
"new": "backstage-cli new --scope internal --no-private --npm-registry https://acme.org/npm"
"new": "backstage-cli new"
}
}
```
The `new` command comes with a default collection of plugins/packages, however,
you can customize this list and even create your own CLI templates. For more
information see [CLI Templates](./04-templates.md).
```text
Usage: backstage-cli create [options]
Usage: backstage-cli new
Options:
--select <name> Select the thing you want to be creating upfront
--option <name>=<value> Pre-fill options for the creation process (default: [])
--scope <scope> The scope to use for new packages
--npm-registry <URL> The package registry to use for new packages
--no-private Do not mark new packages as private
-h, --help display help for command
```
+149
View File
@@ -0,0 +1,149 @@
---
id: templates
title: CLI Templates
description: Overview of the new CLI Declarative Templates
---
The behavior of the `backstage-cli new` command is configurable through your root `package.json`, and you can also create and add custom CLI templates to suit your needs.
## Basic Configuration
```json
{
"name": "root",
"backstage": {
"cli": {
"new": {
"globals": {
"license": "MIT",
"namePrefix": "@my-org/"
}
}
}
}
}
```
- `globals` - Configures input for all generated packages and plugins.
- `version` - Sets the value of the `version` field in `package.json` of all generated packages. Defaults to `0.1.0`.
- `license` - Sets the value of the `license` field in `package.json` of all generated packages. Defaults to `Apache-2.0`.
- `private` - Sets the value of the `private` field in `package.json` of all generated packages. Defaults to `true`.
- `publishRegistry` - Sets the value of the `publishConfig.registry` field in `package.json` of all generated packages.
- `namePrefix` - The prefix used to generate the full package name. Defaults to `@internal/`.
- `namePluginInfix` - The infix used to generate the full package name for plugin packages. Defaults to `plugin-`.
- `templates` - Specifies custom templates.
- See [Installing custom templates](#installing-custom-templates) and [Creating your own CLI templates](#creating-your-own-cli-templates) for more information.
The generated package name is based on the `namePrefix` and `namePluginInfix` globals, as well as the "base name" which is derived from the package role and user input. For plugin packages the final package name will be `<namePrefix><namePluginInfix><baseName>`, and for other packages it will be `<namePrefix><baseName>`.
For example, if you want your plugin frontend packages to end up with the name `@acme/backstage-plugin-<pluginId>`, you should use the following configuration:
```json
{
"name": "root",
"backstage": {
"cli": {
"new": {
"globals": {
"namePrefix": "@acme/",
"namePluginInfix": "backstage-plugin-"
}
}
}
}
}
```
## Installing custom templates
Custom templates can be installed from local directories. To install a template you add it to the `backstage.cli.new.templates` configuration array in your root `package.json`:
```json
{
"name": "root",
"backstage": {
"cli": {
"new": {
"templates": ["./templates/custom-plugin"]
}
}
}
}
```
Each entry in the `templates` array should be a relative path that points to a directory containing a `portable-template.yaml` file. If the path starts with `./` it will be used as is, otherwise it will be resolved as a module within `node_modules`.
When defining the `templates` array it will override the default set of templates. If you want to keep using one of the build-in templates in the Backstage CLI you can reference them directly within the CLI package. This following is the full list of built-in templates:
```json
{
"name": "root",
"backstage": {
"cli": {
"new": {
"templates": [
"@backstage/cli/templates/frontend-plugin",
"@backstage/cli/templates/backend-plugin",
"@backstage/cli/templates/backend-plugin-module",
"@backstage/cli/templates/plugin-web-library",
"@backstage/cli/templates/plugin-node-library",
"@backstage/cli/templates/plugin-common-library",
"@backstage/cli/templates/web-library",
"@backstage/cli/templates/node-library",
"@backstage/cli/templates/scaffolder-backend-module"
]
}
}
}
}
```
## Creating your own CLI templates
Each template lives in its own directory and must have a `portable-template.yaml` file that describes the template. The template directory can also contain any files that should be templated or copied to the generated package.
Start by creating `portable-template.yaml` in a new directory somewhere in your project, in this example we're using `./templates/custom-plugin/portable-template.yaml`:
```yaml title="in templates/custom-plugin/portable-template.yaml"
name: custom-plugin
role: frontend-plugin
description: Description of my CLI template # optional
values: # optional
pluginVar: '{{ camelCase pluginId }}Plugin'
```
The following properties are supported:
- `name` **(required)** - The name of your template, used by the user to select it.
- `role` **(required)** - The role of the template, similar to package role. See [Template Roles](#template-roles) for more details.
- `description` - A description of the type of package that this template produces.
- `values` - A map of additional values that will be present during templating. The values are themselves templated and can reference other values. If the key matches any of the user prompts, such as `pluginId`, the value will be used directly instead of prompting the user.
Next, add any other files you want to be part of the template to the same directory. All files will be copied as is, except any files with a `.hbs` extension. They will be treated as [Handlebars](https://handlebarsjs.com/) templates and will be rendered with the values from the `portable-template.yaml` file as well as additional prompts such as `pluginId`. For example, you could create a `src/index.ts` file with the following content:
```typescript title="in templates/custom-plugin/src/index.ts.hbs"
export function getPluginId() {
return '{{ pluginId }}';
}
```
If you'd like to see more examples, you can find all the default templates and their yaml files [here](https://github.com/backstage/backstage/tree/master/packages/cli/templates).
Once your template is ready, [add it to your config](#installing-custom-templates), and you should now be able to select it when running `yarn new`.
### Template Roles
The `role` property in the template yaml file is used to determine what input will be gathered for the template, as well as what actions will be taken after the new package has been created. The following roles are supported:
| Role | Prompts | Output Directory | Additional Actions |
| :----------------------- | :--------------------- | :--------------- | :-------------------------------------------------------------------------------- |
| `frontend-plugin` | `pluginId` | `plugins` | Add dependency to `packages/app` and entry to `packages/backend/src/App.tsx` |
| `frontend-plugin-module` | `pluginId`, `moduleId` | `plugins` | Add dependency to `packages/app` |
| `backend-plugin` | `pluginId` | `plugins` | Add dependency to `packages/backend` and entry to `packages/backend/src/index.ts` |
| `backend-plugin-module` | `pluginId`, `moduleId` | `plugins` | Add dependency to `packages/backend` and entry to `packages/backend/src/index.ts` |
| `web-library` | `name` | `packages` | none |
| `node-library` | `name` | `packages` | none |
| `common-library` | `name` | `packages` | none |
| `plugin-web-library` | `pluginId` | `plugins` | none |
| `plugin-node-library` | `pluginId` | `plugins` | none |
| `plugin-common-library` | `pluginId` | `plugins` | none |
@@ -181,3 +181,11 @@ GRANT SHOW DATABASES ON *.* TO some_user;
The mechanisms in this guide should help you tackle different database
deployment situations. Good luck!
## Further Reading
If you want to read more about the database configuration, here are some helpful links:
- [Manual Knex Rollback](./manual-knex-rollback.md)
- [Read more about Knex](http://knexjs.org/), the database wrapper that we use.
- [Install `pgAdmin` 4](https://www.pgadmin.org/), a helpful tool for querying your database.
+61
View File
@@ -0,0 +1,61 @@
---
id: manual-knex-rollback
title: Manual Rollback using Knex
# prettier-ignore
description: Guide on how to rollback Knex migrations.
---
The most common case to use Knex directly is when you want to **rollback a migration** that was applied when you upgraded your Backstage instance and want to downgrade due to some problem. You can use the `migrate:down` command to rollback a specific migration. You can also use the `migrate:rollback` command to rollback the last batch of migrations. This is necessary because Knex will mark migrations as corrupted if you try to downgrade your Backstage instance without the rollback. Be aware to run those commands in the new version of the Backstage instance, so you can avoid the corrupted migrations for lower versions.
You are likely to receive a message like this when you try a downgrade without the rollback:
```sh
Backend failed to start up Error: The migration directory is corrupt, the following files are missing: 20230428155633_sessions.js
```
Currently, we don't have a simple way to check which migrations and which plugins have been applied in the database, but you can follow this [issue](https://github.com/backstage/backstage/issues/22439) to get more information about this.
This guide covers a simple way to rollback migrations using Knex. We have plans to support this in Backstage's CLI ([issue](https://github.com/backstage/backstage/issues/6366)), but for now it is possible to use Knex CLI to manage migrations in necessary cases.
To start, you are going to need two things: database access and the plugin migrations directory you want to handle. We are going to use environment variables to access the database and we'll be using the `@backstage/plugin-catalog-backend` plugin as an example. In most cases, there is a `migrations` directory in the root of the plugin package, but you can check the `package.json` file to confirm the directory.
You can get more information about how Backstage handles Databases in [Configuring Plugin Databases](./configuring-plugin-databases.md). This tutorial follows the information in the [Knex migration guide](https://knexjs.org/guide/migrations.html), so you can get more details about the commands there.
You can interact with Knex running the commands below in the project root:
We want to check the migration status:
```sh
$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/
Using environment: production
Found 2 Completed Migration file/files.
20211229105307_init.js
20240113144027_assets-namespace.js
No Pending Migration files Found.
```
Now lets rollback a specific migration called `20240113144027_assets-namespace.js`:
```sh
$ node_modules/.bin/knex migrate:down 20240113144027_assets-namespace.js --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/
Using environment: production
Batch 2 rolled back the following migrations:
20240113144027_assets-namespace.js
```
Now we can check the migration status again to confirm the rollback:
```sh
$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/
Using environment: production
Found 1 Completed Migration file/files.
20211229105307_init.js
Found 1 Pending Migration file/files.
20240113144027_assets-namespace.js
```
Now lets use `migrate:currentVersion` which retrieves the current migration version. If there aren't any migrations run yet, it will return "none".
```sh
$ node_modules/.bin/knex migrate:currentVersion --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg
Using environment: production
Current Version: 20240113144027
```
+1 -1
View File
@@ -30,7 +30,7 @@ title: Adding Custom Plugin to Existing Monorepo App
# The Skeleton Plugin
1. Start by using the built-in creator. From the terminal and root of your
project run: `yarn new --select plugin`
project run: `yarn new` and select `frontend-plugin`.
1. Enter a plugin ID. I used `github-playground`
1. When the process finishes, let's start the backend:
`yarn --cwd packages/backend start`
@@ -181,13 +181,9 @@ It's crucial that you update to `RequirePermission` at the same time as you upda
When migrating over to React Router v6 stable, you might also see browser console warnings for the `Navigate` component. This will need to be wrapped up in a `Route` component with the `Navigate` component in the `element` prop.
```tsx
{
/* highlight-remove-next-line */
}
{/* prettier-ignore */ /* highlight-remove-next-line */}
<Navigate key="/" to="catalog" />;
{
/* highlight-add-next-line */
}
{/* prettier-ignore */ /* highlight-add-next-line */}
<Route path="/" element={<Navigate to="catalog" />} />;
```
+2
View File
@@ -479,6 +479,7 @@ module.exports = {
'tooling/cli/overview',
'tooling/cli/build-system',
'tooling/cli/commands',
'tooling/cli/templates',
{
type: 'category',
label: 'Local Development',
@@ -498,6 +499,7 @@ module.exports = {
Technical: [
'tutorials/quickstart-app-plugin',
'tutorials/configuring-plugin-databases',
'tutorials/manual-knex-rollback',
'tutorials/switching-sqlite-postgres',
'tutorials/using-backstage-proxy-within-plugin',
'tutorials/enable-public-entry',
+3 -3
View File
@@ -10796,11 +10796,11 @@ __metadata:
linkType: hard
"serialize-javascript@npm:^6.0.0, serialize-javascript@npm:^6.0.1":
version: 6.0.1
resolution: "serialize-javascript@npm:6.0.1"
version: 6.0.2
resolution: "serialize-javascript@npm:6.0.2"
dependencies:
randombytes: ^2.1.0
checksum: 3c4f4cb61d0893b988415bdb67243637333f3f574e9e9cc9a006a2ced0b390b0b3b44aef8d51c951272a9002ec50885eefdc0298891bc27eb2fe7510ea87dc4f
checksum: c4839c6206c1d143c0f80763997a361310305751171dd95e4b57efee69b8f6edd8960a0b7fbfc45042aadff98b206d55428aee0dc276efe54f100899c7fa8ab7
languageName: node
linkType: hard
+2
View File
@@ -71,6 +71,7 @@ nav:
- Builtin Actions: 'features/software-templates/builtin-actions.md'
- Writing Custom Actions: 'features/software-templates/writing-custom-actions.md'
- Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md'
- Template Extensions: 'features/software-templates/template-extensions.md'
- Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md'
- Dry Run Testing: 'features/software-templates/dry-run-testing.md'
- Backstage Search:
@@ -211,6 +212,7 @@ nav:
- Package Role Migration: 'tutorials/package-role-migration.md'
- Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md'
- Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md'
- Manual Rollback using Knex: 'tutorials/manual-knex-rollback.md'
- Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md'
- Using the Backstage Proxy from Within a Plugin: 'tutorials/using-backstage-proxy-within-plugin.md'
- Migration to Yarn 3: 'tutorials/yarn-migration.md'
+12 -2
View File
@@ -1,11 +1,21 @@
{
"name": "root",
"version": "1.36.0-next.2",
"version": "1.36.0-next.3",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage"
},
"backstage": {
"cli": {
"new": {
"globals": {
"private": false,
"namePrefix": "@backstage/"
}
}
}
},
"workspaces": {
"packages": [
"packages/*",
@@ -33,7 +43,7 @@
"lint:docs": "node ./scripts/check-docs-quality",
"lint:peer-deps": "backstage-repo-tools peer-deps",
"lint:type-deps": "backstage-repo-tools type-deps",
"new": "backstage-cli new --scope backstage --baseVersion 0.0.0 --no-private",
"new": "backstage-cli new",
"prepare": "husky",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
@@ -1,5 +1,13 @@
# app-next-example-plugin
## 0.0.20-next.3
### Patch Changes
- Updated dependencies
- @backstage/frontend-plugin-api@0.9.5-next.3
- @backstage/core-components@0.16.4-next.1
## 0.0.20-next.2
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "app-next-example-plugin",
"version": "0.0.20-next.2",
"version": "0.0.20-next.3",
"description": "Backstage internal example plugin",
"backstage": {
"role": "frontend-plugin",
+45
View File
@@ -1,5 +1,50 @@
# example-app-next
## 0.0.20-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.27.0-next.3
- @backstage/cli@0.30.0-next.3
- @backstage/plugin-scaffolder@1.28.0-next.3
- @backstage/frontend-plugin-api@0.9.5-next.3
- @backstage/frontend-app-api@0.10.5-next.3
- @backstage/core-compat-api@0.3.6-next.3
- @backstage/plugin-api-docs@0.12.4-next.3
- @backstage/plugin-catalog-graph@0.4.16-next.3
- @backstage/plugin-catalog-import@0.12.10-next.3
- @backstage/plugin-org@0.6.36-next.3
- @backstage/plugin-scaffolder-react@1.14.5-next.3
- @backstage/plugin-techdocs@1.12.3-next.3
- @backstage/plugin-user-settings@0.8.19-next.3
- @backstage/app-defaults@1.5.17-next.1
- @backstage/catalog-model@1.7.3
- @backstage/config@1.3.2
- @backstage/core-app-api@1.15.5-next.0
- @backstage/core-components@0.16.4-next.1
- @backstage/core-plugin-api@1.10.4-next.0
- @backstage/frontend-defaults@0.1.6-next.3
- @backstage/integration-react@1.2.4-next.0
- @backstage/theme@0.6.4-next.0
- @backstage/plugin-app@0.1.6-next.3
- @backstage/plugin-app-visualizer@0.1.16-next.3
- @backstage/plugin-auth-react@0.1.12-next.1
- @backstage/plugin-catalog-common@1.1.3
- @backstage/plugin-catalog-react@1.15.2-next.3
- @backstage/plugin-catalog-unprocessed-entities@0.2.14-next.1
- @backstage/plugin-home@0.8.5-next.3
- @backstage/plugin-kubernetes@0.12.4-next.3
- @backstage/plugin-kubernetes-cluster@0.0.22-next.3
- @backstage/plugin-notifications@0.5.2-next.2
- @backstage/plugin-permission-react@0.4.31-next.0
- @backstage/plugin-search@1.4.23-next.3
- @backstage/plugin-search-common@1.2.17
- @backstage/plugin-search-react@1.8.6-next.3
- @backstage/plugin-signals@0.0.16-next.1
- @backstage/plugin-techdocs-module-addons-contrib@1.1.21-next.1
- @backstage/plugin-techdocs-react@1.2.14-next.1
## 0.0.20-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-app-next",
"version": "0.0.20-next.2",
"version": "0.0.20-next.3",
"backstage": {
"role": "frontend"
},
+41
View File
@@ -1,5 +1,46 @@
# example-app
## 0.2.106-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.27.0-next.3
- @backstage/cli@0.30.0-next.3
- @backstage/plugin-scaffolder@1.28.0-next.3
- @backstage/frontend-app-api@0.10.5-next.3
- @backstage/plugin-api-docs@0.12.4-next.3
- @backstage/plugin-catalog-graph@0.4.16-next.3
- @backstage/plugin-catalog-import@0.12.10-next.3
- @backstage/plugin-org@0.6.36-next.3
- @backstage/plugin-scaffolder-react@1.14.5-next.3
- @backstage/plugin-techdocs@1.12.3-next.3
- @backstage/plugin-user-settings@0.8.19-next.3
- @backstage/app-defaults@1.5.17-next.1
- @backstage/catalog-model@1.7.3
- @backstage/config@1.3.2
- @backstage/core-app-api@1.15.5-next.0
- @backstage/core-components@0.16.4-next.1
- @backstage/core-plugin-api@1.10.4-next.0
- @backstage/integration-react@1.2.4-next.0
- @backstage/theme@0.6.4-next.0
- @backstage/plugin-auth-react@0.1.12-next.1
- @backstage/plugin-catalog-common@1.1.3
- @backstage/plugin-catalog-react@1.15.2-next.3
- @backstage/plugin-catalog-unprocessed-entities@0.2.14-next.1
- @backstage/plugin-devtools@0.1.24-next.3
- @backstage/plugin-home@0.8.5-next.3
- @backstage/plugin-kubernetes@0.12.4-next.3
- @backstage/plugin-kubernetes-cluster@0.0.22-next.3
- @backstage/plugin-notifications@0.5.2-next.2
- @backstage/plugin-permission-react@0.4.31-next.0
- @backstage/plugin-search@1.4.23-next.3
- @backstage/plugin-search-common@1.2.17
- @backstage/plugin-search-react@1.8.6-next.3
- @backstage/plugin-signals@0.0.16-next.1
- @backstage/plugin-techdocs-module-addons-contrib@1.1.21-next.1
- @backstage/plugin-techdocs-react@1.2.14-next.1
## 0.2.106-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-app",
"version": "0.2.106-next.2",
"version": "0.2.106-next.3",
"backstage": {
"role": "frontend"
},
+14
View File
@@ -1,5 +1,19 @@
# @backstage/backend-app-api
## 1.2.0-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-permission-node@0.8.8-next.2
- @backstage/backend-plugin-api@1.2.0-next.2
- @backstage/config-loader@1.9.6-next.0
- @backstage/plugin-auth-node@0.6.0-next.2
- @backstage/cli-common@0.1.15
- @backstage/config@1.3.2
- @backstage/errors@1.2.7
- @backstage/types@1.2.1
## 1.2.0-next.2
### Minor Changes
+26
View File
@@ -23,5 +23,31 @@ export interface Config {
include?: string[];
exclude?: string[];
};
startup?: {
default?: {
/**
* The default value for `onPluginBootFailure` if not specified for a particular plugin.
* This defaults to 'abort', which means `onPluginBootFailure: continue` must be specified
* for backend startup to continue on plugin boot failure. This can also be set to
* 'continue', which flips the logic for individual plugins so that they must be set to
* `onPluginBootFailure: abort` to be required.
*/
onPluginBootFailure?: 'continue' | 'abort';
};
plugins?: {
[pluginId: string]: {
/**
* Used to control backend startup behavior when this plugin fails to boot up. Setting
* this to `continue` allows the backend to continue starting up, even if this plugin
* fails. This can enable leaving a crashing plugin installed, but still permit backend
* startup, which may help troubleshoot data-dependent issues. Plugin failures for plugins
* set to `abort` are fatal (this is the default unless overridden by the `default`
* setting).
*/
onPluginBootFailure?: 'continue' | 'abort';
};
};
};
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-app-api",
"version": "1.2.0-next.2",
"version": "1.2.0-next.3",
"description": "Core API used by Backstage backend apps",
"backstage": {
"role": "node-library"
@@ -560,6 +560,95 @@ describe('BackendInitializer', () => {
);
});
it('should permit startup errors for plugins with onPluginBootFailure: continue', async () => {
const init = new BackendInitializer([
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
mockServices.rootConfig.factory({
data: {
backend: {
startup: { plugins: { test: { onPluginBootFailure: 'continue' } } },
},
},
}),
]);
init.add(
createBackendPlugin({
pluginId: 'test',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).resolves.not.toThrow();
});
it('should permit startup errors if the default onPluginBootFailure is continue', async () => {
const init = new BackendInitializer([
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
mockServices.rootConfig.factory({
data: {
backend: {
startup: { default: { onPluginBootFailure: 'continue' } },
},
},
}),
]);
init.add(
createBackendPlugin({
pluginId: 'test',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).resolves.not.toThrow();
});
it('should forward errors for plugins explicitly marked to abort when the default is continue', async () => {
const init = new BackendInitializer([
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
mockServices.rootConfig.factory({
data: {
backend: {
startup: {
default: { onPluginBootFailure: 'continue' },
plugins: { test: { onPluginBootFailure: 'abort' } },
},
},
},
}),
]);
init.add(
createBackendPlugin({
pluginId: 'test',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).rejects.toThrow(
"Plugin 'test' startup failed; caused by Error: NOPE",
);
});
it('should forward errors when multiple plugins fail to start', async () => {
const init = new BackendInitializer([]);
init.add(
@@ -24,6 +24,7 @@ import {
RootLifecycleService,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { ServiceOrExtensionPoint } from './types';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
@@ -321,9 +322,19 @@ export class BackendInitializer {
await this.#serviceRegistry.get(coreServices.rootLogger, 'root'),
);
const rootConfig = await this.#serviceRegistry.get(
coreServices.rootConfig,
'root',
);
// All plugins are initialized in parallel
const results = await Promise.allSettled(
allPluginIds.map(async pluginId => {
const isBootFailurePermitted = this.#getPluginBootFailurePredicate(
pluginId,
rootConfig,
);
try {
// Initialize all eager services
await this.#serviceRegistry.initializeEagerServicesWithScope(
@@ -392,8 +403,12 @@ export class BackendInitializer {
await lifecycleService.startup();
} catch (error: unknown) {
assertError(error);
initLogger.onPluginFailed(pluginId, error);
throw error;
if (isBootFailurePermitted) {
initLogger.onPermittedPluginFailure(pluginId, error);
} else {
initLogger.onPluginFailed(pluginId, error);
throw error;
}
}
}),
);
@@ -620,6 +635,20 @@ export class BackendInitializer {
}
}
}
#getPluginBootFailurePredicate(pluginId: string, config?: Config): boolean {
const defaultStartupBootFailureValue =
config?.getOptionalString(
'backend.startup.default.onPluginBootFailure',
) ?? 'abort';
const pluginStartupBootFailureValue =
config?.getOptionalString(
`backend.startup.plugins.${pluginId}.onPluginBootFailure`,
) ?? defaultStartupBootFailureValue;
return pluginStartupBootFailureValue === 'continue';
}
}
function toInternalBackendFeature(
@@ -28,6 +28,7 @@ export function createInitializationLogger(
): {
onPluginStarted(pluginId: string): void;
onPluginFailed(pluginId: string, error: Error): void;
onPermittedPluginFailure(pluginId: string, error: Error): void;
onAllStarted(): void;
} {
const logger = rootLogger?.child({ type: 'initialization' });
@@ -79,6 +80,13 @@ export function createInitializationLogger(
error,
);
},
onPermittedPluginFailure(pluginId: string, error: Error) {
starting.delete(pluginId);
logger?.error(
`Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin so startup will continue.`,
error,
);
},
onAllStarted() {
logger?.info(`Plugin initialization complete${getInitStatus()}`);
+23
View File
@@ -1,5 +1,28 @@
# @backstage/backend-defaults
## 0.8.0-next.3
### Patch Changes
- 3740229: In the different `UrlReadersService`, the `search` method have been updated to use the `readUrl` if the given URL doesn't contain a pattern.
For `UrlReaders` that didn't implement the `search` method, `readUrl` is now called internally and throws if the given URL doesn't contain a pattern.
- 72cddf2: Updated `PermissionsRegistryService` to use `PermissionResourceRef`s and added the `getPermissionRuleset` method.
- Updated dependencies
- @backstage/plugin-permission-node@0.8.8-next.2
- @backstage/backend-plugin-api@1.2.0-next.2
- @backstage/backend-app-api@1.2.0-next.3
- @backstage/cli-node@0.2.13-next.1
- @backstage/config-loader@1.9.6-next.0
- @backstage/plugin-auth-node@0.6.0-next.2
- @backstage/plugin-events-node@0.4.8-next.2
- @backstage/backend-dev-utils@0.1.5
- @backstage/cli-common@0.1.15
- @backstage/config@1.3.2
- @backstage/errors@1.2.7
- @backstage/integration@1.16.1
- @backstage/integration-aws-node@0.1.15
- @backstage/types@1.2.1
## 0.8.0-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-defaults",
"version": "0.8.0-next.2",
"version": "0.8.0-next.3",
"description": "Backend defaults used by Backstage backend apps",
"backstage": {
"role": "node-library"
@@ -1,5 +1,29 @@
# @backstage/backend-dynamic-feature-service
## 0.6.0-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-permission-node@0.8.8-next.2
- @backstage/plugin-catalog-backend@1.31.0-next.3
- @backstage/plugin-scaffolder-node@0.7.0-next.2
- @backstage/backend-defaults@0.8.0-next.3
- @backstage/backend-plugin-api@1.2.0-next.2
- @backstage/cli-node@0.2.13-next.1
- @backstage/config-loader@1.9.6-next.0
- @backstage/plugin-auth-node@0.6.0-next.2
- @backstage/plugin-events-backend@0.4.2-next.3
- @backstage/plugin-events-node@0.4.8-next.2
- @backstage/plugin-search-backend-node@1.3.8-next.2
- @backstage/cli-common@0.1.15
- @backstage/config@1.3.2
- @backstage/errors@1.2.7
- @backstage/types@1.2.1
- @backstage/plugin-app-node@0.1.30-next.2
- @backstage/plugin-permission-common@0.8.4
- @backstage/plugin-search-common@1.2.17
## 0.6.0-next.2
### Minor Changes
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-dynamic-feature-service",
"version": "0.6.0-next.2",
"version": "0.6.0-next.3",
"description": "Backstage dynamic feature service",
"backstage": {
"role": "node-library"
+40
View File
@@ -1,5 +1,45 @@
# example-backend-legacy
## 0.2.107-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog-node@1.16.0-next.3
- @backstage/plugin-permission-node@0.8.8-next.2
- @backstage/plugin-catalog-backend@1.31.0-next.3
- @backstage/plugin-scaffolder-backend@1.30.0-next.3
- @backstage/backend-defaults@0.8.0-next.3
- @backstage/backend-plugin-api@1.2.0-next.2
- @backstage/plugin-auth-backend@0.24.3-next.3
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.5-next.3
- @backstage/plugin-catalog-backend-module-unprocessed@0.5.5-next.3
- @backstage/plugin-kubernetes-backend@0.19.3-next.3
- @backstage/plugin-search-backend-module-catalog@0.3.1-next.3
- @backstage/plugin-search-backend-module-techdocs@0.3.6-next.3
- @backstage/plugin-techdocs-backend@1.11.6-next.3
- @backstage/plugin-permission-backend@0.5.54-next.2
- @backstage/plugin-search-backend@1.8.2-next.3
- @backstage/plugin-app-backend@0.4.5-next.2
- @backstage/plugin-auth-node@0.6.0-next.2
- @backstage/plugin-events-backend@0.4.2-next.3
- @backstage/plugin-events-node@0.4.8-next.2
- @backstage/plugin-proxy-backend@0.5.11-next.2
- @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.6-next.2
- @backstage/plugin-scaffolder-backend-module-gitlab@0.8.0-next.3
- @backstage/plugin-scaffolder-backend-module-rails@0.5.6-next.2
- @backstage/plugin-search-backend-module-elasticsearch@1.6.5-next.2
- @backstage/plugin-search-backend-module-explore@0.2.8-next.2
- @backstage/plugin-search-backend-module-pg@0.5.41-next.2
- @backstage/plugin-search-backend-node@1.3.8-next.2
- @backstage/plugin-signals-backend@0.3.1-next.2
- @backstage/catalog-client@1.9.1
- @backstage/catalog-model@1.7.3
- @backstage/config@1.3.2
- @backstage/integration@1.16.1
- @backstage/plugin-permission-common@0.8.4
- @backstage/plugin-signals-node@0.1.17-next.2
## 0.2.107-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend-legacy",
"version": "0.2.107-next.2",
"version": "0.2.107-next.3",
"backstage": {
"role": "backend"
},
@@ -1,5 +1,14 @@
# @backstage/backend-openapi-utils
## 0.5.0-next.3
### Patch Changes
- Updated dependencies
- @backstage/backend-plugin-api@1.2.0-next.2
- @backstage/errors@1.2.7
- @backstage/types@1.2.1
## 0.5.0-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-openapi-utils",
"version": "0.5.0-next.2",
"version": "0.5.0-next.3",
"description": "OpenAPI typescript support.",
"backstage": {
"role": "node-library"
+14
View File
@@ -1,5 +1,19 @@
# @backstage/backend-plugin-api
## 1.2.0-next.2
### Patch Changes
- 72cddf2: Updated `PermissionsRegistryService` to use `PermissionResourceRef`s and added the `getPermissionRuleset` method.
- Updated dependencies
- @backstage/plugin-permission-node@0.8.8-next.2
- @backstage/plugin-auth-node@0.6.0-next.2
- @backstage/cli-common@0.1.15
- @backstage/config@1.3.2
- @backstage/errors@1.2.7
- @backstage/types@1.2.1
- @backstage/plugin-permission-common@0.8.4
## 1.2.0-next.1
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-plugin-api",
"version": "1.2.0-next.1",
"version": "1.2.0-next.2",
"description": "Core API used by Backstage backend plugins",
"backstage": {
"role": "node-library"
+15
View File
@@ -1,5 +1,20 @@
# @backstage/backend-test-utils
## 1.3.0-next.3
### Patch Changes
- 72cddf2: Added the new `getPermissionRuleset` method to `mockServices.permissionsRegistry`.
- Updated dependencies
- @backstage/backend-defaults@0.8.0-next.3
- @backstage/backend-plugin-api@1.2.0-next.2
- @backstage/backend-app-api@1.2.0-next.3
- @backstage/plugin-auth-node@0.6.0-next.2
- @backstage/plugin-events-node@0.4.8-next.2
- @backstage/config@1.3.2
- @backstage/errors@1.2.7
- @backstage/types@1.2.1
## 1.3.0-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-test-utils",
"version": "1.3.0-next.2",
"version": "1.3.0-next.3",
"description": "Test helpers library for Backstage backends",
"backstage": {
"role": "node-library"
+38
View File
@@ -1,5 +1,43 @@
# example-backend
## 0.0.35-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-scaffolder-backend-module-github@0.6.0-next.2
- @backstage/plugin-permission-node@0.8.8-next.2
- @backstage/plugin-catalog-backend@1.31.0-next.3
- @backstage/plugin-scaffolder-backend@1.30.0-next.3
- @backstage/backend-defaults@0.8.0-next.3
- @backstage/backend-plugin-api@1.2.0-next.2
- @backstage/plugin-auth-backend@0.24.3-next.3
- @backstage/plugin-catalog-backend-module-backstage-openapi@0.4.5-next.3
- @backstage/plugin-catalog-backend-module-openapi@0.2.7-next.3
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.5-next.3
- @backstage/plugin-catalog-backend-module-unprocessed@0.5.5-next.3
- @backstage/plugin-kubernetes-backend@0.19.3-next.3
- @backstage/plugin-notifications-backend@0.5.2-next.3
- @backstage/plugin-search-backend-module-catalog@0.3.1-next.3
- @backstage/plugin-search-backend-module-techdocs@0.3.6-next.3
- @backstage/plugin-techdocs-backend@1.11.6-next.3
- @backstage/plugin-devtools-backend@0.5.2-next.3
- @backstage/plugin-permission-backend@0.5.54-next.2
- @backstage/plugin-permission-backend-module-allow-all-policy@0.2.5-next.2
- @backstage/plugin-search-backend@1.8.2-next.3
- @backstage/plugin-app-backend@0.4.5-next.2
- @backstage/plugin-auth-backend-module-github-provider@0.3.0-next.2
- @backstage/plugin-auth-backend-module-guest-provider@0.2.5-next.2
- @backstage/plugin-auth-node@0.6.0-next.2
- @backstage/plugin-events-backend@0.4.2-next.3
- @backstage/plugin-proxy-backend@0.5.11-next.2
- @backstage/plugin-search-backend-module-explore@0.2.8-next.2
- @backstage/plugin-search-backend-node@1.3.8-next.2
- @backstage/plugin-signals-backend@0.3.1-next.2
- @backstage/plugin-scaffolder-backend-module-notifications@0.1.7-next.2
- @backstage/catalog-model@1.7.3
- @backstage/plugin-permission-common@0.8.4
## 0.0.35-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.0.35-next.2",
"version": "0.0.35-next.3",
"backstage": {
"role": "backend"
},
+1 -6
View File
@@ -1,15 +1,12 @@
import React from 'react';
import type { Preview, ReactRenderer } from '@storybook/react';
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import { CanonProvider } from '../src/contexts/canon';
// Canon specific styles
import '../src/css/core.css';
import '../src/css/components.css';
// Custom themes
import './themes/backstage.css';
import { CanonProvider } from '../src/contexts/canon';
const preview: Preview = {
parameters: {
controls: {
@@ -79,8 +76,6 @@ const preview: Preview = {
themes: {
Light: 'light',
Dark: 'dark',
'Backstage Light': 'backstage-light',
'Backstage Dark': 'backstage-dark',
},
defaultTheme: 'Light',
}),
@@ -1,88 +0,0 @@
[data-theme-name='legacy'][data-theme='light'],
[data-theme-name='legacy'][data-theme='dark'] {
--canon-font-regular: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
.canon-Button {
border-radius: 60px;
font-weight: 400;
text-transform: uppercase;
font-size: 0.875rem;
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
line-height: 1.75;
border-radius: 4px;
background-color: #1f5493;
padding: 6px 16px;
box-shadow: none;
&:hover {
box-shadow: none;
}
&.canon-Button--variant-primary {
color: #fff;
background-color: rgb(21, 58, 102);
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2),
0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
&:hover {
box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12);
}
}
&.canon-Button--variant-secondary {
background-color: transparent;
border: 1px solid rgba(31, 84, 147, 0.5);
color: #1f5493;
&:hover {
border: 1px solid #1f5493;
background-color: rgba(31, 84, 147, 0.04);
}
}
&.canon-Button--variant-tertiary {
background-color: transparent;
border: none;
color: #1f5493;
}
}
}
[data-theme-name='legacy'][data-theme='light'] {
/* Colors */
--canon-bg-accent: #2e77d0;
--canon-bg: #f8f8f8;
--canon-bg-elevated: #fff;
--canon-bg: #f4f4f4;
/* Text colors */
--canon-fg-text-primary: #000;
--canon-fg-accent: #fff;
--canon-fg-text-secondary: #646464;
}
[data-theme-name='legacy'][data-theme='dark'] {
/* Colors */
--canon-bg-accent: #fff;
--canon-bg: #000;
--canon-bg-elevated: #121212;
--canon-bg: #1a1a1a;
/* Borders */
--canon-border: rgba(255, 255, 255, 0.2);
--canon-border-warning: #f50000;
--canon-border-danger: #f87503;
--canon-border-focus: #25d262;
/* States - Add more states */
--canon-fg-danger: #f50000;
/* Text colors */
--canon-fg-text-primary: #fff;
--canon-fg-accent: #000;
--canon-fg-text-secondary: #b3b3b3;
}
+10
View File
@@ -1,5 +1,15 @@
# @backstage/canon
## 0.1.0-next.2
### Minor Changes
- 8309bdb: Updated core CSS tokens and fixing the Button component accordingly.
### Patch Changes
- f44e5cf: Fix spacing props not being applied for custom values.
## 0.1.0-next.1
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/canon",
"version": "0.1.0-next.1",
"version": "0.1.0-next.2",
"backstage": {
"role": "web-library"
},
+27 -10
View File
@@ -121,6 +121,27 @@ export const Button: React_2.ForwardRefExoticComponent<
ButtonProps & React_2.RefAttributes<HTMLButtonElement>
>;
// @public (undocumented)
export type ButtonOwnProps = GetPropDefTypes<typeof buttonPropDefs>;
// @public (undocumented)
export const buttonPropDefs: {
variant: {
type: 'enum';
values: ('primary' | 'secondary')[];
className: string;
default: 'primary';
responsive: true;
};
size: {
type: 'enum';
values: ('small' | 'medium')[];
className: string;
default: 'medium';
responsive: true;
};
};
// @public
export interface ButtonProps {
// (undocumented)
@@ -134,15 +155,11 @@ export interface ButtonProps {
// (undocumented)
iconStart?: IconNames;
// (undocumented)
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
size?: ButtonOwnProps['size'];
// (undocumented)
style?: React.CSSProperties;
// (undocumented)
variant?:
| 'primary'
| 'secondary'
| 'tertiary'
| Partial<Record<Breakpoint, 'primary' | 'secondary' | 'tertiary'>>;
variant?: ButtonOwnProps['variant'];
}
// @public (undocumented)
@@ -670,14 +687,14 @@ export const marginPropDefs: (spacingValues: string[]) => {
type: 'enum | string';
values: string[];
className: string;
customProperties: ('--ml' | '--mr')[];
customProperties: '--mx'[];
responsive: true;
};
my: {
type: 'enum | string';
values: string[];
className: string;
customProperties: ('--mt' | '--mb')[];
customProperties: '--my'[];
responsive: true;
};
mt: {
@@ -732,14 +749,14 @@ export const paddingPropDefs: (spacingValues: string[]) => {
px: {
type: 'enum | string';
className: string;
customProperties: ('--pl' | '--pr')[];
customProperties: '--px'[];
values: string[];
responsive: true;
};
py: {
type: 'enum | string';
className: string;
customProperties: ('--pt' | '--pb')[];
customProperties: '--py'[];
values: string[];
responsive: true;
};
@@ -54,6 +54,13 @@ const Card = () => {
};
export const Default: Story = {
args: {
children: 'Hello World',
mb: '4',
},
};
export const Preview: Story = {
args: {
children: <Card />,
display: 'inline',
@@ -141,6 +148,25 @@ export const Padding: Story = {
<Box {...args}>Padding Left</Box>
</Box>
</Flex>
<Flex gap="4" align="center">
<Box p="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding</Box>
</Box>
<Box px="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding X</Box>
</Box>
<Box py="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding Y</Box>
</Box>
</Flex>
<Flex gap="4" align="center">
<Box pt="10px" pb="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding Top Bottom</Box>
</Box>
<Box pl="10px" pr="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding Left Right</Box>
</Box>
</Flex>
</Flex>
),
};
@@ -195,6 +221,35 @@ export const Margin: Story = {
</Box>
</Box>
</Flex>
<Flex align="center" gap="4">
<Box style={styleInsideBox}>
<Box m="10px" {...args}>
Custom Margin
</Box>
</Box>
<Box style={styleInsideBox}>
<Box mx="10px" {...args}>
Custom Margin X
</Box>
</Box>
<Box style={styleInsideBox}>
<Box my="10px" {...args}>
Custom Margin Y
</Box>
</Box>
</Flex>
<Flex align="center" gap="4">
<Box style={styleInsideBox}>
<Box mt="10px" mb="10px" {...args}>
Custom Margin Top Bottom
</Box>
</Box>
<Box style={styleInsideBox}>
<Box ml="10px" mr="10px" {...args}>
Custom Margin Left Right
</Box>
</Box>
</Flex>
</Flex>
),
};
+3 -2
View File
@@ -27,7 +27,7 @@ import { displayPropDefs } from '../../props/display.props';
/** @public */
export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
const { as = 'div', children } = props;
const { children } = props;
const propDefs = {
...spacingPropDefs,
@@ -37,9 +37,10 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
...displayPropDefs,
...boxPropDefs,
};
const { className, style } = extractProps(props, propDefs);
return createElement(as, {
return createElement(props.as || 'div', {
ref,
className: clsx('canon-Box', className),
style,
+1 -1
View File
@@ -1,4 +1,4 @@
.canon-Box {
font-family: var(--canon-font-regular);
color: var(--canon-fg-text-primary);
color: var(--canon-fg-primary);
}
@@ -0,0 +1,41 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { PropDef, GetPropDefTypes } from '../../props/prop-def';
/** @public */
export const buttonPropDefs = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
className: 'canon-Button--variant',
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
className: 'canon-Button--size',
default: 'medium',
responsive: true,
},
} satisfies {
variant: PropDef<'primary' | 'secondary'>;
size: PropDef<'small' | 'medium'>;
};
/** @public */
export type ButtonOwnProps = GetPropDefTypes<typeof buttonPropDefs>;
@@ -29,9 +29,14 @@ const meta = {
control: 'select',
options: ['small', 'medium'],
},
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
args: {
size: 'medium',
variant: 'primary',
},
} satisfies Meta<typeof Button>;
@@ -57,9 +62,6 @@ export const Variants: Story = {
<Button iconStart="cloud" variant="secondary">
Button
</Button>
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Flex>
),
};
@@ -94,7 +96,7 @@ export const FullWidth: Story = {
children: 'Button',
},
render: args => (
<Flex style={{ width: '300px' }}>
<Flex direction="column" gap="4" style={{ width: '300px' }}>
<Button {...args} iconStart="cloud" />
<Button {...args} iconEnd="chevronRight" />
<Button {...args} iconStart="cloud" iconEnd="chevronRight" />
@@ -115,7 +117,6 @@ export const Responsive: Story = {
variant: {
initial: 'primary',
sm: 'secondary',
md: 'tertiary',
},
size: {
xs: 'small',
@@ -124,7 +125,7 @@ export const Responsive: Story = {
},
};
const variants: string[] = ['primary', 'secondary', 'tertiary'];
const variants: string[] = ['primary', 'secondary'];
export const Playground: Story = {
args: {
@@ -137,6 +138,12 @@ export const Playground: Story = {
<Text>{variant}</Text>
{['small', 'medium'].map(size => (
<Flex align="center" key={size}>
<Button
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
>
Button
</Button>
<Button
iconStart="cloud"
variant={variant as ButtonProps['variant']}
@@ -160,6 +167,29 @@ export const Playground: Story = {
>
Button
</Button>
<Button
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
disabled
>
Button
</Button>
<Button
iconStart="cloud"
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
disabled
>
Button
</Button>
<Button
iconEnd="chevronRight"
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
disabled
>
Button
</Button>
</Flex>
))}
</Flex>
@@ -42,7 +42,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
return (
<button
{...rest}
ref={ref}
disabled={disabled}
className={clsx(
@@ -52,19 +51,11 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
className,
)}
style={style}
{...rest}
>
<span
className={[
'canon-Button--content',
iconStart && iconEnd ? 'canon-Button--icon-start-end' : '',
]
.filter(Boolean)
.join(' ')}
>
{iconStart && <Icon name={iconStart} />}
{children}
</span>
{iconEnd && <Icon name={iconEnd} />}
{iconStart && <Icon name={iconStart} className="canon-Button--icon" />}
{children}
{iconEnd && <Icon name={iconEnd} className="canon-Button--icon" />}
</button>
);
},
@@ -15,3 +15,5 @@
*/
export { Button } from './Button';
export type { ButtonProps } from './types';
export { buttonPropDefs } from './Button.props';
export type { ButtonOwnProps } from './Button.props';
+41 -30
View File
@@ -22,62 +22,73 @@
user-select: none;
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-bold);
font-size: var(--canon-font-size-3);
padding: 0;
transition: all 150ms ease;
cursor: pointer;
border-radius: var(--canon-radius-2);
gap: var(--canon-space-1_5);
&:disabled {
cursor: not-allowed;
}
}
.canon-Button--variant-primary {
background-color: var(--canon-bg-accent);
color: var(--canon-fg-accent);
background-color: var(--canon-bg-solid);
color: var(--canon-fg-solid);
transition: background-color 150ms ease;
&:hover {
background-color: transparent;
box-shadow: inset 0 0 0 1px var(--canon-border-focus);
color: var(--canon-fg-text-primary);
background-color: var(--canon-bg-solid-hover);
}
&:active {
background-color: var(--canon-bg-solid-pressed);
}
&:disabled {
background-color: var(--canon-bg-solid-disabled);
color: var(--canon-fg-solid-disabled);
}
}
.canon-Button--variant-secondary {
background-color: transparent;
background-color: var(--canon-bg-elevated);
box-shadow: inset 0 0 0 1px var(--canon-border);
color: var(--canon-fg-text-primary);
color: var(--canon-fg-primary);
transition: box-shadow 150ms ease;
&:hover {
box-shadow: inset 0 0 0 1px var(--canon-border-hover);
}
}
.canon-Button--variant-tertiary {
background-color: transparent;
color: var(--canon-fg-text-primary);
&:active {
box-shadow: inset 0 0 0 1px var(--canon-border-pressed);
}
&:hover {
color: var(--canon-fg-text-secondary);
&:disabled {
box-shadow: inset 0 0 0 1px var(--canon-border-disabled);
color: var(--canon-fg-disabled);
}
}
.canon-Button--size-small {
padding-left: 6px;
padding-right: 6px;
height: 32px;
}
.canon-Button--size-medium {
padding-left: 8px;
padding-right: 8px;
font-size: var(--canon-font-size-4);
padding: 0 var(--canon-space-3);
height: 40px;
}
.canon-Button--content {
display: flex;
align-items: center;
gap: var(--canon-space-3);
font-weight: var(--canon-font-weight-bold);
.canon-Button--size-small {
font-size: var(--canon-font-size-3);
padding: 0 var(--canon-space-2);
height: 32px;
}
.canon-Button--icon-start-end {
flex: 1;
.canon-Button--size-small .canon-Button--icon {
width: 1rem;
height: 1rem;
}
.canon-Button--size-medium .canon-Button--icon {
width: 1.5rem;
height: 1.5rem;
}
@@ -14,19 +14,16 @@
* limitations under the License.
*/
import { IconNames } from '../Icon';
import type { Breakpoint } from '../../types';
import type { ButtonOwnProps } from './Button.props';
/**
* Properties for {@link Button}
*
* @public
*/
export interface ButtonProps {
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
variant?:
| 'primary'
| 'secondary'
| 'tertiary'
| Partial<Record<Breakpoint, 'primary' | 'secondary' | 'tertiary'>>;
size?: ButtonOwnProps['size'];
variant?: ButtonOwnProps['variant'];
children: React.ReactNode;
className?: string;
disabled?: boolean;

Some files were not shown because too many files have changed in this diff Show More