Merge branch 'master' into caon-docs-compose-stories

This commit is contained in:
Charles de Dreuille
2025-03-23 09:29:14 +00:00
94 changed files with 1626 additions and 737 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fixed an issue causing the `repo lint` command to fail when the `--max-warnings` option was used.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-sentry': patch
---
Made "sentry:project:create" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-azure': patch
---
Made "publish:azure" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-react': patch
---
Scaffolding - Template card - button to show template entity detail
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-bitbucket': patch
---
Made "publish:bitbucket" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-github': patch
---
Added `validateLocationsExist` to the config definition where it was missing.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Made "github:autolinks:create" action idempotent
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-node': patch
---
Document the internal built-in filters, and ensure that the types are validated when using `createTemplateFilter` and `createTemplateGlobalFunction` from the `zod` schema.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Add `operation` to alpha `defaultEntityContentGroups`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch
---
Made "publish:bitbucketCloud" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Made "github:branch-protection:create" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Made "publish:github" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-notifications': patch
---
Made "notification:send" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Resolved a problem where the `start` command did not correctly handle multiple `--require` flags, ensuring all specified modules are now properly loaded.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home-react': patch
---
Fixes auto-hiding of content divider when title not specified
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Fix offset pagination to reset when updating filters in `useEntityList`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Made "github:actions:dispatch" action idempotent
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': minor
---
Internal update to move the `clean`, `pre/postpack` and `fix` commands into their own separate module.
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
cat ${{ github.event_path }} > event.json
- name: Upload Artifacts
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: preview-spec
path: |
+11 -11
View File
@@ -73,7 +73,7 @@ jobs:
run: yarn build:api-docs
- name: upload API reference
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: stable-reference
path: docs/reference/
@@ -89,7 +89,7 @@ jobs:
run: yarn docusaurus gen-api-docs all
- name: upload OpenAPI API docs
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: stable-openapi-docs
path: docs/**/*.api.mdx
@@ -129,7 +129,7 @@ jobs:
run: yarn build:api-docs
- name: upload API reference
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: next-reference
path: docs/reference/
@@ -146,7 +146,7 @@ jobs:
working-directory: storybook
- name: storybook upload
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: storybook
path: storybook/dist/
@@ -162,7 +162,7 @@ jobs:
run: yarn docusaurus gen-api-docs all
- name: upload OpenAPI API docs
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: next-openapi-docs
path: docs/**/*.api.mdx
@@ -211,13 +211,13 @@ jobs:
working-directory: microsite
- name: download stable reference
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: stable-reference
path: docs/reference
- name: download stable OpenAPI API docs
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: stable-openapi-docs
path: docs/**/*.api.mdx
@@ -235,7 +235,7 @@ jobs:
run: rm -r docs/reference
- name: clear OpenAPI reference
run: find . -name '*.api.mdx' -delete
run: find . -name '*.api.mdx' -type f -delete
# Next docs
- name: checkout master
@@ -248,13 +248,13 @@ jobs:
working-directory: microsite
- name: download next reference
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: next-reference
path: docs/reference
- name: download next OpenAPI API docs
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: next-openapi-docs
path: docs/**/*.api.mdx
@@ -263,7 +263,7 @@ jobs:
run: yarn build
working-directory: microsite
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
name: storybook
path: microsite/build/storybook
+48 -50
View File
@@ -1754,12 +1754,10 @@ __metadata:
languageName: node
linkType: hard
"detect-libc@npm:^1.0.3":
version: 1.0.3
resolution: "detect-libc@npm:1.0.3"
bin:
detect-libc: ./bin/detect-libc.js
checksum: daaaed925ffa7889bd91d56e9624e6c8033911bb60f3a50a74a87500680652969dbaab9526d1e200a4c94acf80fc862a22131841145a0a8482d60a99c24f4a3e
"detect-libc@npm:^2.0.3":
version: 2.0.3
resolution: "detect-libc@npm:2.0.3"
checksum: 2ba6a939ae55f189aea996ac67afceb650413c7a34726ee92c40fb0deb2400d57ef94631a8a3f052055eea7efb0f99a9b5e6ce923415daa3e68221f963cfc27d
languageName: node
linkType: hard
@@ -3424,91 +3422,91 @@ __metadata:
languageName: node
linkType: hard
"lightningcss-darwin-arm64@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-darwin-arm64@npm:1.29.1"
"lightningcss-darwin-arm64@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-darwin-arm64@npm:1.29.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"lightningcss-darwin-x64@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-darwin-x64@npm:1.29.1"
"lightningcss-darwin-x64@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-darwin-x64@npm:1.29.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"lightningcss-freebsd-x64@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-freebsd-x64@npm:1.29.1"
"lightningcss-freebsd-x64@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-freebsd-x64@npm:1.29.3"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"lightningcss-linux-arm-gnueabihf@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-linux-arm-gnueabihf@npm:1.29.1"
"lightningcss-linux-arm-gnueabihf@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-linux-arm-gnueabihf@npm:1.29.3"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"lightningcss-linux-arm64-gnu@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-linux-arm64-gnu@npm:1.29.1"
"lightningcss-linux-arm64-gnu@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-linux-arm64-gnu@npm:1.29.3"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"lightningcss-linux-arm64-musl@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-linux-arm64-musl@npm:1.29.1"
"lightningcss-linux-arm64-musl@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-linux-arm64-musl@npm:1.29.3"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"lightningcss-linux-x64-gnu@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-linux-x64-gnu@npm:1.29.1"
"lightningcss-linux-x64-gnu@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-linux-x64-gnu@npm:1.29.3"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"lightningcss-linux-x64-musl@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-linux-x64-musl@npm:1.29.1"
"lightningcss-linux-x64-musl@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-linux-x64-musl@npm:1.29.3"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"lightningcss-win32-arm64-msvc@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-win32-arm64-msvc@npm:1.29.1"
"lightningcss-win32-arm64-msvc@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-win32-arm64-msvc@npm:1.29.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"lightningcss-win32-x64-msvc@npm:1.29.1":
version: 1.29.1
resolution: "lightningcss-win32-x64-msvc@npm:1.29.1"
"lightningcss-win32-x64-msvc@npm:1.29.3":
version: 1.29.3
resolution: "lightningcss-win32-x64-msvc@npm:1.29.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"lightningcss@npm:^1.28.2":
version: 1.29.1
resolution: "lightningcss@npm:1.29.1"
version: 1.29.3
resolution: "lightningcss@npm:1.29.3"
dependencies:
detect-libc: ^1.0.3
lightningcss-darwin-arm64: 1.29.1
lightningcss-darwin-x64: 1.29.1
lightningcss-freebsd-x64: 1.29.1
lightningcss-linux-arm-gnueabihf: 1.29.1
lightningcss-linux-arm64-gnu: 1.29.1
lightningcss-linux-arm64-musl: 1.29.1
lightningcss-linux-x64-gnu: 1.29.1
lightningcss-linux-x64-musl: 1.29.1
lightningcss-win32-arm64-msvc: 1.29.1
lightningcss-win32-x64-msvc: 1.29.1
detect-libc: ^2.0.3
lightningcss-darwin-arm64: 1.29.3
lightningcss-darwin-x64: 1.29.3
lightningcss-freebsd-x64: 1.29.3
lightningcss-linux-arm-gnueabihf: 1.29.3
lightningcss-linux-arm64-gnu: 1.29.3
lightningcss-linux-arm64-musl: 1.29.3
lightningcss-linux-x64-gnu: 1.29.3
lightningcss-linux-x64-musl: 1.29.3
lightningcss-win32-arm64-msvc: 1.29.3
lightningcss-win32-x64-msvc: 1.29.3
dependenciesMeta:
lightningcss-darwin-arm64:
optional: true
@@ -3530,7 +3528,7 @@ __metadata:
optional: true
lightningcss-win32-x64-msvc:
optional: true
checksum: d1c4dba66dfe7f6a76532bdb84c35742bee61149550e5eb5b0e84e282f21aecd335f917ca9619bb7ca95fc1eb3092dc7e22f2c16b01e9a0ee472b76452343cce
checksum: 80cad5e1eb1c5eaae22ca6f17b127d65b5c93e859da124d57a90e256d7cb7802afa8dc7a6e1ec1e6aa208d0b23c8b9ec07078e69e61b10748bffdc8cadebed7e
languageName: node
linkType: hard
+77 -11
View File
@@ -7,7 +7,7 @@ description: Documentation for the Auditor service
## Overview
This document describes the Auditor Service, a software service designed to record and report on security-relevant events within an application. This service utilizes the `winston` library for logging and provides a flexible way to capture and format audit events.
This document describes the Auditor Service, a core service designed to record and report on security-relevant events within an application. By default, this service utilizes the `rootLogger` core service for logging.
## Key Features
@@ -16,15 +16,13 @@ This document describes the Auditor Service, a software service designed to reco
- Supports detailed metadata for each event.
- Offers success/failure reporting for events.
- Integrates with authentication and plugin services for enhanced context.
- Uses `winston` for flexible log formatting and transport.
- Provides a service factory for easy integration with Backstage plugins.
- Supports configurable log transports (console, file).
## How it Works
The Auditor Service defines a core class, `Auditor`, which implements the `AuditorService` interface. This class uses `winston` to log audit events with varying levels of severity and associated metadata. It also integrates with authentication and plugin services to capture actor details and plugin context.
The Auditor Service defines a class, `Auditor`, which implements the `AuditorService` interface. This class uses a `logFn` to log audit events with varying levels of severity and associated metadata. It also integrates with authentication and plugin services to capture actor details and plugin context.
The `auditorServiceFactory` creates an `Auditor` instance for the root context and provides a factory function for creating child loggers for individual plugins. This allows each plugin to have its own logger with inherited and additional metadata.
The `auditorServiceFactory` wraps the `rootLogger` core service and provides a factory function for creating child loggers for individual plugins. This allows each plugin to have its own logger with inherited and additional metadata.
## Usage Guidance
@@ -78,10 +76,78 @@ In this example, an audit event is created for each request to `/my-endpoint`. T
## Naming Conventions
When defining `eventId` and `subEventId` for your audit events, follow these guidelines:
When defining audit events, follow these guidelines to ensure consistency and clarity:
- Use kebab-case (e.g., `user-login`, `file-download`, `fetch`, `entity-create`, `entity-update`).
- The `eventId` represents a logical group of similar events or operations. For example, "fetch" could be used as an `eventId` encompassing various fetch methods like `by-id` or `by-location`.
- Use `subEventId` to further categorize events within a logical group. For example, if the `eventId` is "fetch", the `subEventId` could be "by-id" or "by-location" to specify the method used for fetching.
- Avoid redundant prefixes related to the plugin ID, as that context is already provided.
- Choose names that clearly and concisely describe the event being audited.
- **Use kebab-case:** Event IDs should be in kebab-case (e.g., `user-session`, `file-download`, `entity-fetch`).
- **`eventId` for Logical Grouping:** The `eventId` represents a broad category or logical group of related operations. For example, `entity-fetch` would group all entity retrieval events. `location-mutate` would group all actions that mutate a location.
- **`meta.queryType` (or related field) for Specific Actions within a Group:** Use a `meta` field (like `queryType`, `actionType` or similar) to specify the particular action or query that occurred within the broader `eventId` group.
- For instance, with `eventId: entity-fetch`, use `meta: { queryType: 'by-id' }` to represent fetching an entity by its ID. Other examples could be:
- `meta: { queryType: 'all' }` for fetching all entities.
- `meta: { queryType: 'by-query' }` for fetching entities by a query.
- `meta: { actionType: 'delete' }` for `eventId: entity-mutate` when an entity was deleted.
- `meta: { actionType: 'create' }` for `eventId: location-mutate` when a location was added.
- Use `meta` fields to add more context to the event being tracked.
- **Avoid Redundant Prefixes:** Do not include redundant prefixes related to the plugin ID in your event names. The plugin context is already provided separately.
- **Clear and Concise:** Choose names that clearly and concisely describe the event being audited.
## Common Meta Keys and Values
The following table details common keys found within the `meta` object of audit events and their formats:
| Key | Description | Format | Example(s) |
| ------------- | ------------------------------------------------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- |
| `queryType` | Specifies the type of query performed when fetching data. | A kebab-case string | `all`, `by-id`, `by-name`, `by-query`, `by-refs`, `ancestry`, `by-entity` |
| `actionType` | Specifies the type of action performed when modifying data. | A kebab-case string | `create`, `delete`, `refresh` |
| `entityRef` | The full reference of an entity, including kind, namespace, and name. | `[kind]:[namespace]/[name]` | `component:default/my-component`, `group:my-org/team-a` |
| `locationRef` | A specific reference to a location being operated on. | Any string representing the location. | `url:https://example.com/catalog-info.yaml`, `custom:default/my-location` |
| `uid` | The unique identifier of a location or other object involved in the operation. | Any valid unique ID string | `9a4e740b-e557-427f-b9f2-0d4f092b1c1e` |
By following these conventions, you create a more structured and informative audit trail that is easier to search, filter, and understand. This allows you to better group and understand the events being logged.
## Audit Event Examples
To illustrate how these naming conventions and the meta field are used in practice, the following examples demonstrate typical audit events for common operations.
**Typical Read Operation Example:**
For an operation that fetches all entities, a typical audit event would look like this:
```json
{
"eventId": "entity-fetch",
"meta": {
"queryType": "all"
}
...
}
```
**Typical Write Operation Example:**
For an operation that deletes an entity, a typical audit event would look like this:
```json
{
"eventId": "entity-mutate",
"meta": {
"actionType": "delete",
"uid": "some-entity-uid",
"entityRef": "component:default/petstore"
},
"severityLevel": "medium"
...
}
```
## Practical Examples for Auditor Implementation
To clarify how to utilize the Auditor feature effectively, we recommend exploring the Catalog Backend. It offers two valuable resources:
- **Code Implementation Example (createRouter.ts):**
- The [`createRouter.ts`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/createRouter.ts) file within the Catalog Backend showcases a practical integration of the `AuditorService` within a Backstage backend plugin.
- Specifically, the lines that demonstrate the creation of an audit event. This includes setting critical parameters such as `eventId` and `severityLevel`, as well as incorporating relevant metadata like `queryType` and `entityRef`.
- **Documentation Example (README.md):**
- The "Audit Events" section of the Catalog Backend's [`README.md`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/README.md#audit-events) provides a well-structured example of documenting emitted audit events.
- It illustrates how to detail various `eventId` values and their corresponding `meta` fields (e.g., `queryType`, `actionType`) for different plugin operations.
These examples provide both a code-level demonstration and a documentation guideline for effectively utilizing the `AuditorService` to manage audit events within your Backstage plugins.
@@ -561,7 +561,7 @@ catalogFilter:
metadata.annotations.github.com/team-slug: { exists: true }
```
#### Custom validation messages
### Custom validation messages
You may specify custom JSON Schema validation messages as supported by the
[ajv-errors](https://github.com/ajv-validator/ajv-errors) plugin library to [ajv](https://github.com/ajv-validator/ajv).
@@ -131,7 +131,7 @@ export class MyAwesomeApiClient implements MyAwesomeApi {
const proxyUri = `${await this.discoveryApi.getBaseUrl('proxy')}/<your-proxy-uri>`;
const resp = await this.fetchApi.fetch(`${proxyUri}${input}`, init);
if (!resp.ok) throw new Error(resp);
if (!resp.ok) throw new Error(resp.statusText);
return await resp.json();
}
@@ -151,8 +151,9 @@ export class MyAwesomeApiClient implements MyAwesomeApi {
}
```
> For more information on the DiscoveryApi check out the
> [docs](../reference/core-plugin-api.discoveryapi.md)
> Check out the docs for more information on the
> [DiscoveryApi](../reference/core-plugin-api.discoveryapi.md) or the
> [FetchApi](../reference/core-plugin-api.fetchapi.md)
## Bundling your ApiRef with your plugin
@@ -171,6 +172,7 @@ import {
createRoutableExtension,
createComponentExtension,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
//...
@@ -182,8 +184,12 @@ export const myCustomPlugin = createPlugin({
apis: [
createApiFactory({
api: myAwesomeApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new MyAwesomeApiClient({ discoveryApi }),
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) =>
new MyAwesomeApiClient({ discoveryApi, fetchApi }),
}),
],
});
+10
View File
@@ -0,0 +1,10 @@
---
title: Amplication Resources
author: Amplication
authorUrl: https://amplication.com
category: Development
description: A plugin that integrates Amplication services into Backstage, enabling seamless development and management of your applications.
documentation: https://docs.amplication.com/day-zero/integrate-with-backstage
iconUrl: /img/amplication_logo.png
npmPackageName: '@backstage-community/plugin-scaffolder-backend-module-amplication'
addedDate: '2025-03-07'
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

+3 -3
View File
@@ -1637,11 +1637,11 @@ __metadata:
linkType: hard
"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
version: 7.26.7
resolution: "@babel/runtime@npm:7.26.7"
version: 7.26.10
resolution: "@babel/runtime@npm:7.26.10"
dependencies:
regenerator-runtime: ^0.14.0
checksum: a1664a08f3f4854b895b540cca2f5f5c6c1993b5fb788c9615d70fc201e16bb254df8e0550c83eaf2749a14d87775e11a7c9ded6161203e9da7a4a323d546925
checksum: 22d2e0abb86e90de489ab16bb578db6fe2b63a88696db431198b24963749820c723f1982298cdbbea187f7b2b80fb4d98a514faf114ddb2fdc14a4b96277b955
languageName: node
linkType: hard
+3 -2
View File
@@ -200,10 +200,10 @@ Commands:
start [options]
build [options]
test
lint [options] [directories...]
clean
prepack
postpack
lint [options] [directories...]
help [command]
```
@@ -272,7 +272,7 @@ Options:
--check
--inspect [host]
--inspect-brk [host]
--require <path>
--require <path...>
--link <path>
-h, --help
```
@@ -452,6 +452,7 @@ Options:
--since <ref>
--successCache
--successCacheDir <path>
--max-warnings <number>
--fix
-h, --help
```
+10 -41
View File
@@ -35,6 +35,10 @@ import {
registerPackageCommands as registerPackageLintCommands,
registerRepoCommands as registerRepoLintCommands,
} from '../modules/lint';
import {
registerPackageCommands as registerMaintenancePackageCommands,
registerRepoCommands as registerMaintenanceRepoCommands,
} from '../modules/maintenance';
export function registerRepoCommand(program: Command) {
const command = program
@@ -44,30 +48,7 @@ export function registerRepoCommand(program: Command) {
registerRepoBuildCommands(command);
registerRepoTestCommands(command);
registerRepoLintCommands(command);
command
.command('fix')
.description('Automatically fix packages in the project')
.option(
'--publish',
'Enable additional fixes that only apply when publishing packages',
)
.option(
'--check',
'Fail if any packages would have been changed by the command',
)
.action(lazy(() => import('./repo/fix'), 'command'));
command
.command('clean')
.description('Delete cache and output directories')
.action(lazy(() => import('./repo/clean'), 'command'));
command
.command('list-deprecations')
.description('List deprecations')
.option('--json', 'Output as JSON')
.action(lazy(() => import('./repo/list-deprecations'), 'command'));
registerMaintenanceRepoCommands(command);
}
export function registerScriptCommand(program: Command) {
@@ -86,28 +67,17 @@ export function registerScriptCommand(program: Command) {
'--inspect-brk [host]',
'Enable debugger in Node.js environments, breaking before code starts',
)
.option('--require <path>', 'Add a --require argument to the node process')
.option(
'--require <path...>',
'Add a --require argument to the node process',
)
.option('--link <path>', 'Link an external workspace for module resolution')
.action(lazy(() => import('./start'), 'command'));
registerPackageBuildCommands(command);
registerPackageTestCommands(command);
registerMaintenancePackageCommands(command);
registerPackageLintCommands(command);
command
.command('clean')
.description('Delete cache directories')
.action(lazy(() => import('./clean/clean'), 'default'));
command
.command('prepack')
.description('Prepares a package for packaging before publishing')
.action(lazy(() => import('./pack'), 'pre'));
command
.command('postpack')
.description('Restores the changes made by the prepack command')
.action(lazy(() => import('./pack'), 'post'));
}
export function registerCommands(program: Command) {
@@ -153,7 +123,6 @@ export function registerCommands(program: Command) {
registerMigrateCommand(program);
registerBuildCommands(program);
registerInfoCommands(program);
program
.command('create-github-app <github-org>')
.description('Create new GitHub App in your organization.')
+5 -2
View File
@@ -39,7 +39,7 @@ export type RunBackendOptions = {
/** Whether to forward the --inspect-brk flag to the node process */
inspectBrkEnabled: boolean;
/** Additional module to require via the --require flag to the node process */
require?: string;
require?: string | string[];
/** An external linked workspace to override module resolution towards */
linkedWorkspace?: string;
};
@@ -107,7 +107,10 @@ export async function runBackend(options: RunBackendOptions) {
optionArgs.push(inspect);
}
if (options.require) {
optionArgs.push(`--require=${options.require}`);
const requires = [options.require].flat();
for (const r of requires) {
optionArgs.push(`--require=${r}`);
}
}
const userArgs = process.argv
+5 -1
View File
@@ -38,7 +38,7 @@ export default createCliPlugin({
);
command.option(
'--max-warnings <number>',
'Fail if more than this number of warnings. -1 allows warnings. (default: 0)',
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
);
command.description('Lint a package');
command.action(
@@ -76,6 +76,10 @@ export default createCliPlugin({
'--since <ref>',
'Only lint packages that changed since the specified ref',
);
command.option(
'--max-warnings <number>',
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
);
command.description('Lint a repository');
command.action(lazy(() => import('./commands/repo/lint'), 'command'));
@@ -111,6 +111,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
fix: Boolean(opts.fix),
format: opts.format as string | undefined,
shouldCache: Boolean(cacheContext),
maxWarnings: opts.maxWarnings ?? -1,
successCache: cacheContext?.entries,
rootDir: paths.targetRoot,
},
@@ -120,6 +121,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
shouldCache,
successCache,
rootDir,
maxWarnings,
}) => {
const { ESLint } = require('eslint') as typeof import('eslint');
const crypto = require('crypto') as typeof import('crypto');
@@ -131,7 +133,6 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
return async ({
fullDir,
relativeDir,
lintOptions,
parentHash,
}): Promise<{
relativeDir: string;
@@ -199,7 +200,6 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
await ESLint.outputFixes(results);
}
const maxWarnings = lintOptions?.maxWarnings ?? 0;
const ignoreWarnings = +maxWarnings === -1;
const resultText = formatter.format(results) as string;
+5 -1
View File
@@ -31,7 +31,7 @@ export function registerPackageCommands(command: Command) {
.option('--fix', 'Attempt to automatically fix violations')
.option(
'--max-warnings <number>',
'Fail if more than this number of warnings. -1 allows warnings. (default: 0)',
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
)
.description('Lint a package')
.action(lazy(() => import('./commands/package/lint'), 'default'));
@@ -62,6 +62,10 @@ export function registerRepoCommands(command: Command) {
'--successCacheDir <path>',
'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
)
.option(
'--max-warnings <number>',
'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
)
.option('--fix', 'Attempt to automatically fix violations')
.action(lazy(() => import('./commands/repo/lint'), 'command'));
}
@@ -0,0 +1,110 @@
/*
* 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 { Command } from 'commander';
import { createCliPlugin } from '../../wiring/factory';
import { lazy } from '../../lib/lazy';
export default createCliPlugin({
pluginId: 'maintenance',
init: async reg => {
reg.addCommand({
path: ['package', 'clean'],
description: 'Delete cache directories',
execute: async ({ args }) => {
const command = new Command();
const defaultCommand = command.action(
lazy(() => import('./commands/package/clean'), 'default'),
);
await defaultCommand.parseAsync(args, { from: 'user' });
},
});
reg.addCommand({
path: ['package', 'prepack'],
description: 'Prepares a package for packaging before publishing',
execute: async ({ args }) => {
const command = new Command();
const defaultCommand = command.action(
lazy(() => import('./commands/package/pack'), 'pre'),
);
await defaultCommand.parseAsync(args, { from: 'user' });
},
});
reg.addCommand({
path: ['package', 'postpack'],
description: 'Restores the changes made by the prepack command',
execute: async ({ args }) => {
const command = new Command();
const defaultCommand = command.action(
lazy(() => import('./commands/package/pack'), 'post'),
);
await defaultCommand.parseAsync(args, { from: 'user' });
},
});
reg.addCommand({
path: ['repo', 'fix'],
description: 'Automatically fix packages in the project',
execute: async ({ args }) => {
const command = new Command();
const defaultCommand = command
.option(
'--publish',
'Enable additional fixes that only apply when publishing packages',
)
.option(
'--check',
'Fail if any packages would have been changed by the command',
)
.action(lazy(() => import('./commands/repo/fix'), 'command'));
await defaultCommand.parseAsync(args, { from: 'user' });
},
});
reg.addCommand({
path: ['repo', 'clean'],
description: 'Delete cache and output directories',
execute: async ({ args }) => {
const command = new Command();
const defaultCommand = command.action(
lazy(() => import('./commands/repo/clean'), 'command'),
);
await defaultCommand.parseAsync(args, { from: 'user' });
},
});
reg.addCommand({
path: ['repo', 'list-deprecations'],
description: 'List deprecations',
execute: async ({ args }) => {
const command = new Command();
const defaultCommand = command
.option('--json', 'Output as JSON')
.action(
lazy(() => import('./commands/repo/list-deprecations'), 'command'),
);
await defaultCommand.parseAsync(args, { from: 'user' });
},
});
},
});
@@ -15,7 +15,7 @@
*/
import fs from 'fs-extra';
import { paths } from '../../lib/paths';
import { paths } from '../../../../lib/paths';
export default async function clean() {
await fs.remove(paths.resolveTarget('dist'));
@@ -17,11 +17,11 @@
import {
productionPack,
revertProductionPack,
} from '../modules/build/lib/packager/productionPack';
import { paths } from '../lib/paths';
} from '../../../../modules/build/lib/packager/productionPack';
import { paths } from '../../../../lib/paths';
import fs from 'fs-extra';
import { publishPreflightCheck } from '../lib/publishing';
import { createTypeDistProject } from '../lib/typeDistProject';
import { publishPreflightCheck } from '../../../../lib/publishing';
import { createTypeDistProject } from '../../../../lib/typeDistProject';
export const pre = async () => {
publishPreflightCheck({
@@ -19,7 +19,7 @@ import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { promisify } from 'util';
import { PackageGraph } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { paths } from '../../../../lib/paths';
const execFile = promisify(execFileCb);
@@ -24,8 +24,8 @@ import {
import { OptionValues } from 'commander';
import fs from 'fs-extra';
import { resolve as resolvePath, posix, relative as relativePath } from 'path';
import { paths } from '../../lib/paths';
import { publishPreflightCheck } from '../../lib/publishing';
import { paths } from '../../../../lib/paths';
import { publishPreflightCheck } from '../../../../lib/publishing';
/**
* A mutable object representing a package.json file with potential fixes.
@@ -19,7 +19,7 @@ import { ESLint } from 'eslint';
import { OptionValues } from 'commander';
import { relative as relativePath } from 'path';
import { PackageGraph } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { paths } from '../../../../lib/paths';
export async function command(opts: OptionValues) {
const packages = await PackageGraph.listTargetPackages();
@@ -0,0 +1,61 @@
/*
* 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 { Command } from 'commander';
import { lazy } from '../../lib/lazy';
export function registerPackageCommands(command: Command) {
command
.command('clean')
.description('Delete cache directories')
.action(lazy(() => import('./commands/package/clean'), 'default'));
command
.command('prepack')
.description('Prepares a package for packaging before publishing')
.action(lazy(() => import('./commands/package/pack'), 'pre'));
command
.command('postpack')
.description('Restores the changes made by the prepack command')
.action(lazy(() => import('./commands/package/pack'), 'post'));
}
export function registerRepoCommands(command: Command) {
command
.command('fix')
.description('Automatically fix packages in the project')
.option(
'--publish',
'Enable additional fixes that only apply when publishing packages',
)
.option(
'--check',
'Fail if any packages would have been changed by the command',
)
.action(lazy(() => import('./commands/repo/fix'), 'command'));
command
.command('clean')
.description('Delete cache and output directories')
.action(lazy(() => import('./commands/repo/clean'), 'command'));
command
.command('list-deprecations')
.description('List deprecations')
.option('--json', 'Output as JSON')
.action(lazy(() => import('./commands/repo/list-deprecations'), 'command'));
}
@@ -18,7 +18,7 @@ import {
fixPackageExports,
readFixablePackages,
writeFixedPackages,
} from '../../../commands/repo/fix';
} from '../../maintenance/commands/repo/fix';
export async function command() {
console.log(
+5
View File
@@ -71,6 +71,11 @@ export interface Config {
* Default: `/catalog-info.yaml`.
*/
catalogPath?: string;
/**
* (Optional) Whether to validate locations that exist before emitting them.
* Default: `false`.
*/
validateLocationsExist?: boolean;
/**
* (Optional) Filter configuration.
*/
@@ -99,6 +99,7 @@ export const defaultEntityContentGroups: {
documentation: string;
development: string;
deployment: string;
operation: string;
observability: string;
};
@@ -41,6 +41,7 @@ export const defaultEntityContentGroups = {
documentation: 'Documentation',
development: 'Development',
deployment: 'Deployment',
operation: 'Operation',
observability: 'Observability',
};
@@ -23,7 +23,7 @@ import {
identityApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { act, renderHook, waitFor } from '@testing-library/react';
import qs from 'qs';
import React, { PropsWithChildren } from 'react';
@@ -593,6 +593,7 @@ describe(`<EntityListProvider pagination={{ mode: 'offset' }} />`, () => {
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
filter: { kind: 'component' },
limit,
offset: 0,
orderFields,
fullTextFilter: {
term: '2',
@@ -620,6 +621,7 @@ describe(`<EntityListProvider pagination={{ mode: 'offset' }} />`, () => {
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
filter: { kind: 'component' },
limit,
offset: 0,
orderFields,
});
});
@@ -746,6 +748,7 @@ describe(`<EntityListProvider pagination={{ mode: 'offset' }} />`, () => {
expect(mockCatalogApi.queryEntities).toHaveBeenNthCalledWith(2, {
filter: { kind: 'api', 'spec.type': ['service'] },
limit,
offset: 0,
orderFields,
});
});
@@ -379,14 +379,19 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
// TODO(vinzscam): this is currently causing issues at page reload
// where the state is not kept. Unfortunately we need to rethink
// the way filters work in order to fix this.
setCursor(undefined);
if (paginationMode === 'cursor') {
setCursor(undefined);
} else if (paginationMode === 'offset') {
// Same thing with offset
setOffset(0);
}
setRequestedFilters(prevFilters => {
const newFilters =
typeof update === 'function' ? update(prevFilters) : update;
return { ...prevFilters, ...newFilters };
});
},
[],
[paginationMode],
);
const pageInfo = useMemo(() => {
+2 -1
View File
@@ -152,7 +152,8 @@ function CardExtension<T>(props: CardExtensionComponentProps<T>) {
}
const cardProps = {
...(title && { title, divider: !!title }),
divider: !!title,
...(title && { title }),
...(Settings && !isCustomizable
? {
action: (
@@ -174,37 +174,49 @@ export function createPublishAzureAction(options: {
const webApi = new WebApi(url, authHandler);
const client = await webApi.getGitApi();
const createOptions: GitRepositoryCreateOptions = { name: repo };
const returnedRepo = await client.createRepository(
createOptions,
project,
);
if (!returnedRepo) {
throw new InputError(
`Unable to create the repository with Organization ${organization}, Project ${project} and Repo ${repo}.
const { remoteUrl, repositoryId, repoContentsUrl } = await ctx.checkpoint(
{
key: `create.repo.${repo}`,
fn: async () => {
const returnedRepo = await client.createRepository(
createOptions,
project,
);
if (!returnedRepo) {
throw new InputError(
`Unable to create the repository with Organization ${organization}, Project ${project} and Repo ${repo}.
Please make sure that both the Org and Project are typed corrected and exist.`,
);
}
const remoteUrl = returnedRepo.remoteUrl;
);
}
if (!remoteUrl) {
throw new InputError(
'No remote URL returned from create repository for Azure',
);
}
const repositoryId = returnedRepo.id;
if (!returnedRepo.remoteUrl) {
throw new InputError(
'No remote URL returned from create repository for Azure',
);
}
if (!repositoryId) {
throw new InputError('No Id returned from create repository for Azure');
}
if (!returnedRepo.id) {
throw new InputError(
'No Id returned from create repository for Azure',
);
}
const repoContentsUrl = returnedRepo.webUrl;
if (!returnedRepo.webUrl) {
throw new InputError(
'No web URL returned from create repository for Azure',
);
}
if (!repoContentsUrl) {
throw new InputError(
'No web URL returned from create repository for Azure',
);
}
return {
remoteUrl: returnedRepo.remoteUrl,
repositoryId: returnedRepo.id,
repoContentsUrl: returnedRepo.webUrl,
};
},
},
);
const gitAuthorInfo = {
name: gitAuthorName
@@ -229,20 +241,30 @@ export function createPublishAzureAction(options: {
);
}
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
remoteUrl,
defaultBranch,
auth: auth,
logger: ctx.logger,
commitMessage: gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
const commitHash = await ctx.checkpoint({
key: `init.repo.and.push.${remoteUrl}`,
fn: async () => {
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(
ctx.workspacePath,
ctx.input.sourcePath,
),
remoteUrl,
defaultBranch,
auth: auth,
logger: ctx.logger,
commitMessage: gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
return commitResult?.commitHash;
},
});
ctx.output('commitHash', commitResult?.commitHash);
ctx.output('commitHash', commitHash);
ctx.output('remoteUrl', remoteUrl);
ctx.output('repoContentsUrl', repoContentsUrl);
ctx.output('repositoryId', repositoryId);
@@ -224,15 +224,19 @@ export function createPublishBitbucketCloudAction(options: {
const apiBaseUrl = integrationConfig.config.apiBaseUrl;
const { remoteUrl, repoContentsUrl } = await createRepository({
authorization,
workspace: workspace || '',
project,
repo,
repoVisibility,
mainBranch: defaultBranch,
description,
apiBaseUrl,
const { remoteUrl, repoContentsUrl } = await ctx.checkpoint({
key: `create.repo.${host}.${repo}`,
fn: async () =>
await createRepository({
authorization,
workspace: workspace || '',
project,
repo,
repoVisibility,
mainBranch: defaultBranch,
description,
apiBaseUrl,
}),
});
const gitAuthorInfo = {
@@ -272,20 +276,29 @@ export function createPublishBitbucketCloudAction(options: {
);
}
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
remoteUrl,
auth,
defaultBranch,
logger: ctx.logger,
commitMessage:
gitCommitMessage ||
config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
const commitHash = await ctx.checkpoint({
key: `init.repo.and.push${host}.${repo}`,
fn: async () => {
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(
ctx.workspacePath,
ctx.input.sourcePath,
),
remoteUrl,
auth,
defaultBranch,
logger: ctx.logger,
commitMessage:
gitCommitMessage ||
config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
return commitResult?.commitHash;
},
});
ctx.output('commitHash', commitResult?.commitHash);
ctx.output('commitHash', commitHash);
ctx.output('remoteUrl', remoteUrl);
ctx.output('repoContentsUrl', repoContentsUrl);
},
@@ -368,15 +368,19 @@ export function createPublishBitbucketAction(options: {
? createBitbucketCloudRepository
: createBitbucketServerRepository;
const { remoteUrl, repoContentsUrl } = await createMethod({
authorization,
workspace: workspace || '',
project,
repo,
repoVisibility,
mainBranch: defaultBranch,
description,
apiBaseUrl,
const { remoteUrl, repoContentsUrl } = await ctx.checkpoint({
key: `create.repo.${host}.${repo}`,
fn: async () =>
createMethod({
authorization,
workspace: workspace || '',
project,
repo,
repoVisibility,
mainBranch: defaultBranch,
description,
apiBaseUrl,
}),
});
const gitAuthorInfo = {
@@ -414,24 +418,33 @@ export function createPublishBitbucketAction(options: {
};
}
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
remoteUrl,
auth,
defaultBranch,
logger: ctx.logger,
commitMessage: gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
const commitHash = await ctx.checkpoint({
key: `init.repo.and.push${host}.${repo}`,
fn: async () => {
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(
ctx.workspacePath,
ctx.input.sourcePath,
),
remoteUrl,
auth,
defaultBranch,
logger: ctx.logger,
commitMessage: gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
signingKey: signCommit ? signingKey : undefined,
});
return commitResult?.commitHash;
},
});
if (enableLFS && host !== 'bitbucket.org') {
await performEnableLFS({ authorization, host, project, repo });
}
ctx.output('commitHash', commitResult?.commitHash);
ctx.output('commitHash', commitHash);
ctx.output('remoteUrl', remoteUrl);
ctx.output('repoContentsUrl', repoContentsUrl);
},
@@ -241,36 +241,43 @@ export function createPublishGithubAction(options: {
});
const client = new Octokit(octokitOptions);
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
client,
repo,
owner,
repoVisibility,
description,
homepage,
deleteBranchOnMerge,
allowMergeCommit,
allowSquashMerge,
squashMergeCommitTitle,
squashMergeCommitMessage,
allowRebaseMerge,
allowAutoMerge,
access,
collaborators,
hasProjects,
hasWiki,
hasIssues,
topics,
repoVariables,
secrets,
oidcCustomization,
customProperties,
subscribe,
ctx.logger,
);
const { remoteUrl, repoContentsUrl } = await ctx.checkpoint({
key: `create.github.repo.${owner}.${repo}`,
fn: async () => {
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
client,
repo,
owner,
repoVisibility,
description,
homepage,
deleteBranchOnMerge,
allowMergeCommit,
allowSquashMerge,
squashMergeCommitTitle,
squashMergeCommitMessage,
allowRebaseMerge,
allowAutoMerge,
access,
collaborators,
hasProjects,
hasWiki,
hasIssues,
topics,
repoVariables,
secrets,
oidcCustomization,
customProperties,
subscribe,
ctx.logger,
);
const remoteUrl = newRepo.clone_url;
const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`;
return {
remoteUrl: newRepo.clone_url,
repoContentsUrl: `${newRepo.html_url}/blob/${defaultBranch}`,
};
},
});
const commitResult = await initRepoPushAndProtect(
remoteUrl,
@@ -115,15 +115,20 @@ export function createGithubActionsDispatchAction(options: {
}),
);
await client.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: workflowId,
ref: branchOrTagName,
inputs: workflowInputs,
});
await ctx.checkpoint({
key: `create.workflow.dispatch.${owner}.${repo}.${workflowId}`,
fn: async () => {
await client.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: workflowId,
ref: branchOrTagName,
inputs: workflowInputs,
});
ctx.logger.info(`Workflow ${workflowId} dispatched successfully`);
ctx.logger.info(`Workflow ${workflowId} dispatched successfully`);
},
});
},
});
}
@@ -108,15 +108,20 @@ export function createGithubAutolinksAction(options: {
}),
);
await client.rest.repos.createAutolink({
owner,
repo,
key_prefix: keyPrefix,
url_template: urlTemplate,
is_alphanumeric: isAlphanumeric,
});
await ctx.checkpoint({
key: `create.auto.link.${owner}.${repo}`,
fn: async () => {
await client.rest.repos.createAutolink({
owner,
repo,
key_prefix: keyPrefix,
url_template: urlTemplate,
is_alphanumeric: isAlphanumeric,
});
ctx.logger.info(`Autolink reference created successfully`);
ctx.logger.info(`Autolink reference created successfully`);
},
});
},
});
}
@@ -130,29 +130,40 @@ export function createGithubBranchProtectionAction(options: {
});
const client = new Octokit(octokitOptions);
const repository = await client.rest.repos.get({
owner: owner,
repo: repo,
const defaultBranch = await ctx.checkpoint({
key: `read.default.branch.${owner}.${repo}`,
fn: async () => {
const repository = await client.rest.repos.get({
owner: owner,
repo: repo,
});
return repository.data.default_branch;
},
});
await enableBranchProtectionOnDefaultRepoBranch({
repoName: repo,
client,
owner,
logger: ctx.logger,
requireCodeOwnerReviews,
bypassPullRequestAllowances,
requiredApprovingReviewCount,
restrictions,
requiredStatusCheckContexts,
requireBranchesToBeUpToDate,
requiredConversationResolution,
requireLastPushApproval,
defaultBranch: branch ?? repository.data.default_branch,
enforceAdmins,
dismissStaleReviews,
requiredCommitSigning,
requiredLinearHistory,
await ctx.checkpoint({
key: `enable.branch.protection.${owner}.${repo}`,
fn: async () => {
await enableBranchProtectionOnDefaultRepoBranch({
repoName: repo,
client,
owner,
logger: ctx.logger,
requireCodeOwnerReviews,
bypassPullRequestAllowances,
requiredApprovingReviewCount,
restrictions,
requiredStatusCheckContexts,
requireBranchesToBeUpToDate,
requiredConversationResolution,
requireLastPushApproval,
defaultBranch: branch ?? defaultBranch,
enforceAdmins,
dismissStaleReviews,
requiredCommitSigning,
requiredLinearHistory,
});
},
});
},
});
@@ -133,9 +133,14 @@ export function createSendNotificationAction(options: {
};
try {
await notifications.send({
recipients: notificationRecipients,
payload,
await ctx.checkpoint({
key: `send.notification.${payload.title}`,
fn: async () => {
await notifications.send({
recipients: notificationRecipients,
payload,
});
},
});
} catch (e) {
ctx.logger.error(`Failed to send notification: ${e}`);
@@ -88,32 +88,41 @@ export function createSentryCreateProjectAction(options: { config: Config }) {
throw new InputError(`No valid sentry token given`);
}
const response = await fetch(
`https://sentry.io/api/0/teams/${organizationSlug}/${teamSlug}/projects/`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
const { result } = await ctx.checkpoint({
key: `create.project.${organizationSlug}.${teamSlug}`,
fn: async () => {
const response = await fetch(
`https://sentry.io/api/0/teams/${organizationSlug}/${teamSlug}/projects/`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);
const contentType = response.headers.get('content-type');
if (contentType !== 'application/json') {
throw new InputError(
`Unexpected Sentry Response Type: ${await response.text()}`,
);
}
const res = await response.json();
if (response.status !== 201) {
throw new InputError(`Sentry Response was: ${await res.detail}`);
}
return {
code: response.status,
result: res as { id: string },
};
},
);
const contentType = response.headers.get('content-type');
if (contentType !== 'application/json') {
throw new InputError(
`Unexpected Sentry Response Type: ${await response.text()}`,
);
}
const code = response.status;
const result = await response.json();
if (code !== 201) {
throw new InputError(`Sentry Response was: ${await result.detail}`);
}
});
ctx.output('id', result.id);
ctx.output('result', result);
+1 -1
View File
@@ -529,7 +529,7 @@ export interface RouterOptions {
// (undocumented)
additionalTemplateFilters?:
| Record<string, TemplateFilter_2>
| CreatedTemplateFilter[];
| CreatedTemplateFilter<any, any>[];
// (undocumented)
additionalTemplateGlobals?:
| Record<string, TemplateGlobal_2>
@@ -82,7 +82,7 @@ export const scaffolderPlugin = createBackendPlugin({
},
});
const additionalTemplateFilters: CreatedTemplateFilter[] = [];
const additionalTemplateFilters: CreatedTemplateFilter<any, any>[] = [];
const additionalTemplateGlobals: CreatedTemplateGlobal[] = [];
env.registerExtensionPoint(scaffolderTemplatingExtensionPoint, {
@@ -1,40 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseEntityRef } from '@backstage/catalog-model';
import { ScmIntegrations } from '@backstage/integration';
import type { JsonObject, JsonValue } from '@backstage/types';
import {
parseRepoUrl,
TemplateFilter,
} from '@backstage/plugin-scaffolder-node';
import get from 'lodash/get';
export default ({
integrations,
}: {
integrations: ScmIntegrations;
}): Record<string, TemplateFilter> => {
return {
parseRepoUrl: url => parseRepoUrl(url as string, integrations),
parseEntityRef: (ref: JsonValue, context?: JsonValue) =>
parseEntityRef(ref as string, context as JsonObject),
pick: (obj: JsonValue, key: JsonValue) => get(obj, key as string),
projectSlug: repoUrl => {
const { owner, repo } = parseRepoUrl(repoUrl as string, integrations);
return `${owner}/${repo}`;
},
};
};
@@ -0,0 +1,29 @@
/*
* 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 { ScmIntegrations } from '@backstage/integration';
import { createParseRepoUrl } from './parseRepoUrl';
import { parseEntityRef } from './parseEntityRef';
import { pick } from './pick';
import { createProjectSlug } from './projectSlug';
export const createDefaultFilters = (options: {
integrations: ScmIntegrations;
}) => [
createParseRepoUrl(options.integrations),
parseEntityRef,
pick,
createProjectSlug(options.integrations),
];
@@ -0,0 +1,48 @@
/*
* 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 { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
export const examples: TemplateFilterExample[] = [
{
description: 'Without context',
example: `\
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: \${{ parameters.owner | parseEntityRef }}
`,
notes: `\
- **Input**: \`group:techdocs\`
- **Output**: \`{"kind": "group", "namespace": "default", "name": "techdocs"}\`
`,
},
{
description: 'With context',
example: `\
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: \${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
`,
notes: `\
- **Input**: \`techdocs\`
- **Arguments:**: \`[{ "defaultKind": "group", "defaultNamespace": "another-namespace" }]\`
- **Output**: \`{"kind": "group", "namespace": "another-namespace", "name": "techdocs"}\`
`,
},
];
@@ -0,0 +1,63 @@
/*
* 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 { parseEntityRef as filter } from '@backstage/catalog-model';
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { examples } from './examples';
export const parseEntityRef = createTemplateFilter({
id: 'parseEntityRef',
description:
'Extracts the parts of an entity reference, such as the kind, namespace, and name.',
schema: z =>
z
.function()
.args(
z.union([
z.string().describe('compact entity reference'),
z
.object({
kind: z.string().optional(),
namespace: z.string().optional(),
name: z.string(),
})
.describe('`CompoundEntityRef`'),
]),
z
.object({
defaultKind: z
.string()
.describe('The default kind, if none is given in the reference'),
defaultNamespace: z
.string()
.describe(
'The default namespace, if none is given in the reference',
),
})
.partial()
.optional(),
)
.returns(
z
.object({
kind: z.string(),
namespace: z.string(),
name: z.string(),
})
.describe('`CompoundEntityRef`'),
),
examples,
filter,
});
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { parseEntityRef } from './filter';
@@ -0,0 +1,31 @@
/*
* 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 { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
export const examples: TemplateFilterExample[] = [
{
example: `\
- id: log
name: Parse Repo URL
action: debug:log
input:
message: \${{ parameters.repoUrl | parseRepoUrl }}`,
notes: ` - **Input**: \`github.com?repo=backstage&owner=backstage\`
- **Output**: \`{"host":"github.com","owner":"backstage","repo":"backstage"}\`
`,
},
];
@@ -0,0 +1,52 @@
/*
* 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 { ScmIntegrations } from '@backstage/integration';
import { parseRepoUrl } from '@backstage/plugin-scaffolder-node';
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { examples } from './examples';
export const createParseRepoUrl = (integrations: ScmIntegrations) =>
createTemplateFilter({
id: 'parseRepoUrl',
description:
'Parses a repository URL into its constituent parts: owner, repository name, etc.',
schema: z =>
z
.function()
.args(
z.string().describe('repo URL as collected from repository picker'),
)
.returns(
z
.object({
repo: z.string(),
host: z.string(),
})
.merge(
z
.object({
owner: z.string(),
organization: z.string(),
workspace: z.string(),
project: z.string(),
})
.partial(),
)
.describe('`RepoSpec`'),
),
examples,
filter: url => parseRepoUrl(url, integrations),
});
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { createParseRepoUrl } from './filter';
@@ -0,0 +1,31 @@
import { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
/*
* 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.
*/
export const examples: TemplateFilterExample[] = [
{
example: `\
- id: log
name: Pick
action: debug:log
input:
message: \${{ parameters.owner | parseEntityRef | pick('name') }}`,
notes: `\
- **Input**: \`{ kind: 'Group', namespace: 'default', name: 'techdocs'\` }
- **Output**: \`techdocs\`
`,
},
];
@@ -0,0 +1,31 @@
/*
* 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 { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { get as filter } from 'lodash';
import { examples } from './examples';
export const pick = createTemplateFilter({
id: 'pick',
description:
'Selects a specific property (e.g. kind, namespace, name) from an object.',
schema: z =>
z
.function()
.args(z.any(), z.string().describe('Property'))
.returns(z.any().describe('Selected property')),
examples,
filter,
});
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { pick } from './filter';
@@ -0,0 +1,32 @@
/*
* 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 { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
export const examples: TemplateFilterExample[] = [
{
example: `\
- id: log
name: Project Slug
action: debug:log
input:
message: \${{ parameters.repoUrl | projectSlug }}
`,
notes: `\
- **Input**: \`github.com?repo=backstage&owner=backstage\`
- **Output**: backstage/backstage
`,
},
];
@@ -0,0 +1,37 @@
/*
* 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 { ScmIntegrations } from '@backstage/integration';
import { parseRepoUrl } from '@backstage/plugin-scaffolder-node';
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { examples } from './examples';
export const createProjectSlug = (integrations: ScmIntegrations) =>
createTemplateFilter({
id: 'projectSlug',
description: 'Generates a project slug from a repository URL.',
schema: z =>
z
.function()
.args(
z.string().describe('repo URL as collected from repository picker'),
)
.returns(z.string()),
examples,
filter: repoUrl => {
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
return `${owner}/${repo}`;
},
});
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { createProjectSlug } from './filter';
@@ -29,7 +29,7 @@ import globby from 'globby';
import fs from 'fs-extra';
import { isBinaryFile } from 'isbinaryfile';
import { SecureTemplater } from '../../../../lib/templating/SecureTemplater';
import createDefaultFilters from '../../../../lib/templating/filters';
import { createDefaultFilters } from '../../../../lib/templating/filters/createDefaultFilters';
import { examples } from './template.examples';
import { convertFiltersToRecord } from '../../../../util/templating';
@@ -25,7 +25,7 @@ import {
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
import { SecureTemplater } from '../../../../lib/templating/SecureTemplater';
import createDefaultFilters from '../../../../lib/templating/filters';
import { createDefaultFilters } from '../../../../lib/templating/filters/createDefaultFilters';
import path from 'path';
import fs from 'fs-extra';
import { convertFiltersToRecord } from '../../../../util/templating';
@@ -55,7 +55,7 @@ import {
TemplateFilter,
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
import createDefaultFilters from '../../lib/templating/filters';
import { createDefaultFilters } from '../../lib/templating/filters/createDefaultFilters';
import { scaffolderActionRules } from '../../service/rules';
import { createCounterMetric, createHistogramMetric } from '../../util/metrics';
import { BackstageLoggerTransport, WinstonLogger } from './logger';
@@ -179,7 +179,7 @@ export interface RouterOptions {
taskBroker?: TaskBroker;
additionalTemplateFilters?:
| Record<string, TemplateFilter>
| CreatedTemplateFilter[];
| CreatedTemplateFilter<any, any>[];
additionalTemplateGlobals?:
| Record<string, TemplateGlobal>
| CreatedTemplateGlobal[];
@@ -28,14 +28,14 @@ import {
extractGlobalValueMetadata,
} from './templating';
import { JsonValue } from '@backstage/types';
import builtInFilters from '../lib/templating/filters';
import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
describe('templating utilities', () => {
describe('built-in filters', () => {
const integrations = ScmIntegrations.fromConfig(new ConfigReader({}));
const filters = builtInFilters({ integrations });
const filters = createDefaultFilters({ integrations });
it('generates equivalent filter metadata', () => {
const metadata = extractFilterMetadata(filters);
expect(metadata).toMatchObject(extractFilterMetadata(filters));
@@ -64,7 +64,7 @@ describe('templating utilities', () => {
z.string().describe('separator').optional(),
)
.returns(z.string()),
filter: (input: string, times: number, separator?: string) =>
filter: (input, times, separator) =>
Array(times)
.fill(input)
.join(separator ?? ''),
@@ -149,7 +149,7 @@ describe('templating utilities', () => {
id: 'respond',
schema: z =>
z.function().args(z.string().describe('prompt')).returns(z.string()),
fn: (prompt: string) =>
fn: prompt =>
prompt === 'knock knock' ? "who's there?" : "nobody's home",
}),
];
@@ -22,7 +22,7 @@ import {
CreatedTemplateGlobal,
CreatedTemplateGlobalFunction,
CreatedTemplateGlobalValue,
TemplateGlobalFunctionSchema,
ZodFunctionSchema,
} from '@backstage/plugin-scaffolder-node/alpha';
import { JsonValue } from '@backstage/types';
import { Schema } from 'jsonschema';
@@ -63,8 +63,12 @@ type ExportFilterSchema = {
* Converts a Zod function schema to JSON schema
*/
function convertZodFunctionToJsonSchema(
t: ReturnType<TemplateGlobalFunctionSchema<any, any>>,
t: ReturnType<ZodFunctionSchema<any, any>>,
): ExportFunctionSchema {
if (!('parameters' in t) || !('returnType' in t)) {
throw new Error('Invalid Zod function schema');
}
const args = (t.parameters().items as ZodType[]).map(
zt => zodToJsonSchema(zt) as Schema,
);
@@ -142,7 +146,9 @@ export function extractFilterMetadata(
if (filter.schema) {
metadata.schema = convertToFilterSchema(
convertZodFunctionToJsonSchema(filter.schema(z)),
convertZodFunctionToJsonSchema(
filter.schema(z) as z.ZodFunction<any, any>,
),
);
}
@@ -167,10 +173,7 @@ export function extractFilterMetadata(
*/
function isGlobalFunction(
global: CreatedTemplateGlobal,
): global is CreatedTemplateGlobalFunction<
TemplateGlobalFunctionSchema<any, any> | undefined,
any
> {
): global is CreatedTemplateGlobalFunction<any, any> {
return 'fn' in global;
}
+42 -68
View File
@@ -29,56 +29,31 @@ export type AutocompleteHandler = ({
// @alpha (undocumented)
export type CreatedTemplateFilter<
TSchema extends
| TemplateFilterSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter = TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter,
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
> = {
id: string;
description?: string;
examples?: TemplateFilterExample[];
schema?: TSchema;
filter: TFilterSchema;
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
filter: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
};
// @alpha (undocumented)
export type CreatedTemplateGlobal =
| CreatedTemplateGlobalValue
| CreatedTemplateGlobalFunction<unknown, unknown>;
| CreatedTemplateGlobalFunction<any, any>;
// @alpha (undocumented)
export type CreatedTemplateGlobalFunction<
TSchema extends
| TemplateGlobalFunctionSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<
TemplateGlobal,
JsonValue
> = TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<TemplateGlobal, JsonValue>,
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
> = {
id: string;
description?: string;
examples?: TemplateGlobalFunctionExample[];
schema?: TSchema;
fn: TFilterSchema;
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
fn: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
};
// @alpha (undocumented)
@@ -90,23 +65,27 @@ export type CreatedTemplateGlobalValue<T extends JsonValue = JsonValue> = {
// @alpha
export const createTemplateFilter: <
TSchema extends TemplateFilterSchema<any, any> | undefined,
TFunctionSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: (arg: JsonValue, ...rest: JsonValue[]) => JsonValue | undefined,
>(
filter: CreatedTemplateFilter<TSchema, TFunctionSchema>,
) => CreatedTemplateFilter<unknown, unknown>;
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
>(options: {
id: string;
description?: string;
examples?: TemplateFilterExample[];
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
filter: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
}) => CreatedTemplateFilter<TFunctionArgs, TReturnType>;
// @alpha
export const createTemplateGlobalFunction: <
TSchema extends TemplateGlobalFunctionSchema<any, any> | undefined,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: (...args: JsonValue[]) => JsonValue | undefined,
>(
fn: CreatedTemplateGlobalFunction<TSchema, TFilterSchema>,
) => CreatedTemplateGlobalFunction<any, any>;
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
>(options: {
id: string;
description?: string;
examples?: TemplateGlobalFunctionExample[];
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
fn: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
}) => CreatedTemplateGlobalFunction<TFunctionArgs, TReturnType>;
// @alpha
export const createTemplateGlobalValue: (
@@ -156,7 +135,9 @@ export const scaffolderTaskBrokerExtensionPoint: ExtensionPoint<ScaffolderTaskBr
export interface ScaffolderTemplatingExtensionPoint {
// (undocumented)
addTemplateFilters(
filters: Record<string, TemplateFilter_2> | CreatedTemplateFilter[],
filters:
| Record<string, TemplateFilter_2>
| CreatedTemplateFilter<any, any>[],
): void;
// (undocumented)
addTemplateGlobals(
@@ -194,16 +175,6 @@ export type TemplateFilterExample = {
notes?: string;
};
// @alpha (undocumented)
export type TemplateFilterSchema<
Args extends z.ZodTuple<
| [z.ZodType<JsonValue>]
| [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
// @public (undocumented)
export type TemplateGlobal =
| ((...args: JsonValue[]) => JsonValue | undefined)
@@ -216,15 +187,6 @@ export type TemplateGlobalFunctionExample = {
notes?: string;
};
// @alpha (undocumented)
export type TemplateGlobalFunctionSchema<
Args extends z.ZodTuple<
[] | [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
// @alpha
export interface WorkspaceProvider {
// (undocumented)
@@ -244,5 +206,17 @@ export interface WorkspaceProvider {
}): Promise<void>;
}
// @alpha (undocumented)
export type ZodFunctionSchema<
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
> = (
zod: typeof z,
) =>
| z.ZodFunction<z.ZodTuple<TFunctionArgs, null>, TReturnType>
| z.ZodType<
(...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>
>;
// (No @packageDocumentation comment for this package)
```
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
import { CreatedTemplateFilter, TemplateFilterSchema } from './types';
import { ZodFunctionSchema } from '../types';
import { CreatedTemplateFilter, TemplateFilterExample } from './types';
import { z } from 'zod';
/**
@@ -23,10 +23,12 @@ import { z } from 'zod';
* @alpha
*/
export const createTemplateFilter = <
TSchema extends TemplateFilterSchema<any, any> | undefined,
TFunctionSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: (arg: JsonValue, ...rest: JsonValue[]) => JsonValue | undefined,
>(
filter: CreatedTemplateFilter<TSchema, TFunctionSchema>,
): CreatedTemplateFilter<unknown, unknown> => filter;
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
>(options: {
id: string;
description?: string;
examples?: TemplateFilterExample[];
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
filter: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
}): CreatedTemplateFilter<TFunctionArgs, TReturnType> => options;
@@ -14,21 +14,10 @@
* limitations under the License.
*/
import { z } from 'zod';
import { TemplateFilter } from '../../types';
import { JsonValue } from '@backstage/types';
import { ZodFunctionSchema } from '../types';
export type { TemplateFilter } from '../../types';
/** @alpha */
export type TemplateFilterSchema<
Args extends z.ZodTuple<
| [z.ZodType<JsonValue>]
| [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
/** @alpha */
export type TemplateFilterExample = {
description?: string;
@@ -38,23 +27,12 @@ export type TemplateFilterExample = {
/** @alpha */
export type CreatedTemplateFilter<
TSchema extends
| TemplateFilterSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter = TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter,
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
> = {
id: string;
description?: string;
examples?: TemplateFilterExample[];
schema?: TSchema;
filter: TFilterSchema;
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
filter: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
};
@@ -18,9 +18,9 @@ import { z } from 'zod';
import {
CreatedTemplateGlobalFunction,
CreatedTemplateGlobalValue,
TemplateGlobalFunctionSchema,
TemplateGlobalFunctionExample,
} from './types';
import { JsonValue } from '@backstage/types';
import { ZodFunctionSchema } from '../types';
/**
* This function is used to create new template global values in type-safe manner.
@@ -39,10 +39,12 @@ export const createTemplateGlobalValue = (
* @alpha
*/
export const createTemplateGlobalFunction = <
TSchema extends TemplateGlobalFunctionSchema<any, any> | undefined,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: (...args: JsonValue[]) => JsonValue | undefined,
>(
fn: CreatedTemplateGlobalFunction<TSchema, TFilterSchema>,
): CreatedTemplateGlobalFunction<any, any> => fn;
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
>(options: {
id: string;
description?: string;
examples?: TemplateGlobalFunctionExample[];
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
fn: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
}): CreatedTemplateGlobalFunction<TFunctionArgs, TReturnType> => options;
@@ -15,7 +15,7 @@
*/
import { JsonValue } from '@backstage/types';
import { z } from 'zod';
import { TemplateGlobal } from '../../types';
import { ZodFunctionSchema } from '../types';
export type { TemplateGlobal } from '../../types';
@@ -26,15 +26,6 @@ export type CreatedTemplateGlobalValue<T extends JsonValue = JsonValue> = {
description?: string;
};
/** @alpha */
export type TemplateGlobalFunctionSchema<
Args extends z.ZodTuple<
[] | [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
/** @alpha */
export type TemplateGlobalFunctionExample = {
description?: string;
@@ -44,31 +35,17 @@ export type TemplateGlobalFunctionExample = {
/** @alpha */
export type CreatedTemplateGlobalFunction<
TSchema extends
| TemplateGlobalFunctionSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<
TemplateGlobal,
JsonValue
> = TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<TemplateGlobal, JsonValue>,
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
> = {
id: string;
description?: string;
examples?: TemplateGlobalFunctionExample[];
schema?: TSchema;
fn: TFilterSchema;
schema?: ZodFunctionSchema<TFunctionArgs, TReturnType>;
fn: (...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>;
};
/** @alpha */
export type CreatedTemplateGlobal =
| CreatedTemplateGlobalValue
| CreatedTemplateGlobalFunction<unknown, unknown>;
| CreatedTemplateGlobalFunction<any, any>;
+2 -1
View File
@@ -27,6 +27,7 @@ import { CreatedTemplateGlobal } from './globals';
export * from '../tasks/alpha';
export * from './filters';
export * from './globals';
export * from './types';
/**
* Extension point for managing scaffolder actions.
@@ -73,7 +74,7 @@ export const scaffolderTaskBrokerExtensionPoint =
*/
export interface ScaffolderTemplatingExtensionPoint {
addTemplateFilters(
filters: Record<string, TemplateFilter> | CreatedTemplateFilter[],
filters: Record<string, TemplateFilter> | CreatedTemplateFilter<any, any>[],
): void;
addTemplateGlobals(
@@ -0,0 +1,30 @@
/*
* 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 { z } from 'zod';
/**
* @alpha
*/
export type ZodFunctionSchema<
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
TReturnType extends z.ZodTypeAny,
> = (
zod: typeof z,
) =>
| z.ZodFunction<z.ZodTuple<TFunctionArgs, null>, TReturnType>
| z.ZodType<
(...args: z.infer<z.ZodTuple<TFunctionArgs>>) => z.infer<TReturnType>
>;
@@ -347,6 +347,7 @@ export const scaffolderReactTranslationRef: TranslationRef<
readonly 'templateCategoryPicker.title': 'Categories';
readonly 'templateCard.noDescription': 'No description';
readonly 'templateCard.chooseButtonText': 'Choose';
readonly 'cardHeader.detailBtnTitle': 'Show template entity details';
readonly 'templateOutputs.title': 'Text Output';
}
>;
@@ -24,12 +24,21 @@ import {
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { starredEntitiesApiRef } from '@backstage/plugin-catalog-react';
import {
entityRouteRef,
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog';
import Observable from 'zen-observable';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
const mountedRoutes = {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
};
describe('CardHeader', () => {
it('should select the correct theme from the theme provider from the header', async () => {
// Can't really test what we want here.
@@ -64,6 +73,7 @@ describe('CardHeader', () => {
/>
</ThemeProvider>
</TestApiProvider>,
mountedRoutes,
);
expect(mockTheme.getPageTheme).toHaveBeenCalledWith({ themeId: 'service' });
@@ -93,6 +103,7 @@ describe('CardHeader', () => {
}}
/>
</TestApiProvider>,
mountedRoutes,
);
expect(getByText('service')).toBeInTheDocument();
@@ -118,6 +129,7 @@ describe('CardHeader', () => {
<TestApiProvider apis={[[starredEntitiesApiRef, starredEntitiesApi]]}>
<CardHeader template={mockTemplate} />
</TestApiProvider>,
mountedRoutes,
);
const favorite = getByRole('button', { name: 'Add to favorites' });
@@ -129,6 +141,38 @@ describe('CardHeader', () => {
);
});
it('renders TemplateDetailButton with link to entity page', async () => {
const { getByTitle } = await renderInTestApp(
<TestApiProvider
apis={[
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: mockApis.storage(),
}),
],
]}
>
<CardHeader
template={{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: { name: 'test-template', namespace: 'default' },
spec: {
steps: [],
type: 'service',
},
}}
/>
</TestApiProvider>,
mountedRoutes,
);
const detailButton = getByTitle('Show template entity details');
const link = detailButton.querySelector('a');
expect(link).toBeInTheDocument();
});
it('should render the name of the entity', async () => {
const { getByText } = await renderInTestApp(
<TestApiProvider
@@ -153,6 +197,7 @@ describe('CardHeader', () => {
}}
/>
</TestApiProvider>,
mountedRoutes,
);
expect(getByText('bob')).toBeInTheDocument();
@@ -182,6 +227,7 @@ describe('CardHeader', () => {
}}
/>
</TestApiProvider>,
mountedRoutes,
);
expect(getByText('Iamtitle')).toBeInTheDocument();
@@ -15,10 +15,11 @@
*/
import React from 'react';
import { Theme, makeStyles, useTheme } from '@material-ui/core/styles';
import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
import { ItemCardHeader } from '@backstage/core-components';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { FavoriteEntity } from '@backstage/plugin-catalog-react';
import { TemplateDetailButton } from './TemplateDetailButton.tsx';
const useStyles = makeStyles<
Theme,
@@ -66,7 +67,11 @@ export const CardHeader = (props: CardHeaderProps) => {
<div className={styles.subtitleWrapper}>
<div>{type}</div>
<div>
<FavoriteEntity entity={props.template} style={{ padding: 0 }} />
<TemplateDetailButton template={props.template} />
<FavoriteEntity
entity={props.template}
style={{ padding: 0, marginLeft: 6 }}
/>
</div>
</div>
);
@@ -33,6 +33,12 @@ import { permissionApiRef } from '@backstage/plugin-permission-react';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';
const mountedRoutes = {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
};
describe('TemplateCard', () => {
it('should render the card title', async () => {
const mockTemplate: TemplateEntityV1beta3 = {
@@ -59,6 +65,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
mountedRoutes,
);
expect(getByText('bob')).toBeInTheDocument();
@@ -89,6 +96,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
mountedRoutes,
);
const description = getByText('hello');
@@ -121,6 +129,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
mountedRoutes,
);
expect(getByText('No description')).toBeInTheDocument();
@@ -151,6 +160,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
mountedRoutes,
);
expect(queryByTestId('template-card-separator')).toBeInTheDocument();
@@ -187,6 +197,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
mountedRoutes,
);
for (const tag of mockTemplate.metadata.tags!) {
@@ -227,11 +238,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
mountedRoutes,
);
expect(queryByTestId('template-card-separator')).toBeInTheDocument();
@@ -272,11 +279,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
mountedRoutes,
);
expect(queryByTestId('template-card-separator')).toBeInTheDocument();
@@ -321,11 +324,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
mountedRoutes,
);
expect(queryByTestId('template-card-separator')).not.toBeInTheDocument();
@@ -364,17 +363,13 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} />
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
mountedRoutes,
);
expect(getByRole('link', { name: /.*my-test-user$/ })).toBeInTheDocument();
expect(getByRole('link', { name: /.*my-test-user$/ })).toHaveAttribute(
'href',
'/catalog/group/default/my-test-user',
'/catalog/default/group/my-test-user',
);
});
@@ -404,11 +399,7 @@ describe('TemplateCard', () => {
>
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
mountedRoutes,
);
expect(getByRole('button', { name: 'Choose' })).toBeInTheDocument();
@@ -448,11 +439,7 @@ describe('TemplateCard', () => {
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
</TestApiProvider>
</SWRConfig>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
mountedRoutes,
);
expect(queryByText('Choose')).toBeNull();
@@ -0,0 +1,64 @@
/*
* 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 React from 'react';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import DescriptionIcon from '@material-ui/icons/Description';
import { Link } from '@backstage/core-components';
import {
entityRouteParams,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { useApp, useRouteRef } from '@backstage/core-plugin-api';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { scaffolderReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
export interface TemplateDetailButtonProps {
template: Entity;
}
export const TemplateDetailButton = ({
template,
}: TemplateDetailButtonProps) => {
const catalogEntityRoute = useRouteRef(entityRouteRef);
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const entityRef = stringifyEntityRef(template);
const app = useApp();
const TemplateIcon = app.getSystemIcon('kind:template') || DescriptionIcon;
return (
<Tooltip id={`tooltip-${entityRef}`} title={t('cardHeader.detailBtnTitle')}>
<IconButton
aria-label={t('cardHeader.detailBtnTitle')}
id={`viewDetail-${entityRef}`}
style={{ padding: 0 }}
color="inherit"
>
<Typography component="span">
<Link
to={catalogEntityRoute(entityRouteParams(template))}
style={{ display: 'flex', alignItems: 'center' }}
>
<TemplateIcon />
</Link>
</Typography>
</IconButton>
</Tooltip>
);
};
@@ -44,6 +44,9 @@ export const scaffolderReactTranslationRef = createTranslationRef({
noDescription: 'No description',
chooseButtonText: 'Choose',
},
cardHeader: {
detailBtnTitle: 'Show template entity details',
},
templateOutputs: {
title: 'Text Output',
},
@@ -17,6 +17,7 @@
import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog';
import {
catalogApiRef,
entityRouteRef,
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
@@ -30,6 +31,13 @@ import React from 'react';
import { rootRouteRef } from '../../../routes';
import { TemplateListPage } from './TemplateListPage';
const mountedRoutes = {
mountedRoutes: {
'/': rootRouteRef,
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
};
describe('TemplateListPage', () => {
const mockCatalogApi = catalogApiMock({
entities: [
@@ -60,7 +68,7 @@ describe('TemplateListPage', () => {
>
<TemplateListPage />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
mountedRoutes,
);
expect(getByPlaceholderText('Search')).toBeInTheDocument();
@@ -82,7 +90,7 @@ describe('TemplateListPage', () => {
>
<TemplateListPage />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
mountedRoutes,
);
expect(getByRole('menuitem', { name: /All/ })).toBeInTheDocument();
@@ -105,7 +113,7 @@ describe('TemplateListPage', () => {
>
<TemplateListPage />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
mountedRoutes,
);
expect(getByText('Categories')).toBeInTheDocument();
@@ -127,7 +135,7 @@ describe('TemplateListPage', () => {
>
<TemplateListPage />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
mountedRoutes,
);
expect(getByText('Owner')).toBeInTheDocument();
@@ -150,6 +158,7 @@ describe('TemplateListPage', () => {
>
<TemplateListPage />
</TestApiProvider>,
mountedRoutes,
);
expect(getByText('Tags')).toBeInTheDocument();
@@ -172,7 +181,7 @@ describe('TemplateListPage', () => {
>
<TemplateListPage />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
mountedRoutes,
);
expect(queryByTestId('menu-button')).toBeInTheDocument();
});
@@ -199,7 +208,7 @@ describe('TemplateListPage', () => {
}}
/>
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
mountedRoutes,
);
expect(queryByTestId('menu-button')).not.toBeInTheDocument();
});
+163 -163
View File
@@ -970,9 +970,9 @@ __metadata:
languageName: node
linkType: hard
"@storybook/addon-actions@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-actions@npm:8.6.4"
"@storybook/addon-actions@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-actions@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
"@types/uuid": ^9.0.1
@@ -980,231 +980,231 @@ __metadata:
polished: ^4.2.2
uuid: ^9.0.0
peerDependencies:
storybook: ^8.6.4
checksum: b08f73070ac90f2c0d404b63f3148ebe11d4683f0728fd04459df62b521422d8c17b3619f6c012a90571a2d5520ee34d35e02fd104c0298580177dd57e045312
storybook: ^8.6.7
checksum: 28add3c30e397894bb13d1d492b88e331cec2a24b47ad9e865002e54851a17a0916387dca02074b834c6d665748bd2a5ec88904ad08795645d1b8a9345fb1328
languageName: node
linkType: hard
"@storybook/addon-backgrounds@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-backgrounds@npm:8.6.4"
"@storybook/addon-backgrounds@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-backgrounds@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
memoizerific: ^1.11.3
ts-dedent: ^2.0.0
peerDependencies:
storybook: ^8.6.4
checksum: 53a67d385d6241730d0d992552de58316cfa4c46f56fa5a81df8647c76356778164783b8a872e31b3624692f456186074e06407778cf4442ffc8596cdaded49e
storybook: ^8.6.7
checksum: 574b634e49225f52800a7ed0ea7b120f15198ae91068c06842b16e2d197a076bf94c1b42dc05fc0daac328169317768f28294260a14451f928cb0641010bec0d
languageName: node
linkType: hard
"@storybook/addon-controls@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-controls@npm:8.6.4"
"@storybook/addon-controls@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-controls@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
dequal: ^2.0.2
ts-dedent: ^2.0.0
peerDependencies:
storybook: ^8.6.4
checksum: a25c7f2ae7c3b4aee89ba865e5a305293d54bc4723f4e8e4f22d3c42a626d09b83a69521e728e2b3b35107a7709702801c1f5a4fe96e16871d77108282b8a8a2
storybook: ^8.6.7
checksum: 002184ac5444ea21bb76e2737f596920d1d06fbf2f7eab75d12d8f4b4f176439132e81e7433ad46f968e379bfb5224992a0a351b19f7673a96bbd74212d6d6c6
languageName: node
linkType: hard
"@storybook/addon-docs@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-docs@npm:8.6.4"
"@storybook/addon-docs@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-docs@npm:8.6.7"
dependencies:
"@mdx-js/react": ^3.0.0
"@storybook/blocks": 8.6.4
"@storybook/csf-plugin": 8.6.4
"@storybook/react-dom-shim": 8.6.4
"@storybook/blocks": 8.6.7
"@storybook/csf-plugin": 8.6.7
"@storybook/react-dom-shim": 8.6.7
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
ts-dedent: ^2.0.0
peerDependencies:
storybook: ^8.6.4
checksum: 28705136eb3117fcc46b4038a6fb8bf24207fa39b2b5043ab3dd124a3a186d02d36afdb03a456638ec652e1944679de0721361bca27caa5920b234e92c4e9149
storybook: ^8.6.7
checksum: 764c3fddbbb92a2c4c7df4d96d943f9511a96a40e87f94b084dbaed789bec4de8d87a83e3877cb7648aafb1fa61940be8d52c2910c037ade89c6ef02beb3d745
languageName: node
linkType: hard
"@storybook/addon-essentials@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/addon-essentials@npm:8.6.4"
version: 8.6.7
resolution: "@storybook/addon-essentials@npm:8.6.7"
dependencies:
"@storybook/addon-actions": 8.6.4
"@storybook/addon-backgrounds": 8.6.4
"@storybook/addon-controls": 8.6.4
"@storybook/addon-docs": 8.6.4
"@storybook/addon-highlight": 8.6.4
"@storybook/addon-measure": 8.6.4
"@storybook/addon-outline": 8.6.4
"@storybook/addon-toolbars": 8.6.4
"@storybook/addon-viewport": 8.6.4
"@storybook/addon-actions": 8.6.7
"@storybook/addon-backgrounds": 8.6.7
"@storybook/addon-controls": 8.6.7
"@storybook/addon-docs": 8.6.7
"@storybook/addon-highlight": 8.6.7
"@storybook/addon-measure": 8.6.7
"@storybook/addon-outline": 8.6.7
"@storybook/addon-toolbars": 8.6.7
"@storybook/addon-viewport": 8.6.7
ts-dedent: ^2.0.0
peerDependencies:
storybook: ^8.6.4
checksum: 564cc7e6f4559572f12c7f015e40a3995c55c681704e4082ef80d9db0c7b06433a67473e810ea9d67a46121f3a4aca8d7851953bc7f109f54e96c069b05230e0
storybook: ^8.6.7
checksum: 774b50e98888ece9536ec2a7f491c8486055d3c7515ac9a7f0264916f684c6c4608e95da7db14bceb65dfdf05f3fc93011a0b74a690eec2d44b6c969511b9313
languageName: node
linkType: hard
"@storybook/addon-highlight@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-highlight@npm:8.6.4"
"@storybook/addon-highlight@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-highlight@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
peerDependencies:
storybook: ^8.6.4
checksum: e080b543bd3fbe0e8d6e916f2abd562b7136371dff64e96d196532dca4958016b7890c6dcd48545f11c2ac31aa0d3a713b98d6fd2cde2a1f94b1ebaf7031095d
storybook: ^8.6.7
checksum: 7baae25d40e717e5138d10b4ea7ef424b55500ab7496ef38738ea0fb99f910bdf3a56651efbfa85d3e4218382498b045894653b47cdd066c6b16ed0076537940
languageName: node
linkType: hard
"@storybook/addon-interactions@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/addon-interactions@npm:8.6.4"
version: 8.6.7
resolution: "@storybook/addon-interactions@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
"@storybook/instrumenter": 8.6.4
"@storybook/test": 8.6.4
"@storybook/instrumenter": 8.6.7
"@storybook/test": 8.6.7
polished: ^4.2.2
ts-dedent: ^2.2.0
peerDependencies:
storybook: ^8.6.4
checksum: 6abfd78b17aef00708eb6221d3fdfc45fb738e1fd206a5c08c3909476abe6f042a507b6226ff99708538196294f37aea1d743ca7c33ea270ca0456c0f332ab55
storybook: ^8.6.7
checksum: f7a66307aa4e83b1a230d0103123eef15ce7d27aa25c47270624b9ddb6653293e9d0dfe9bb93d3c348604ec05c774716bd513f4d3987ae0b9818a0f865fa0d69
languageName: node
linkType: hard
"@storybook/addon-links@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/addon-links@npm:8.6.4"
version: 8.6.7
resolution: "@storybook/addon-links@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
ts-dedent: ^2.0.0
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^8.6.4
storybook: ^8.6.7
peerDependenciesMeta:
react:
optional: true
checksum: 92ffa01adda87133dd97623e3beb69b1095929a8ec79c00191b816b4dcdf35d4dcad33b2dfa16791647f85357b8fa8f662cdc382772a6c9376953c8f49564dde
checksum: 3770196105742335eb862eea9e096a7be4cb056067b9bd72a1616ca8b1033d2549199494e017f252342959e51ebb4205b1562c0db17b800aceb4656c5bbe2fa3
languageName: node
linkType: hard
"@storybook/addon-measure@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-measure@npm:8.6.4"
"@storybook/addon-measure@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-measure@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
tiny-invariant: ^1.3.1
peerDependencies:
storybook: ^8.6.4
checksum: b020d052c87aab591b4b7e31dd60764af9f70140ad4d529d468cdf0c8eb45d02ad7ec2bc01a83d88a1547cf567da8db5e4ba0b4c7294e968df2bace3f07f4fd7
storybook: ^8.6.7
checksum: 38c51463366405216aa21f5e99a5c66acf8a626084427a439bbbdf76f41119cf66a5fd25e1b27f7859e180101b4a536ee058a3978b03cbe6a48f2e0807927c83
languageName: node
linkType: hard
"@storybook/addon-outline@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-outline@npm:8.6.4"
"@storybook/addon-outline@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-outline@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
ts-dedent: ^2.0.0
peerDependencies:
storybook: ^8.6.4
checksum: d705c2dda8fc4b9af2ecb8217a0463f0f8213f836150627f67e4702d449071e5982580617ff75ae8c48960ca27e957fe3700c17cd854e05d326ceeaf622202c2
storybook: ^8.6.7
checksum: 8919842d53079cb6ae8efcecc5673d1fcd3ec8f9cf2dbf2f44de0fa2648ef684bd626c9be187ef570e7ff2b21eb92bfe1759b8e72bd41f9f0cb82e2e57073cc7
languageName: node
linkType: hard
"@storybook/addon-storysource@npm:^8.3.6":
version: 8.6.4
resolution: "@storybook/addon-storysource@npm:8.6.4"
version: 8.6.7
resolution: "@storybook/addon-storysource@npm:8.6.7"
dependencies:
"@storybook/source-loader": 8.6.4
"@storybook/source-loader": 8.6.7
estraverse: ^5.2.0
tiny-invariant: ^1.3.1
peerDependencies:
storybook: ^8.6.4
checksum: 6172f5ea802a5081a6e777597ca68c2e3bddb58be2bcc7b52f7646bdd2216902a2bb426b3fd8e2a6a8710741d9e72d569a279fb6e0aadadae47fbb881e72308f
storybook: ^8.6.7
checksum: 3aecdc32867b7ecf0a38efffa80caae6f0249391b8e21ea7a7434ba6dd8f054cd918b4669e799b40fe2f654084c114ab20d411d233e540d46b8c208f9d6f5c2a
languageName: node
linkType: hard
"@storybook/addon-themes@npm:^8.3.6":
version: 8.6.4
resolution: "@storybook/addon-themes@npm:8.6.4"
version: 8.6.7
resolution: "@storybook/addon-themes@npm:8.6.7"
dependencies:
ts-dedent: ^2.0.0
peerDependencies:
storybook: ^8.6.4
checksum: 11ae08e570df4d94ed452c0be6b15773d51c0a8003c6a238156262fafbb8e094fb5c3d20c11bceaae68461ba12fe305732e4d3ea53bf16c8e115ec2ef6fd1f00
storybook: ^8.6.7
checksum: 209793b93774e3e1696e33fcf3a62dd63a4ca60920e45b465a86a2504545d00463ebc683f4ff65606b502175d575a969698efe96538daebc5254adfe9b767929
languageName: node
linkType: hard
"@storybook/addon-toolbars@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-toolbars@npm:8.6.4"
"@storybook/addon-toolbars@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-toolbars@npm:8.6.7"
peerDependencies:
storybook: ^8.6.4
checksum: c15ef11c50ac067e5b226dba30db956405d94b5ff56d67279697bcdcc63367ef0ab523f091b8755f8b8ab5c1ac6e55c3ded9cef6f5b917261ea9a232bcf9a8dc
storybook: ^8.6.7
checksum: 72de8200fcbd95714b79deae7194451725abfd75dac56862b76d9f19150aa844829d7e2c99597fc3be650c6cd952c3fe4789866512314fb26f5d112620ae8145
languageName: node
linkType: hard
"@storybook/addon-viewport@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/addon-viewport@npm:8.6.4"
"@storybook/addon-viewport@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/addon-viewport@npm:8.6.7"
dependencies:
memoizerific: ^1.11.3
peerDependencies:
storybook: ^8.6.4
checksum: 746cd771f371b825da4c5b974602a1c14287212c19ddff9f593e77e9867a3efe76ccc9ee3e503efdcf91949bf3a03979c8f37aa2fe62eb888a3eb11dd208def2
storybook: ^8.6.7
checksum: 2a83a8683ca8306e13578266aeffbc24968f9bf11b1b71c19a5aba0239e0365f19d335950ec261f11c4907577a9600ab6182ccc0ad8e60c13f9a9ea02b6b0adc
languageName: node
linkType: hard
"@storybook/blocks@npm:8.6.4, @storybook/blocks@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/blocks@npm:8.6.4"
"@storybook/blocks@npm:8.6.7, @storybook/blocks@npm:^8.3.5":
version: 8.6.7
resolution: "@storybook/blocks@npm:8.6.7"
dependencies:
"@storybook/icons": ^1.2.12
ts-dedent: ^2.0.0
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
storybook: ^8.6.4
storybook: ^8.6.7
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
checksum: fb891491f99fa63c02c90e00d166c9299cf93512d2d6036a932a1c243cd568f7ca45af961b89a19ba1c1c51c4ad76d0de8d687249f1dcb8b3411fbec2b7cf238
checksum: 89b600680772712500318d82195d2a78885d84127a05ab3e2e6474170f871239ab5de72669d0bfdeede99186f8487c48e197ed33fc3e7fc17d0aea228f915bff
languageName: node
linkType: hard
"@storybook/builder-vite@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/builder-vite@npm:8.6.4"
"@storybook/builder-vite@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/builder-vite@npm:8.6.7"
dependencies:
"@storybook/csf-plugin": 8.6.4
"@storybook/csf-plugin": 8.6.7
browser-assert: ^1.2.1
ts-dedent: ^2.0.0
peerDependencies:
storybook: ^8.6.4
storybook: ^8.6.7
vite: ^4.0.0 || ^5.0.0 || ^6.0.0
checksum: e3e6c1674af25a51b87c084476a2f6e07245a4206493ca7758f116588efc3986e1200b45a9e626406e31a37f001f9c8abf0c77e83f02a14a713c5ac407196775
checksum: 8ffc8b2f60c4f1f88d99becd35f75cf34e6b7506b2a25ba6ec054a98758f3fada2a054a475c0f8fd74a7baba84fa1d799b9b843352df4cafab7e3d7a96d74348
languageName: node
linkType: hard
"@storybook/components@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/components@npm:8.6.4"
"@storybook/components@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/components@npm:8.6.7"
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
checksum: b50923e759eea99320fdf8946056fb33614b9d993ca567207cd0d714e5fae83da485b64b0d74a18b84355396b33c28b5eea050484c40967873c540f83d314b9a
checksum: cb20f8baa76deef1af1739d6c1b1a9f57eb22d158fa046d9919a91840e7a00f1b48e96b7ffe7994365b60808f864c963a3476ee98b4c8b63968a3a877068da3a
languageName: node
linkType: hard
"@storybook/core@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/core@npm:8.6.4"
"@storybook/core@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/core@npm:8.6.7"
dependencies:
"@storybook/theming": 8.6.4
"@storybook/theming": 8.6.7
better-opn: ^3.0.2
browser-assert: ^1.2.1
esbuild: ^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0
@@ -1220,18 +1220,18 @@ __metadata:
peerDependenciesMeta:
prettier:
optional: true
checksum: 8352b0f65bd7c0462471409a7f086125f4e1a93e1777fa20268beb78109f9784cc1d75536a4f9136368db87ba3c5c11987c95835937ba4c2776f968365795d8f
checksum: b150b7472766fccbbc1dec74cba3c16e009a9f24c13f01198923f553d12e2a2070ee2a35f5514f9af350b79e34577946ea65a4a495fd1fa18a27655f897c013a
languageName: node
linkType: hard
"@storybook/csf-plugin@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/csf-plugin@npm:8.6.4"
"@storybook/csf-plugin@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/csf-plugin@npm:8.6.7"
dependencies:
unplugin: ^1.3.1
peerDependencies:
storybook: ^8.6.4
checksum: f1750183ad0c83438803da83d705a0e73819ad7e0cfd010182cc504d8a37ab2f500df4c6ea61ec82b464b13e992a2a4cf6756aabb2a66e12e8c2ee3f22553929
storybook: ^8.6.7
checksum: 75c799fd222552f447e24c852b696bc00f1399a631c6a29e4da1b0be97600aa6085f0ebb56beb8c22731b6099fdddaf52edf01c0d0803ba8e22dd04f79bf483a
languageName: node
linkType: hard
@@ -1261,143 +1261,143 @@ __metadata:
languageName: node
linkType: hard
"@storybook/instrumenter@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/instrumenter@npm:8.6.4"
"@storybook/instrumenter@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/instrumenter@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
"@vitest/utils": ^2.1.1
peerDependencies:
storybook: ^8.6.4
checksum: acfc58fd5c29ec78a00498441c97d4528e8f915956c67600c75e7a30d240fb86551e9ce0dad8c0f83c78a828417a64d8aff38db754b87b854c70507d8d3638d0
storybook: ^8.6.7
checksum: 8b5bbdd73637233ddebf6d3f14d3904c6f122f29f2efe4a0172a1f03e066a88a3453ae3eef140b227eb626ee959c0c3522f62944407f4530e24e6a6ac804598b
languageName: node
linkType: hard
"@storybook/manager-api@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/manager-api@npm:8.6.4"
"@storybook/manager-api@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/manager-api@npm:8.6.7"
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
checksum: 50447d3961dfab2b2f36afa76aaeb779a9b51ca66a912bc763103f18f2f71514191388b101ec83093ac9dee6ab509b955da41325f15d88cfe1c480e7d7d1f9eb
checksum: 6bb0860c3795945189832613da5866e5159b2adb376478a2b596c113928c64f5962efc325f927a6bcb7a48d7257f42cf6489b0c44397876129626ac68c44aa28
languageName: node
linkType: hard
"@storybook/preview-api@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/preview-api@npm:8.6.4"
"@storybook/preview-api@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/preview-api@npm:8.6.7"
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
checksum: d8f642de8053686192e28f7ec748944c99c9e4d0cef7f13d96e840fd159c8bea2e3139b88a84eb16009fbda3152a61d9e075777211c10773e4d01fd103bf0e28
checksum: 0d24d62444b7b153bde89d3949ab6539a163e15bc80c5bfcc73b7d49bbb34fb02eb74a99434793b8a6446aeec491e07f006c0f4e404f98486c6d1d614d01defa
languageName: node
linkType: hard
"@storybook/react-dom-shim@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/react-dom-shim@npm:8.6.4"
"@storybook/react-dom-shim@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/react-dom-shim@npm:8.6.7"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^8.6.4
checksum: 0062191f312c0bf32e622904b9da7176c961cec99258c6b230c3fb6758c1cca95ac4142d4701628dc8eb922601d74c350549b78e146b5af1747edf8cb3def756
storybook: ^8.6.7
checksum: 51c20259ac4485d9a996266341a4d9898c949049e2a9af9a08bec58c2ace3a40be1a534005c52fc2db2c81bb010dbd36030bb70581b36c05cf4b34d58f140d3a
languageName: node
linkType: hard
"@storybook/react-vite@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/react-vite@npm:8.6.4"
version: 8.6.7
resolution: "@storybook/react-vite@npm:8.6.7"
dependencies:
"@joshwooding/vite-plugin-react-docgen-typescript": 0.5.0
"@rollup/pluginutils": ^5.0.2
"@storybook/builder-vite": 8.6.4
"@storybook/react": 8.6.4
"@storybook/builder-vite": 8.6.7
"@storybook/react": 8.6.7
find-up: ^5.0.0
magic-string: ^0.30.0
react-docgen: ^7.0.0
resolve: ^1.22.8
tsconfig-paths: ^4.2.0
peerDependencies:
"@storybook/test": 8.6.4
"@storybook/test": 8.6.7
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^8.6.4
storybook: ^8.6.7
vite: ^4.0.0 || ^5.0.0 || ^6.0.0
peerDependenciesMeta:
"@storybook/test":
optional: true
checksum: 877ea89891e18a860b598b48df086db58d161bb840093dc4563d470eab61a5042730de356a306d7a2b9de96afe522caa8ebceb0a4210e9e1b924a2548c451d3c
checksum: 75ed76ac6293520aaab02365a12ba93f360cb2f97b632e1c213de535e1d01e93c202f4a3795863ad273246b2bf1096e5b696ea531902d490534db470a712349f
languageName: node
linkType: hard
"@storybook/react@npm:8.6.4, @storybook/react@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/react@npm:8.6.4"
"@storybook/react@npm:8.6.7, @storybook/react@npm:^8.3.5":
version: 8.6.7
resolution: "@storybook/react@npm:8.6.7"
dependencies:
"@storybook/components": 8.6.4
"@storybook/components": 8.6.7
"@storybook/global": ^5.0.0
"@storybook/manager-api": 8.6.4
"@storybook/preview-api": 8.6.4
"@storybook/react-dom-shim": 8.6.4
"@storybook/theming": 8.6.4
"@storybook/manager-api": 8.6.7
"@storybook/preview-api": 8.6.7
"@storybook/react-dom-shim": 8.6.7
"@storybook/theming": 8.6.7
peerDependencies:
"@storybook/test": 8.6.4
"@storybook/test": 8.6.7
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^8.6.4
storybook: ^8.6.7
typescript: ">= 4.2.x"
peerDependenciesMeta:
"@storybook/test":
optional: true
typescript:
optional: true
checksum: 4e9860c8e177d0dd496e7d6c53927daa8429b82f9c02d5b004de61244573901160a936ba37ce69810fb13e3602391e18723237a901c57e1bd19cf9f58241c30c
checksum: c98438ddfd764300e1a805623fab976f9ef94a2d939984d6caf98ce92ed29d8a7307389b98e2df2d1302b302dc549c7c95050464731d9c588347cd154c360834
languageName: node
linkType: hard
"@storybook/source-loader@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/source-loader@npm:8.6.4"
"@storybook/source-loader@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/source-loader@npm:8.6.7"
dependencies:
es-toolkit: ^1.22.0
estraverse: ^5.2.0
prettier: ^3.1.1
peerDependencies:
storybook: ^8.6.4
checksum: 3336f11b99014bb55a7ad88eabc8149ed0ffa4e4cc3d64e22cfc7d7e7f0b36987eda0ca55b6b20d805e89305f277104db6c60c2100eea439bc385808f6193bc2
storybook: ^8.6.7
checksum: 74d5dd20384c0e17720ba3492458362bfd2f527614f37cffef438b202d12f4f45c411d2568308975286c34eea05d7bcb1baeaf6605337b7906725f7257b542c6
languageName: node
linkType: hard
"@storybook/test@npm:8.6.4, @storybook/test@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/test@npm:8.6.4"
"@storybook/test@npm:8.6.7, @storybook/test@npm:^8.3.5":
version: 8.6.7
resolution: "@storybook/test@npm:8.6.7"
dependencies:
"@storybook/global": ^5.0.0
"@storybook/instrumenter": 8.6.4
"@storybook/instrumenter": 8.6.7
"@testing-library/dom": 10.4.0
"@testing-library/jest-dom": 6.5.0
"@testing-library/user-event": 14.5.2
"@vitest/expect": 2.0.5
"@vitest/spy": 2.0.5
peerDependencies:
storybook: ^8.6.4
checksum: 2c16f1cc8e0f6e57f5972a664839885467c7a8b01cf807b32ea7cdd3508354cd408603f05b3f6d1c118e4560f4224102df6dfc0242deb0d2ea670895e8cdcc81
storybook: ^8.6.7
checksum: d269423580cbcc61de74a3940504f00bb580b389b04432aa6a0a7e8fed54536782ef254e8edcf6bac3ce98d35e439fdfd86316cf53f3435396f12cbc2e060541
languageName: node
linkType: hard
"@storybook/theming@npm:8.6.4":
version: 8.6.4
resolution: "@storybook/theming@npm:8.6.4"
"@storybook/theming@npm:8.6.7":
version: 8.6.7
resolution: "@storybook/theming@npm:8.6.7"
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
checksum: b7e13026c381e18e7ad2064e1528876eb01b7563ad16aababcac531ce76f5e59b985a72603f57d75465a611ea564bd46b9b1760634211faf4307f9265f06e044
checksum: 747a2377df65d5f0962dc8606d06247ad89fb7ecabab0bd9611055d599b5bd0fa15769e4c76e948ae34ae232ba756bf6795ccb3467b1adf35ff961db7350f4de
languageName: node
linkType: hard
"@storybook/types@npm:^8.3.5":
version: 8.6.4
resolution: "@storybook/types@npm:8.6.4"
version: 8.6.7
resolution: "@storybook/types@npm:8.6.7"
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
checksum: 99c59e960b9b4500c1bfcdcaedf0070fc7eb500c1d3226e39bc17c7ab3671fca89645abe30baf4dcfd65764d4955db6b2c416049aef78ab81535a27a4c671eb5
checksum: bae7a202bda4f83f8f8d9a176ff79a033f2e67f5db4d9ee05cd468c3921d8bcc2a44d93a11c8401ad7b3a08f6a54510013791ce932117f10fc6b289642be18aa
languageName: node
linkType: hard
@@ -4451,10 +4451,10 @@ __metadata:
linkType: hard
"storybook@npm:^8.3.5":
version: 8.6.4
resolution: "storybook@npm:8.6.4"
version: 8.6.7
resolution: "storybook@npm:8.6.7"
dependencies:
"@storybook/core": 8.6.4
"@storybook/core": 8.6.7
peerDependencies:
prettier: ^2 || ^3
peerDependenciesMeta:
@@ -4464,7 +4464,7 @@ __metadata:
getstorybook: ./bin/index.cjs
sb: ./bin/index.cjs
storybook: ./bin/index.cjs
checksum: 96c59b5c6ac27dfb9fb85d110b0257a77264015454dd0ca7893ae9df04dbf31e523ff351ba45283f33a58ecb28fd985349aa9c1fb83ab51d0c781a4d5809fa15
checksum: fe21ce4d9cd17069a5b409ed518603f97ec6ec421b7f41cff3e43b409820fcbc2c8dbcb3aed99dd7d0a99a0c6138c7576ddf72f78186b6f86085d503b3c8b32f
languageName: node
linkType: hard
+16 -16
View File
@@ -14589,13 +14589,13 @@ __metadata:
linkType: hard
"@playwright/test@npm:^1.32.3":
version: 1.51.0
resolution: "@playwright/test@npm:1.51.0"
version: 1.51.1
resolution: "@playwright/test@npm:1.51.1"
dependencies:
playwright: 1.51.0
playwright: 1.51.1
bin:
playwright: cli.js
checksum: 14959f930db55882ff45ba660dfd37e066bea2607619549f85b6c9c0591a895f4dcbc5419e2fc4069b03b02fef6d00628bc9382b65216c99735d6d93eb578374
checksum: c3c14f37451cc0323234e37fa559a075e5aee9f5e6ca06fa3b4a132ec6c809909b22877daca1fa48c95a1112d859346c10111e7003f44582a05100dd5385bbef
languageName: node
linkType: hard
@@ -20229,12 +20229,12 @@ __metadata:
linkType: hard
"@types/react@npm:^18.0.0":
version: 18.3.18
resolution: "@types/react@npm:18.3.18"
version: 18.3.19
resolution: "@types/react@npm:18.3.19"
dependencies:
"@types/prop-types": "*"
csstype: ^3.0.2
checksum: 5933597bc9f53e282f0438f0bb76d0f0fab60faabe760ea806e05ffe6f5c61b9b4d363e1a03a8fea47c510d493c6cf926cdeeba9f7074fa97b61940c350245e7
checksum: 41305614686cfc618ef154f06d3f80e7b2df4673ebbc474880c524ecbbaa48f14620b3b1c6cff9f225176932d362c62fe617020027069157cb040752902c35cb
languageName: node
linkType: hard
@@ -39310,27 +39310,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.51.0":
version: 1.51.0
resolution: "playwright-core@npm:1.51.0"
"playwright-core@npm:1.51.1":
version: 1.51.1
resolution: "playwright-core@npm:1.51.1"
bin:
playwright-core: cli.js
checksum: e14798a7323539e1d6f076a6aa0492ea7c5fa89f4db85a53b27001b0f84bde2a5f8b14e25e05c5396594c89173a48398728522932bb7bd2006dd4bb48c8b0979
checksum: 1eb37e22e97435a5ed6389b4caa666fbe618348861cae97e67586e20c8fed9ac3d3dc899ff3b9237d0ddfcf087d5b552b80be247e246fc45b75282f96be714bb
languageName: node
linkType: hard
"playwright@npm:1.51.0":
version: 1.51.0
resolution: "playwright@npm:1.51.0"
"playwright@npm:1.51.1":
version: 1.51.1
resolution: "playwright@npm:1.51.1"
dependencies:
fsevents: 2.3.2
playwright-core: 1.51.0
playwright-core: 1.51.1
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 02ec1077e2997872415fd40c9a5df6fd796d9cc15dade24bff9a88a16a451217744d850bdfc09efd4812d05c2fc856f92bfe106aae036fe21defa62c6d5b8ea5
checksum: 2d78f53eed438d26df9743f98227d2e33bb5a378c8d0144a4ed75b4010c9c517d5a6ee6c1c9245ad61dc615054c5e19d2ded5a6e285e35d2bca39b05bc9c4a35
languageName: node
linkType: hard