From 0d4c4db867d890783bd10b57e8b64e5829e0f35f Mon Sep 17 00:00:00 2001 From: Zeshan Ziya Date: Wed, 27 Aug 2025 12:44:46 +0530 Subject: [PATCH 001/193] fix(home): correct clearAll logic to properly handle deletable flag Signed-off-by: Zeshan Ziya --- .../home/src/components/CustomHomepage/CustomHomepageGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index 5d85e834e8..807837acf6 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -275,7 +275,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { }; const clearLayout = () => { - setWidgets(widgets.filter(w => !w.deletable)); + setWidgets(widgets.filter(w => w.deletable === false)); }; const changeEditMode = (mode: boolean) => { From 9a3f11889ab275c777d2a88507324e8417b96fb2 Mon Sep 17 00:00:00 2001 From: Zeshan Ziya Date: Wed, 27 Aug 2025 12:45:32 +0530 Subject: [PATCH 002/193] fix(home): add example to deletable flag on customizable home page Signed-off-by: Zeshan Ziya --- packages/app/src/components/home/CustomizableHomePage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app/src/components/home/CustomizableHomePage.tsx b/packages/app/src/components/home/CustomizableHomePage.tsx index b592490f2e..1d3cab646b 100644 --- a/packages/app/src/components/home/CustomizableHomePage.tsx +++ b/packages/app/src/components/home/CustomizableHomePage.tsx @@ -37,6 +37,7 @@ const defaultConfig = [ y: 0, width: 24, height: 2, + deletable: false, }, { component: 'HomePageRecentlyVisited', From e7d59d3c72589aaca620a80fbd60daf1b5235ecc Mon Sep 17 00:00:00 2001 From: Zeshan Ziya Date: Wed, 27 Aug 2025 12:46:34 +0530 Subject: [PATCH 003/193] chore: add changeset Signed-off-by: Zeshan Ziya --- .changeset/fuzzy-trams-kick.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fuzzy-trams-kick.md diff --git a/.changeset/fuzzy-trams-kick.md b/.changeset/fuzzy-trams-kick.md new file mode 100644 index 0000000000..19e12b1073 --- /dev/null +++ b/.changeset/fuzzy-trams-kick.md @@ -0,0 +1,6 @@ +--- +'example-app': patch +'@backstage/plugin-home': patch +--- + +fix(home): correct clearAll logic to properly handle deletable flag From 28d0c0f43c5b8393aa1a807acb00c804aa563e44 Mon Sep 17 00:00:00 2001 From: Zeshan Ziya Date: Wed, 27 Aug 2025 13:00:59 +0530 Subject: [PATCH 004/193] chore: remove example-app from changeset Signed-off-by: Zeshan Ziya --- .changeset/fuzzy-trams-kick.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/fuzzy-trams-kick.md b/.changeset/fuzzy-trams-kick.md index 19e12b1073..71c06891d0 100644 --- a/.changeset/fuzzy-trams-kick.md +++ b/.changeset/fuzzy-trams-kick.md @@ -1,5 +1,4 @@ --- -'example-app': patch '@backstage/plugin-home': patch --- From 431130cb833e18d81287e7d301537094e8f05593 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Wed, 13 Aug 2025 09:37:50 +0200 Subject: [PATCH 005/193] Add changeset Signed-off-by: TA31OU (cherry picked from commit ce965429e38c3817432085b97a63447289586af3) --- .changeset/flat-peas-run.md | 6 ++++ .../DependencyGraph/DependencyGraph.tsx | 8 +++++ .../src/components/DependencyGraph/types.ts | 34 +++++++++++++++++++ plugins/catalog-graph/package.json | 2 ++ .../DefaultRenderEdge.tsx | 15 ++++++++ .../EntityRelationsGraph.tsx | 3 ++ .../EntityRelationsGraph/useEntityStore.ts | 2 +- yarn.lock | 2 ++ 8 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 .changeset/flat-peas-run.md create mode 100644 plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx diff --git a/.changeset/flat-peas-run.md b/.changeset/flat-peas-run.md new file mode 100644 index 0000000000..116e8eb449 --- /dev/null +++ b/.changeset/flat-peas-run.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': minor +'@backstage/plugin-catalog-graph': minor +--- + +Added `renderEdge` prop to component in @backstage/core-components to allow custom rendering of graph edges. Fixed local development for EntityRelationsGraph plugin. diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index d690253b06..f4b3d6bd58 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -138,6 +138,11 @@ export interface DependencyGraphProps * Weight applied to edges in graph */ edgeWeight?: number; + /** + * Custom edge rendering component + */ + renderEdge?: Types.RenderEdgeFunction; + /** * Custom node rendering component */ @@ -210,6 +215,7 @@ export function DependencyGraph( labelOffset = 10, edgeRanks = 1, edgeWeight = 1, + renderEdge, renderLabel, defs, zoom = 'enabled', @@ -442,6 +448,8 @@ export function DependencyGraph( {graphEdges.map(e => { const edge = graph.current.edge(e) as GraphEdge; if (!edge) return null; + if (renderEdge) return renderEdge({ edge, id: e }); + return ( , ) => ReactNode; + /** + * Properties of {@link DependencyGraphTypes.RenderEdgeFunction} for {@link DependencyGraphTypes.DependencyEdge} + * + * @public + */ + export type RenderEdgeProps = { + edge: DependencyEdge; + id: dagre.Edge; + }; + + /** + * Custom React component for graph {@link DependencyGraphTypes.DependencyEdge} + * + * @public + */ + export type RenderEdgeFunction = (props: { + edge: T & { + points: { x: number; y: number }[]; + label?: string; + labeloffset?: number; + labelpos?: string; + width?: number; + height?: number; + weight?: number; + minlen?: number; + showArrowHeads?: boolean; + from?: string; + to?: string; + relations?: string[]; + }; + id: { v: string; w: string }; + }) => ReactNode; + /** * Graph direction * diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index dd3271f468..7cea775bbe 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -61,6 +61,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "classnames": "^2.3.1", + "d3-shape": "^3.0.0", "lodash": "^4.17.15", "p-limit": "^3.1.0", "qs": "^6.9.4", @@ -76,6 +77,7 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.0.0", + "@types/d3-shape": "^3.0.1", "@types/react": "^18.0.0", "react": "^18.0.2", "react-dom": "^18.0.2", diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx new file mode 100644 index 0000000000..379ce77f52 --- /dev/null +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 933aa230be..f08cd7a760 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -89,6 +89,7 @@ export type EntityRelationsGraphProps = { zoom?: 'enabled' | 'disabled' | 'enable-on-click'; renderNode?: DependencyGraphTypes.RenderNodeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; + renderEdge?: DependencyGraphTypes.RenderEdgeFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; }; @@ -114,6 +115,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { zoom = 'enabled', renderNode, renderLabel, + renderEdge, curve, showArrowHeads, } = props; @@ -156,6 +158,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { edges={edges} renderNode={renderNode || DefaultRenderNode} renderLabel={renderLabel || DefaultRenderLabel} + renderEdge={renderEdge} direction={direction} className={classes.graph} paddingX={theme.spacing(4)} diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts index c55becdd49..be6ea81f57 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts @@ -64,7 +64,7 @@ export function useEntityStore(): { return; } - const { items } = await catalogClient.getEntitiesByRefs({ entityRefs }); + const { items } = await catalogClient.getEntities(); items.forEach(ent => { if (ent) { const entityRef = stringifyEntityRef(ent); diff --git a/yarn.lock b/yarn.lock index 853749b99e..fde37ae4e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4771,8 +4771,10 @@ __metadata: "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" + "@types/d3-shape": "npm:^3.0.1" "@types/react": "npm:^18.0.0" classnames: "npm:^2.3.1" + d3-shape: "npm:^3.0.0" lodash: "npm:^4.17.15" p-limit: "npm:^3.1.0" qs: "npm:^6.9.4" From cd961c25d181fb907c1481e9af6dce6fed375552 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Wed, 3 Sep 2025 12:17:06 +0200 Subject: [PATCH 006/193] Revert useEntityStore change Signed-off-by: TA31OU --- .changeset/flat-peas-run.md | 2 +- .../src/components/EntityRelationsGraph/useEntityStore.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/flat-peas-run.md b/.changeset/flat-peas-run.md index 116e8eb449..c09eadecf8 100644 --- a/.changeset/flat-peas-run.md +++ b/.changeset/flat-peas-run.md @@ -3,4 +3,4 @@ '@backstage/plugin-catalog-graph': minor --- -Added `renderEdge` prop to component in @backstage/core-components to allow custom rendering of graph edges. Fixed local development for EntityRelationsGraph plugin. +Added `renderEdge` prop to component in @backstage/core-components to allow custom rendering of graph edges. diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts index be6ea81f57..c55becdd49 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts @@ -64,7 +64,7 @@ export function useEntityStore(): { return; } - const { items } = await catalogClient.getEntities(); + const { items } = await catalogClient.getEntitiesByRefs({ entityRefs }); items.forEach(ent => { if (ent) { const entityRef = stringifyEntityRef(ent); From 05b7ac0c560c6daa2f2bb0cbc36bdc4db3deeeab Mon Sep 17 00:00:00 2001 From: TA31OU Date: Wed, 3 Sep 2025 13:25:07 +0200 Subject: [PATCH 007/193] Update catalog-graph report-api Signed-off-by: TA31OU --- plugins/catalog-graph/report.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 25479bd9b7..7f001531b8 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -129,6 +129,7 @@ export type EntityRelationsGraphProps = { zoom?: 'enabled' | 'disabled' | 'enable-on-click'; renderNode?: DependencyGraphTypes.RenderNodeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; + renderEdge?: DependencyGraphTypes.RenderEdgeFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; }; From 3c994731243c37dd37ceaa709760e66dfd2ebd1c Mon Sep 17 00:00:00 2001 From: TA31OU Date: Wed, 3 Sep 2025 13:46:28 +0200 Subject: [PATCH 008/193] Update core-components report-api Signed-off-by: TA31OU --- packages/core-components/report-alpha.api.md | 2 +- packages/core-components/report.api.md | 33 ++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index 32bd2f4939..e258777b9c 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -24,10 +24,10 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'signIn.title': 'Sign In'; readonly 'signIn.loginFailed': 'Login failed'; readonly 'signIn.customProvider.title': 'Custom User'; + readonly 'signIn.customProvider.continue': 'Continue'; readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.'; readonly 'signIn.customProvider.userId': 'User ID'; readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token'; - readonly 'signIn.customProvider.continue': 'Continue'; readonly 'signIn.customProvider.idToken': 'ID Token (optional)'; readonly 'signIn.guestProvider.title': 'Guest'; readonly 'signIn.guestProvider.enter': 'Enter'; diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index fff1d0365a..4998accf01 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -18,6 +18,7 @@ import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; +import dagre from '@dagrejs/dagre'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; import IconButton from '@material-ui/core/IconButton'; @@ -273,6 +274,7 @@ export interface DependencyGraphProps paddingY?: number; ranker?: DependencyGraphTypes.Ranker; rankMargin?: number; + renderEdge?: DependencyGraphTypes.RenderEdgeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; renderNode?: DependencyGraphTypes.RenderNodeFunction; showArrowHeads?: boolean; @@ -314,6 +316,33 @@ export namespace DependencyGraphTypes { NETWORK_SIMPLEX = 'network-simplex', TIGHT_TREE = 'tight-tree', } + export type RenderEdgeFunction = (props: { + edge: T & { + points: { + x: number; + y: number; + }[]; + label?: string; + labeloffset?: number; + labelpos?: string; + width?: number; + height?: number; + weight?: number; + minlen?: number; + showArrowHeads?: boolean; + from?: string; + to?: string; + relations?: string[]; + }; + id: { + v: string; + w: string; + }; + }) => ReactNode; + export type RenderEdgeProps = { + edge: DependencyEdge; + id: dagre.Edge; + }; export type RenderLabelFunction = ( props: RenderLabelProps, ) => ReactNode; @@ -1548,8 +1577,8 @@ export type WarningPanelClassKey = // Warnings were encountered during analysis: // -// src/components/DependencyGraph/types.d.ts:22:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" -// src/components/DependencyGraph/types.d.ts:26:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" +// src/components/DependencyGraph/types.d.ts:23:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" +// src/components/DependencyGraph/types.d.ts:27:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" // src/components/TabbedLayout/RoutedTabs.d.ts:8:5 - (ae-forgotten-export) The symbol "SubRoute_2" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts From c0990e2586588b8167f1b95cf9b1ad29c7705ac3 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Wed, 3 Sep 2025 14:00:53 +0200 Subject: [PATCH 009/193] Update core-components report-api (again) Signed-off-by: TA31OU --- packages/core-components/report-alpha.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index e258777b9c..32bd2f4939 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -24,10 +24,10 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'signIn.title': 'Sign In'; readonly 'signIn.loginFailed': 'Login failed'; readonly 'signIn.customProvider.title': 'Custom User'; - readonly 'signIn.customProvider.continue': 'Continue'; readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.'; readonly 'signIn.customProvider.userId': 'User ID'; readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token'; + readonly 'signIn.customProvider.continue': 'Continue'; readonly 'signIn.customProvider.idToken': 'ID Token (optional)'; readonly 'signIn.guestProvider.title': 'Guest'; readonly 'signIn.guestProvider.enter': 'Enter'; From 8b7351f3dff72cdab67157d7626b116d11e7f533 Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 9 Sep 2025 14:48:48 +0200 Subject: [PATCH 010/193] feat(plugins/org): add overwrites to membership and ownership cards Signed-off-by: secustor --- .changeset/giant-weeks-jump.md | 5 +++ packages/app-next/app-config.yaml | 5 +++ plugins/org/report-alpha.api.md | 40 +++++++++++++++++--- plugins/org/src/alpha.tsx | 61 ++++++++++++++++++++++++------- 4 files changed, 91 insertions(+), 20 deletions(-) create mode 100644 .changeset/giant-weeks-jump.md diff --git a/.changeset/giant-weeks-jump.md b/.changeset/giant-weeks-jump.md new file mode 100644 index 0000000000..1b11bd1490 --- /dev/null +++ b/.changeset/giant-weeks-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Add `initialRelationAggregation` and `showAggregateMembersToggle` options to `EntityMembersListCard` as well to `EntityOwnershipCard` diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index f83e4d4ba0..59fecf7e64 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -22,6 +22,11 @@ app: ownerEntityRefs: [cubic-belugas] extensions: + - entity-card:org/members-list: + config: + showAggregateMembersToggle: true + initialRelationAggregation: aggregated + # - apis.plugin.graphiql.browse.gitlab: true # - graphiql-endpoint:graphiql/gitlab: true diff --git a/plugins/org/report-alpha.api.md b/plugins/org/report-alpha.api.md index 3cebe00d33..3f8fdca05d 100644 --- a/plugins/org/report-alpha.api.md +++ b/plugins/org/report-alpha.api.md @@ -63,13 +63,17 @@ const _default: OverridableFrontendPlugin< }; }>; 'entity-card:org/members-list': ExtensionDefinition<{ - kind: 'entity-card'; - name: 'members-list'; config: { + initialRelationAggregation: 'direct' | 'aggregated' | undefined; + showAggregateMembersToggle: boolean | undefined; + } & { filter: EntityPredicate | undefined; type: 'content' | 'summary' | 'info' | undefined; }; configInput: { + showAggregateMembersToggle?: boolean | undefined; + initialRelationAggregation?: 'direct' | 'aggregated' | undefined; + } & { filter?: EntityPredicate | undefined; type?: 'content' | 'summary' | 'info' | undefined; }; @@ -96,7 +100,17 @@ const _default: OverridableFrontendPlugin< optional: true; } >; - inputs: {}; + inputs: { + [x: string]: ExtensionInput< + ExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }; + kind: 'entity-card'; + name: 'members-list'; params: { loader: () => Promise; filter?: string | EntityPredicate | ((entity: Entity) => boolean); @@ -104,13 +118,17 @@ const _default: OverridableFrontendPlugin< }; }>; 'entity-card:org/ownership': ExtensionDefinition<{ - kind: 'entity-card'; - name: 'ownership'; config: { + initialRelationAggregation: 'direct' | 'aggregated' | undefined; + showAggregateMembersToggle: boolean | undefined; + } & { filter: EntityPredicate | undefined; type: 'content' | 'summary' | 'info' | undefined; }; configInput: { + showAggregateMembersToggle?: boolean | undefined; + initialRelationAggregation?: 'direct' | 'aggregated' | undefined; + } & { filter?: EntityPredicate | undefined; type?: 'content' | 'summary' | 'info' | undefined; }; @@ -137,7 +155,17 @@ const _default: OverridableFrontendPlugin< optional: true; } >; - inputs: {}; + inputs: { + [x: string]: ExtensionInput< + ExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }; + kind: 'entity-card'; + name: 'ownership'; params: { loader: () => Promise; filter?: string | EntityPredicate | ((entity: Entity) => boolean); diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx index 92ae7d8801..ddb8094688 100644 --- a/plugins/org/src/alpha.tsx +++ b/plugins/org/src/alpha.tsx @@ -36,26 +36,59 @@ const EntityGroupProfileCard = EntityCardBlueprint.make({ }); /** @alpha */ -const EntityMembersListCard = EntityCardBlueprint.make({ +const EntityMembersListCard = EntityCardBlueprint.makeWithOverrides({ name: 'members-list', - params: { - filter: { kind: 'group' }, - loader: async () => - import('./components/Cards/Group/MembersList/MembersListCard').then(m => - compatWrapper(), - ), + config: { + schema: { + initialRelationAggregation: z => + z.enum(['direct', 'aggregated']).optional(), + showAggregateMembersToggle: z => z.boolean().optional(), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + filter: { kind: 'group' }, + loader: async () => + import('./components/Cards/Group/MembersList/MembersListCard').then(m => + compatWrapper( + , + ), + ), + }); }, }); /** @alpha */ -const EntityOwnershipCard = EntityCardBlueprint.make({ +const EntityOwnershipCard = EntityCardBlueprint.makeWithOverrides({ name: 'ownership', - params: { - filter: { kind: { $in: ['group', 'user'] } }, - loader: async () => - import('./components/Cards/OwnershipCard/OwnershipCard').then(m => - compatWrapper(), - ), + config: { + schema: { + initialRelationAggregation: z => + z.enum(['direct', 'aggregated']).optional(), + showAggregateMembersToggle: z => z.boolean().optional(), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + filter: { kind: { $in: ['group', 'user'] } }, + loader: async () => + import('./components/Cards/OwnershipCard/OwnershipCard').then(m => + compatWrapper( + , + ), + ), + }); }, }); From 555d43996d5d95fc0edb20d0541bc1dfcbacc269 Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 9 Sep 2025 15:14:33 +0200 Subject: [PATCH 011/193] fix prettier Signed-off-by: secustor --- packages/app-next/app-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 59fecf7e64..704cc8d8a8 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -26,7 +26,7 @@ app: config: showAggregateMembersToggle: true initialRelationAggregation: aggregated - + # - apis.plugin.graphiql.browse.gitlab: true # - graphiql-endpoint:graphiql/gitlab: true From f7a414425d94bca21d40b1d1af921a4500b6dbd1 Mon Sep 17 00:00:00 2001 From: Adam Letizia Date: Thu, 4 Sep 2025 13:39:11 -0500 Subject: [PATCH 012/193] fix(kubernetes): fixes calculation of pod cpu utilization Signed-off-by: Adam Letizia --- .changeset/twelve-guests-sit.md | 5 +++++ .../src/components/Pods/PodsTable.test.tsx | 2 +- plugins/kubernetes-react/src/utils/pod.test.tsx | 2 +- plugins/kubernetes-react/src/utils/pod.tsx | 12 ++---------- 4 files changed, 9 insertions(+), 12 deletions(-) create mode 100644 .changeset/twelve-guests-sit.md diff --git a/.changeset/twelve-guests-sit.md b/.changeset/twelve-guests-sit.md new file mode 100644 index 0000000000..0d75e07f0c --- /dev/null +++ b/.changeset/twelve-guests-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +Fixes calculation of CPU utilization in the PodTable diff --git a/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx b/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx index fb90d17e2b..76136641af 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx @@ -74,7 +74,7 @@ describe('PodsTable', () => { limitTotal: '134217728', }, cpu: { - currentUsage: 0.4966115, + currentUsage: 0.0496, requestTotal: 0.05, limitTotal: 0.05, }, diff --git a/plugins/kubernetes-react/src/utils/pod.test.tsx b/plugins/kubernetes-react/src/utils/pod.test.tsx index 3f82c14f86..a75039ceea 100644 --- a/plugins/kubernetes-react/src/utils/pod.test.tsx +++ b/plugins/kubernetes-react/src/utils/pod.test.tsx @@ -42,7 +42,7 @@ describe('pod', () => { const result = podStatusToCpuUtil({ cpu: { // ~50m - currentUsage: 0.4966115, + currentUsage: 0.0496, // 50m requestTotal: 0.05, // 100m diff --git a/plugins/kubernetes-react/src/utils/pod.tsx b/plugins/kubernetes-react/src/utils/pod.tsx index 97b44d4d24..e033b7c3b8 100644 --- a/plugins/kubernetes-react/src/utils/pod.tsx +++ b/plugins/kubernetes-react/src/utils/pod.tsx @@ -139,22 +139,14 @@ export const currentToDeclaredResourceToPerc = ( export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => { const cpuUtil = podStatus.cpu; - let currentUsage: number | string = cpuUtil.currentUsage; - - // current usage number for CPU is a different unit than request/limit total - // this might be a bug in the k8s library - if (typeof cpuUtil.currentUsage === 'number') { - currentUsage = cpuUtil.currentUsage / 10; - } - return ( From 4d2816dbe908db34a8ecd13e4cb899d6d1b196b5 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 10 Sep 2025 13:54:15 +0100 Subject: [PATCH 013/193] First pass improving theming docs Signed-off-by: Charles de Dreuille --- docs/conf/user-interface/icons.md | 124 +++++ docs/conf/user-interface/index.md | 471 +++++++++++++++++ docs/conf/user-interface/logo.md | 25 + docs/conf/user-interface/sidebar.md | 93 ++++ docs/dls/component-design-guidelines.md | 6 +- docs/dls/design.md | 2 +- docs/features/search/how-to-guides.md | 2 +- docs/getting-started/app-custom-theme.md | 613 ----------------------- microsite/sidebars.ts | 11 +- 9 files changed, 729 insertions(+), 618 deletions(-) create mode 100644 docs/conf/user-interface/icons.md create mode 100644 docs/conf/user-interface/index.md create mode 100644 docs/conf/user-interface/logo.md create mode 100644 docs/conf/user-interface/sidebar.md delete mode 100644 docs/getting-started/app-custom-theme.md diff --git a/docs/conf/user-interface/icons.md b/docs/conf/user-interface/icons.md new file mode 100644 index 0000000000..44b8782430 --- /dev/null +++ b/docs/conf/user-interface/icons.md @@ -0,0 +1,124 @@ +--- +id: icons +title: Customizing Icons +sidebar_label: Icons +description: Customizing Icons +--- + +So far you've seen how to create your own theme and add your own logo, in the following sections you'll be shown how to override the existing icons and how to add more icons + +## Custom Icons + +You can also customize the Project's _default_ icons. + +You can change the following [icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx). + +### Requirements + +- Files in `.svg` format +- React components created for the icons + +### Create React Component + +In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `CustomIcons.tsx` file. + +```tsx title="customIcons.tsx" +import { SvgIcon, SvgIconProps } from '@material-ui/core'; + +export const ExampleIcon = (props: SvgIconProps) => ( + + + +); +``` + +### Using the custom icon + +Supply your custom icon in `packages/app/src/App.tsx` + +```tsx title="packages/app/src/App.tsx" +/* highlight-add-next-line */ +import { ExampleIcon } from './assets/icons/CustomIcons' + + +const app = createApp({ + apis, + components: { + {/* ... */} + }, + themes: [ + {/* ... */} + ], + /* highlight-add-start */ + icons: { + github: ExampleIcon, + }, + /* highlight-add-end */ + bindRoutes({ bind }) { + {/* ... */} + } +}) +``` + +## Adding Icons + +You can add more icons, if the [default icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx) do not fit your needs, so that they can be used in other places like for Links in your entities. For this example we'll be using icons from[Material UI](https://v4.mui.com/components/material-icons/) and specifically the `AlarmIcon`. Here's how to do that: + +1. First you will want to open your `App.tsx` in `/packages/app/src` +2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';` +3. Next you want to add the icon like this to your `createApp`: + + ```tsx title="packages/app/src/App.tsx" + const app = createApp({ + apis: ..., + plugins: ..., + /* highlight-add-start */ + icons: { + alert: AlarmIcon, + }, + /* highlight-add-end */ + themes: ..., + components: ..., + }); + ``` + +4. Now we can reference `alert` for our icon in our entity links like this: + + ```yaml + apiVersion: backstage.io/v1alpha1 + kind: Component + metadata: + name: artist-lookup + description: Artist Lookup + links: + - url: https://example.com/alert + title: Alerts + icon: alert + ``` + + And this is the result: + + ![Example Link with Alert icon](../../assets/getting-started/add-icons-links-example.png) + + Another way you can use these icons is from the `AppContext` like this: + + ```ts + import { useApp } from '@backstage/core-plugin-api'; + + const app = useApp(); + const alertIcon = app.getSystemIcon('alert'); + ``` + + You might want to use this method if you have an icon you want to use in several locations. + +:::note Note + +If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon` + +::: diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md new file mode 100644 index 0000000000..9be726d2dc --- /dev/null +++ b/docs/conf/user-interface/index.md @@ -0,0 +1,471 @@ +--- +id: index +title: Customizing Your App's UI +sidebar_label: Introduction +description: Learn how to customize the look and feel of your Backstage app, including theming and branding options. +--- + +Backstage offers built-in support for both light and dark themes, making it easy to get started with a professional look and feel. But many teams want to go further—tailoring the interface to reflect their organization’s unique brand, identity, and experience. + +This section explores the different ways you can customize the appearance of your Backstage instance. You'll learn how the theming system is structured today, how to work with the two coexisting UI systems, and how to define themes that align with your visual language. + +## Theming architecture overview + +Backstage currently supports two parallel UI systems. The original theming and component model is built on Material UI (MUI), a popular React-based framework. More recently, Backstage introduced Backstage UI (BUI), a custom-designed, CSS-first system developed to meet the platform’s evolving needs. Both systems are supported today, with many parts of the ecosystem still using MUI while new components adopt BUI. + +
+
+

MUI (Legacy)

+
    +
  • Theming: JS-based with UnifiedThemeProvider
  • +
  • Coverage: Most existing plugins
  • +
  • Documentation: mui.com
  • +
+
+
+

Backstage UI (New)

+
    +
  • Theming: CSS variables and tokens
  • +
  • Coverage: Growing, focused on new work
  • +
  • Documentation: ui.backstage.io
  • +
+
+
+ +:::info +We recognize that maintaining two separate theming systems is not ideal. Because of the fundamental architectural differences between MUI and Backstage UI, it can be challenging to automate theme updates or know exactly which theme to modify for a given component. Our recommendation is to inspect the component’s code and check its class names: if you see a class name starting with `bui`, you should use the Backstage UI theming approach to style it. +::: + +## Creating custom themes + +During the transition to Backstage UI, you will need to maintain themes in two places: some components and plugins still rely on MUI, while others use Backstage UI. At this time, there is no automated solution to generate MUI themes from your Backstage UI theme, so you will need to manage both separately until the migration is complete. + +```tsx title="packages/app/src/App.tsx" +/* highlight-add-start */ +import { lightTheme, darkTheme } from './themes'; // MUI themes +import './styles.css'; // Backstage UI (BUI) theme +/* highlight-add-end */ + +const app = createApp({ + apis, + components, + /* highlight-add-start */ + themes: [ + { + id: 'light', + title: 'Light theme', + variant: 'light', + icon: , + Provider: ({ children }) => ( + + ), + }, + { + id: 'dark', + title: 'Dark theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + ), + }, + ], + /* highlight-add-end */ +}); +``` + +| Name | Description | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | Each theme has a unique `id` | +| `title` | This will be shown in the settings page to select the right theme. | +| `variant` | This can be either `light` or `dark`. This is also refered as `mode`. On the `body` of your app we are inserting a data attribute to set the theme based on this value `data-theme-mode="light"`. | +| `icon` | This will be shown in the settings page as a visual element to complement the title. | +| `Provider` | This is needed to set the legacy theme with MUI only. This will be become redundant later on when we fully replace with BUI but for now you need to have it for MUI to work. BUI is based on CSS and don't rely on any global providers. | + +:::note +Your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.dark` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). Be sure to provide both `light` and `dark` modes so users can choose their preference. +::: + +## Create a theme for Backstage UI (New) + +Backstage UI is built entirely using CSS. By default we are providing a default theme that include all our core CSS variables and component styles. To start customising Backstage UI to match your brand you need to create a new CSS file and import it directly in `packages/app/src/App.tsx`. All styles declared in this file will override the default styles. As your file grow you can organise it the way you want or even import multiple files. + +Backstage UI is using light by default under `:root` but you can target it more specifically using the data attribute for mode + +```css title="packages/app/src/styles.css" +:root { + /* Use :root to set styles for both light and dark themes */ + .bui-Button { + background-color: #000; + color: #fff; + } +} + +[data-theme-mode='light'] { + /* Light theme specific styles */ +  --bui-bg: #f8f8f8; + --bui-fg-primary: #000; +} + +[data-theme-mode='dark'] { + /* Dark theme specific styles */ +  --bui-bg: #333333; + --bui-fg-primary: #fff; +} +``` + +### Available tokens + +### Component class names + +### Custom font + +## Create a theme for MUI (Legacy) + +To customize the appearance of your Backstage app using the legacy MUI theming system, you can define your own theme by extending the built-in light or dark themes. This is done using the createUnifiedTheme utility provided by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. This function allows you to override key aspects of the theme—such as color palette, typography, spacing, and shape—while preserving Backstage’s base configuration and component compatibility. + +The example below shows how to create a new theme based on the default light theme: + +```ts title="packages/app/src/themes.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +export const lightTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', +}); + +export const darkTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.dark, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', +}); +``` + +You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the +[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done. + +
+ Example of a custom MUI theme + +For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io). + +```ts title="packages/app/src/themes.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + genPageTheme, + palettes, + shapes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: { + ...palettes.light, + primary: { + main: '#343b58', + }, + secondary: { + main: '#565a6e', + }, + error: { + main: '#8c4351', + }, + warning: { + main: '#8f5e15', + }, + info: { + main: '#34548a', + }, + success: { + main: '#485e30', + }, + background: { + default: '#d5d6db', + paper: '#d5d6db', + }, + banner: { + info: '#34548a', + error: '#8c4351', + text: '#343b58', + link: '#565a6e', + }, + errorBackground: '#8c4351', + warningBackground: '#8f5e15', + infoBackground: '#343b58', + navigation: { + background: '#343b58', + indicator: '#8f5e15', + color: '#d5d6db', + selectedColor: '#ffffff', + }, + }, + }), + defaultPageTheme: 'home', + fontFamily: 'Comic Sans MS', + /* below drives the header colors */ + pageTheme: { + home: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + documentation: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave2, + }), + tool: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.round }), + service: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave, + }), + website: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave, + }), + library: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave, + }), + other: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + app: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + apis: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + }, +}); +``` + +
+ +
+ Custom Typography + +When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + htmlFontSize: 16, + fontFamily: 'Arial, sans-serif', + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h5: { + fontWeight: 700, + fontSize: 24, + marginBottom: 4, + }, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + defaultTypography, + palettes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + ...defaultTypography, + htmlFontSize: 16, + fontFamily: 'Roboto, sans-serif', + h1: { + fontSize: 72, + fontWeight: 700, + marginBottom: 10, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +
+ +
+ Custom Fonts + +To add custom fonts, you first need to store the font so that it can be imported. We suggest creating the `assets/fonts` directory in your front-end application `src` folder. + +You can then declare the font style following the `@font-face` syntax from [Material UI Typography](https://mui.com/material-ui/customization/typography/). + +After that you can then utilize the `styleOverrides` of `MuiCssBaseline` under components to add a font to the `@font-face` array. + +```ts title="packages/app/src/theme/myTheme.ts" +import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; + +const myCustomFont = { + fontFamily: 'My-Custom-Font', + fontStyle: 'normal', + fontDisplay: 'swap', + fontWeight: 300, + src: ` + local('My-Custom-Font'), + url(${MyCustomFont}) format('woff2'), + `, +}; + +export const myTheme = createUnifiedTheme({ + fontFamily: 'My-Custom-Font', + palette: palettes.light, + components: { + MuiCssBaseline: { + styleOverrides: { + '@font-face': [myCustomFont], + }, + }, + }, +}); +``` + +If you want to utilize different or multiple fonts, then you can set the top level `fontFamily` to what you want for your body, and then override `fontFamily` in `typography` to control fonts for various headings. + +```ts title="packages/app/src/theme/myTheme.ts" +import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; +import myAwesomeFont from '../assets/fonts/My-Awesome-Font.woff2'; + +const myCustomFont = { + fontFamily: 'My-Custom-Font', + fontStyle: 'normal', + fontDisplay: 'swap', + fontWeight: 300, + src: ` + local('My-Custom-Font'), + url(${MyCustomFont}) format('woff2'), + `, +}; + +const myAwesomeFont = { + fontFamily: 'My-Awesome-Font', + fontStyle: 'normal', + fontDisplay: 'swap', + fontWeight: 300, + src: ` + local('My-Awesome-Font'), + url(${myAwesomeFont}) format('woff2'), + `, +}; + +export const myTheme = createUnifiedTheme({ + fontFamily: 'My-Custom-Font', + components: { + MuiCssBaseline: { + styleOverrides: { + '@font-face': [myCustomFont, myAwesomeFont], + }, + }, + }, + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + ...defaultTypography, + htmlFontSize: 16, + fontFamily: 'My-Custom-Font', + h1: { + fontSize: 72, + fontWeight: 700, + marginBottom: 10, + fontFamily: 'My-Awesome-Font', + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +
+ +
+ Overriding Backstage and Material UI components styles + +When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this: + +```tsx +const useStyles = makeStyles( + theme => ({ + header: { + padding: theme.spacing(3), + boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)', + backgroundImage: theme.page.backgroundImage, + }, + }), + { name: 'BackstageHeader' }, +); +``` + +Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override. + +Here's how you would do that: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', + components: { + BackstageHeader: { + styleOverrides: { + header: ({ theme }) => ({ + width: 'auto', + margin: '20px', + boxShadow: 'none', + borderBottom: `4px solid ${theme.palette.primary.main}`, + }), + }, + }, + }, +}); +``` + +
diff --git a/docs/conf/user-interface/logo.md b/docs/conf/user-interface/logo.md new file mode 100644 index 0000000000..35789d6330 --- /dev/null +++ b/docs/conf/user-interface/logo.md @@ -0,0 +1,25 @@ +--- +id: logo +title: Customizing Your Logo +sidebar_label: Logo +description: Learn how to customize your logo. +--- + +In addition to a custom theme, you can also customize the logo displayed at the far top left of the site. + +In your frontend app, locate `src/components/Root/` folder. You'll find two components: + +- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened. +- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed. + +To replace the images, you can simply replace the relevant code in those components with raw SVG definitions. + +You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code: + +```tsx +import MyCustomLogoFull from './logo/my-company-logo.png'; + +const LogoFull = () => { + return ; +}; +``` diff --git a/docs/conf/user-interface/sidebar.md b/docs/conf/user-interface/sidebar.md new file mode 100644 index 0000000000..f66e6bd106 --- /dev/null +++ b/docs/conf/user-interface/sidebar.md @@ -0,0 +1,93 @@ +--- +id: sidebar +title: Customizing Your Sidebar +sidebar_label: Sidebar +description: Learn how to customize the look and feel of your Sidebar. +--- + +As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar. + +## Sidebar Sub-menu + +For this example we'll show you how you can expand the sidebar with a sub-menu: + +1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives +2. Then we want to add the following import for `useApp`: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + import { useApp } from '@backstage/core-plugin-api'; + ``` + +3. Then update the `@backstage/core-components` import like this: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + import { + Sidebar, + sidebarConfig, + SidebarDivider, + SidebarGroup, + SidebarItem, + SidebarPage, + SidebarScrollWrapper, + SidebarSpace, + useSidebarOpenState, + Link, + /* highlight-add-start */ + GroupIcon, + SidebarSubmenu, + SidebarSubmenuItem, + /* highlight-add-end */ + } from '@backstage/core-components'; + ``` + +4. Finally replace `` with this: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + + + + + + + + + + + + + + ``` + +When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this: + +![Sidebar sub-menu example](./../../assets/getting-started/sidebar-submenu-example.png) + +You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar) diff --git a/docs/dls/component-design-guidelines.md b/docs/dls/component-design-guidelines.md index 35e2f987d2..17e9d1f11a 100644 --- a/docs/dls/component-design-guidelines.md +++ b/docs/dls/component-design-guidelines.md @@ -103,6 +103,8 @@ accessibility. [7]: https://v4.mui.com/components/cards/ [8]: https://v4.mui.com/customization/palette/#default-values [9]: https://v4.mui.com/customization/typography/ -[10]: https://backstage.io/docs/getting-started/app-custom-theme -[11]: https://backstage.io/docs/getting-started/app-custom-theme#overriding-backstage-and-material-ui-components-styles +[10]: https://backstage.io/docs/conf/user-interface + + + [12]: https://v4.mui.com/customization/default-theme/#explore diff --git a/docs/dls/design.md b/docs/dls/design.md index 7e2ae90528..e988d1032f 100644 --- a/docs/dls/design.md +++ b/docs/dls/design.md @@ -127,7 +127,7 @@ your own plugin for Backstage. **[Discord](https://discord.gg/backstage-687207715902193673)** - all design questions should be directed to the _#design_ channel. -**[Customize Backstage's look and feel](https://backstage.io/docs/getting-started/app-custom-theme)** - +**[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)** - How to customize the look and feel of your Backstage instance by extending the theme. diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 7055db1138..f0823ac47e 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -116,7 +116,7 @@ indexBuilder.addCollator({ The default highlighting styling for matched terms in search results is your browsers default styles for the `` HTML tag. If you want to customize how highlighted terms look you can follow Backstage's guide on how to -[Customize the look-and-feel of your App](https://backstage.io/docs/getting-started/app-custom-theme) +[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface) to create an override with your preferred styling. For example, using the new MUI V4+V5 unified theming method, the following will result diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md deleted file mode 100644 index 38f2fd4c98..0000000000 --- a/docs/getting-started/app-custom-theme.md +++ /dev/null @@ -1,613 +0,0 @@ ---- -id: app-custom-theme -title: Customize the look-and-feel of your App -description: Documentation on Customizing look and feel of the App ---- - -Backstage ships with a default theme with a light and dark mode variant. The themes are provided as a part of the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, which also includes utilities for customizing the default theme, or creating completely new themes. - -## Creating a Custom Theme - -The easiest way to create a new theme is to use the `createUnifiedTheme` function exported by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You can use it to override some basic parameters of the default theme such as the color palette and font. - -For example, you can create a new theme based on the default light theme like this: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - }), - fontFamily: 'Comic Sans MS', - defaultPageTheme: 'home', -}); -``` - -:::note Note - -we recommend creating a `theme` folder in `packages/app/src` to place your theme file to keep things nicely organized. - -::: - -You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the -[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done. - -## Using your Custom Theme - -To add a custom theme to your Backstage app, you pass it as configuration to `createApp`. - -For example, adding the theme that we created in the previous section can be done like this: - -```tsx title="packages/app/src/App.tsx" -import { createApp } from '@backstage/app-defaults'; -import { ThemeProvider } from '@material-ui/core/styles'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import LightIcon from '@material-ui/icons/WbSunny'; -import { UnifiedThemeProvider} from '@backstage/theme'; -import { myTheme } from './themes/myTheme'; - -const app = createApp({ - apis: ..., - plugins: ..., - themes: [{ - id: 'my-theme', - title: 'My Custom Theme', - variant: 'light', - icon: , - Provider: ({ children }) => ( - - ), - }] -}) -``` - -Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.dark` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). - -## Example of a custom theme - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - genPageTheme, - palettes, - shapes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: { - ...palettes.light, - primary: { - main: '#343b58', - }, - secondary: { - main: '#565a6e', - }, - error: { - main: '#8c4351', - }, - warning: { - main: '#8f5e15', - }, - info: { - main: '#34548a', - }, - success: { - main: '#485e30', - }, - background: { - default: '#d5d6db', - paper: '#d5d6db', - }, - banner: { - info: '#34548a', - error: '#8c4351', - text: '#343b58', - link: '#565a6e', - }, - errorBackground: '#8c4351', - warningBackground: '#8f5e15', - infoBackground: '#343b58', - navigation: { - background: '#343b58', - indicator: '#8f5e15', - color: '#d5d6db', - selectedColor: '#ffffff', - }, - }, - }), - defaultPageTheme: 'home', - fontFamily: 'Comic Sans MS', - /* below drives the header colors */ - pageTheme: { - home: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - documentation: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave2, - }), - tool: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.round }), - service: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave, - }), - website: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave, - }), - library: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave, - }), - other: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - app: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - apis: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - }, -}); -``` - -For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io). - -## Custom Typography - -When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - typography: { - htmlFontSize: 16, - fontFamily: 'Arial, sans-serif', - h1: { - fontSize: 54, - fontWeight: 700, - marginBottom: 10, - }, - h2: { - fontSize: 40, - fontWeight: 700, - marginBottom: 8, - }, - h3: { - fontSize: 32, - fontWeight: 700, - marginBottom: 6, - }, - h4: { - fontWeight: 700, - fontSize: 28, - marginBottom: 6, - }, - h5: { - fontWeight: 700, - fontSize: 24, - marginBottom: 4, - }, - h6: { - fontWeight: 700, - fontSize: 20, - marginBottom: 2, - }, - }, - defaultPageTheme: 'home', - }), -}); -``` - -If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - defaultTypography, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - typography: { - ...defaultTypography, - htmlFontSize: 16, - fontFamily: 'Roboto, sans-serif', - h1: { - fontSize: 72, - fontWeight: 700, - marginBottom: 10, - }, - }, - defaultPageTheme: 'home', - }), -}); -``` - -## Custom Fonts - -To add custom fonts, you first need to store the font so that it can be imported. We suggest creating the `assets/fonts` directory in your front-end application `src` folder. - -You can then declare the font style following the `@font-face` syntax from [Material UI Typography](https://mui.com/material-ui/customization/typography/). - -After that you can then utilize the `styleOverrides` of `MuiCssBaseline` under components to add a font to the `@font-face` array. - -```ts title="packages/app/src/theme/myTheme.ts" -import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; - -const myCustomFont = { - fontFamily: 'My-Custom-Font', - fontStyle: 'normal', - fontDisplay: 'swap', - fontWeight: 300, - src: ` - local('My-Custom-Font'), - url(${MyCustomFont}) format('woff2'), - `, -}; - -export const myTheme = createUnifiedTheme({ - fontFamily: 'My-Custom-Font', - palette: palettes.light, - components: { - MuiCssBaseline: { - styleOverrides: { - '@font-face': [myCustomFont], - }, - }, - }, -}); -``` - -If you want to utilize different or multiple fonts, then you can set the top level `fontFamily` to what you want for your body, and then override `fontFamily` in `typography` to control fonts for various headings. - -```ts title="packages/app/src/theme/myTheme.ts" -import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; -import myAwesomeFont from '../assets/fonts/My-Awesome-Font.woff2'; - -const myCustomFont = { - fontFamily: 'My-Custom-Font', - fontStyle: 'normal', - fontDisplay: 'swap', - fontWeight: 300, - src: ` - local('My-Custom-Font'), - url(${MyCustomFont}) format('woff2'), - `, -}; - -const myAwesomeFont = { - fontFamily: 'My-Awesome-Font', - fontStyle: 'normal', - fontDisplay: 'swap', - fontWeight: 300, - src: ` - local('My-Awesome-Font'), - url(${myAwesomeFont}) format('woff2'), - `, -}; - -export const myTheme = createUnifiedTheme({ - fontFamily: 'My-Custom-Font', - components: { - MuiCssBaseline: { - styleOverrides: { - '@font-face': [myCustomFont, myAwesomeFont], - }, - }, - }, - ...createBaseThemeOptions({ - palette: palettes.light, - typography: { - ...defaultTypography, - htmlFontSize: 16, - fontFamily: 'My-Custom-Font', - h1: { - fontSize: 72, - fontWeight: 700, - marginBottom: 10, - fontFamily: 'My-Awesome-Font', - }, - }, - defaultPageTheme: 'home', - }), -}); -``` - -## Overriding Backstage and Material UI components styles - -When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this: - -```tsx -const useStyles = makeStyles( - theme => ({ - header: { - padding: theme.spacing(3), - boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)', - backgroundImage: theme.page.backgroundImage, - }, - }), - { name: 'BackstageHeader' }, -); -``` - -Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override. - -Here's how you would do that: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - }), - fontFamily: 'Comic Sans MS', - defaultPageTheme: 'home', - components: { - BackstageHeader: { - styleOverrides: { - header: ({ theme }) => ({ - width: 'auto', - margin: '20px', - boxShadow: 'none', - borderBottom: `4px solid ${theme.palette.primary.main}`, - }), - }, - }, - }, -}); -``` - -## Custom Logo - -In addition to a custom theme, you can also customize the logo displayed at the far top left of the site. - -In your frontend app, locate `src/components/Root/` folder. You'll find two components: - -- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened. -- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed. - -To replace the images, you can simply replace the relevant code in those components with raw SVG definitions. - -You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code: - -```tsx -import MyCustomLogoFull from './logo/my-company-logo.png'; - -const LogoFull = () => { - return ; -}; -``` - -## Icons - -So far you've seen how to create your own theme and add your own logo, in the following sections you'll be shown how to override the existing icons and how to add more icons - -### Custom Icons - -You can also customize the Project's _default_ icons. - -You can change the following [icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx). - -#### Requirements - -- Files in `.svg` format -- React components created for the icons - -#### Create React Component - -In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `CustomIcons.tsx` file. - -```tsx title="customIcons.tsx" -import { SvgIcon, SvgIconProps } from '@material-ui/core'; - -export const ExampleIcon = (props: SvgIconProps) => ( - - - -); -``` - -#### Using the custom icon - -Supply your custom icon in `packages/app/src/App.tsx` - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ExampleIcon } from './assets/icons/CustomIcons' - - -const app = createApp({ - apis, - components: { - {/* ... */} - }, - themes: [ - {/* ... */} - ], - /* highlight-add-start */ - icons: { - github: ExampleIcon, - }, - /* highlight-add-end */ - bindRoutes({ bind }) { - {/* ... */} - } -}) -``` - -### Adding Icons - -You can add more icons, if the [default icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx) do not fit your needs, so that they can be used in other places like for Links in your entities. For this example we'll be using icons from[Material UI](https://v4.mui.com/components/material-icons/) and specifically the `AlarmIcon`. Here's how to do that: - -1. First you will want to open your `App.tsx` in `/packages/app/src` -2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';` -3. Next you want to add the icon like this to your `createApp`: - - ```tsx title="packages/app/src/App.tsx" - const app = createApp({ - apis: ..., - plugins: ..., - /* highlight-add-start */ - icons: { - alert: AlarmIcon, - }, - /* highlight-add-end */ - themes: ..., - components: ..., - }); - ``` - -4. Now we can reference `alert` for our icon in our entity links like this: - - ```yaml - apiVersion: backstage.io/v1alpha1 - kind: Component - metadata: - name: artist-lookup - description: Artist Lookup - links: - - url: https://example.com/alert - title: Alerts - icon: alert - ``` - - And this is the result: - - ![Example Link with Alert icon](../assets/getting-started/add-icons-links-example.png) - - Another way you can use these icons is from the `AppContext` like this: - - ```ts - import { useApp } from '@backstage/core-plugin-api'; - - const app = useApp(); - const alertIcon = app.getSystemIcon('alert'); - ``` - - You might want to use this method if you have an icon you want to use in several locations. - -:::note Note - -If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon` - -::: - -## Custom Sidebar - -As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar. - -### Sidebar Sub-menu - -For this example we'll show you how you can expand the sidebar with a sub-menu: - -1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives -2. Then we want to add the following import for `useApp`: - - ```tsx title="packages/app/src/components/Root/Root.tsx" - import { useApp } from '@backstage/core-plugin-api'; - ``` - -3. Then update the `@backstage/core-components` import like this: - - ```tsx title="packages/app/src/components/Root/Root.tsx" - import { - Sidebar, - sidebarConfig, - SidebarDivider, - SidebarGroup, - SidebarItem, - SidebarPage, - SidebarScrollWrapper, - SidebarSpace, - useSidebarOpenState, - Link, - /* highlight-add-start */ - GroupIcon, - SidebarSubmenu, - SidebarSubmenuItem, - /* highlight-add-end */ - } from '@backstage/core-components'; - ``` - -4. Finally replace `` with this: - - ```tsx title="packages/app/src/components/Root/Root.tsx" - - - - - - - - - - - - - - ``` - -When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this: - -![Sidebar sub-menu example](./../assets/getting-started/sidebar-submenu-example.png) - -You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar) - -## Custom Homepage - -In addition to a custom theme, a custom logo, you can also customize the -homepage of your app. Read the full guide on the [next page](homepage.md). - -## Migrating to Material UI v5 - -We now support Material UI v5 in Backstage. Check out our [migration guide](../tutorials/migrate-to-mui5.md) to get started. diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 5a408d5d1f..b7e89825ca 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -35,7 +35,6 @@ export default { 'getting-started/config/database', 'getting-started/config/authentication', 'getting-started/configure-app-with-plugins', - 'getting-started/app-custom-theme', 'getting-started/homepage', ], }, @@ -398,6 +397,16 @@ export default { 'conf/reading', 'conf/writing', 'conf/defining', + { + type: 'category', + label: 'User Interface', + items: [ + 'conf/user-interface/index', + 'conf/user-interface/logo', + 'conf/user-interface/icons', + 'conf/user-interface/sidebar', + ], + }, ], Framework: [ { From f23cd8862150f64f109a6cbff2bcce9931fd67fc Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 10 Sep 2025 13:56:59 +0100 Subject: [PATCH 014/193] Fix spelling mistakes Signed-off-by: Charles de Dreuille --- docs/conf/user-interface/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index 9be726d2dc..5f2d355137 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -78,7 +78,7 @@ const app = createApp({ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Each theme has a unique `id` | | `title` | This will be shown in the settings page to select the right theme. | -| `variant` | This can be either `light` or `dark`. This is also refered as `mode`. On the `body` of your app we are inserting a data attribute to set the theme based on this value `data-theme-mode="light"`. | +| `variant` | This can be either `light` or `dark`. This is also referred to as `mode`. On the `body` of your app we are inserting a data attribute to set the theme based on this value: `data-theme-mode="light"`. | | `icon` | This will be shown in the settings page as a visual element to complement the title. | | `Provider` | This is needed to set the legacy theme with MUI only. This will be become redundant later on when we fully replace with BUI but for now you need to have it for MUI to work. BUI is based on CSS and don't rely on any global providers. | From 8b25afdf12c0cbfc54a5e5e57eedb9460bb9b874 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 11 Sep 2025 11:32:32 +0100 Subject: [PATCH 015/193] Fix link error Signed-off-by: Charles de Dreuille --- docs/getting-started/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 252884dc54..876af4f0a5 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -157,7 +157,7 @@ Choose the correct next steps for your user role, if you're likely to be deployi - Configuring Backstage - [Adding plugins](./configure-app-with-plugins.md) - - [Customizing the theme](./app-custom-theme.md) + - [Customizing Your App's UI](../conf/user-interface/index.md) - [Populating the homepage](./homepage.md) ### Developer From 99f7e737d61a67aa8879aa10647fbab1b901e94c Mon Sep 17 00:00:00 2001 From: TA31OU Date: Thu, 11 Sep 2025 14:45:29 +0200 Subject: [PATCH 016/193] Merge Signed-off-by: TA31OU --- .changeset/angry-heads-design.md | 5 + .changeset/better-eagles-tickle.md | 5 + .changeset/big-cameras-turn.md | 5 + .changeset/brave-jars-speak.md | 5 + .changeset/chilly-llamas-attend.md | 5 + .changeset/clear-houses-wonder.md | 5 + .changeset/cold-garlics-care.md | 5 + .changeset/create-app-1757429965.md | 5 + .changeset/dirty-spies-drop.md | 5 + .changeset/eighty-numbers-act.md | 5 + .changeset/eleven-doors-down.md | 5 + .changeset/eleven-doors-own.md | 5 + .changeset/funny-eagles-try.md | 7 + .changeset/giant-buttons-flash.md | 5 + .changeset/gold-words-smoke.md | 5 + .changeset/heavy-cats-unite.md | 6 + .changeset/heavy-lies-listen.md | 5 + .changeset/hot-friends-act.md | 5 + .changeset/hungry-carrots-grow.md | 5 + .changeset/itchy-moons-start.md | 5 + .changeset/kind-eyes-worry.md | 5 + .changeset/late-swans-press.md | 5 + .changeset/lemon-jobs-create.md | 5 + .changeset/lovely-actors-love.md | 5 + .changeset/pre.json | 26 +- .changeset/quiet-papayas-mate.md | 5 + .changeset/red-shrimps-fall.md | 5 + .changeset/silent-results-stick.md | 5 + .changeset/stupid-areas-share.md | 5 + .changeset/tangy-squids-film.md | 5 + .changeset/ten-boxes-lie.md | 5 + .changeset/thin-phones-press.md | 5 + .changeset/tiny-spoons-mix.md | 5 + .changeset/tired-cobras-fly.md | 5 + .changeset/warm-emus-itch.md | 5 + .changeset/wet-onions-sneeze.md | 5 + .changeset/young-doodles-enter.md | 5 + .../config/vocabularies/Backstage/accept.txt | 2 + app-config.yaml | 5 + docs/auth/index.md | 1 + docs/auth/openshift/provider.md | 130 +++ .../core-services/actions-registry.md | 2 +- docs/backend-system/core-services/cache.md | 33 + docs/conf/index.md | 22 +- docs/contribute/project-structure.md | 4 - .../well-known-annotations.md | 2 +- docs/features/techdocs/FAQ.md | 6 + .../building-plugins/01-index.md | 2 +- docs/getting-started/index.md | 10 +- docs/references/glossary.md | 2 +- docs/releases/v1.43.0-next.2-changelog.md | 695 ++++++++++++++ lerna.json | 6 - microsite/data/plugins/datacontract.yaml | 10 + .../pluginsSearch/pluginsSearch.tsx | 19 + microsite/src/pages/plugins/index.tsx | 84 +- .../src/pages/plugins/plugins.module.scss | 14 + package.json | 2 +- packages/app-defaults/src/defaults/apis.ts | 18 + packages/app-next/CHANGELOG.md | 27 + packages/app-next/app-config.yaml | 1 - packages/app-next/package.json | 3 +- packages/app/CHANGELOG.md | 25 + packages/app/package.json | 2 +- packages/app/src/identityProviders.ts | 7 + packages/backend-defaults/CHANGELOG.md | 11 + packages/backend-defaults/package.json | 2 +- .../entrypoints/cache/CacheManager.test.ts | 143 +++ .../src/entrypoints/cache/CacheManager.ts | 24 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- packages/backend/package.json | 1 + packages/backend/src/index.ts | 1 + packages/catalog-client/CHANGELOG.md | 23 + packages/catalog-client/package.json | 2 +- packages/cli/CHANGELOG.md | 8 + packages/cli/package.json | 2 +- .../src/modules/build/lib/bundler/config.ts | 8 + packages/config-loader/CHANGELOG.md | 6 + packages/config-loader/package.json | 2 +- .../config-loader/src/schema/collect.test.ts | 10 - .../src/sources/ConfigSources.test.ts | 13 + .../src/sources/ConfigSources.ts | 28 + packages/core-app-api/report.api.md | 7 + .../src/apis/implementations/auth/index.ts | 1 + .../auth/openshift/OpenShiftAuth.ts | 52 ++ .../implementations/auth/openshift/index.ts | 5 +- packages/core-compat-api/CHANGELOG.md | 7 + packages/core-compat-api/package.json | 2 +- packages/core-components/CHANGELOG.md | 7 + packages/core-components/package.json | 4 +- packages/core-components/report-alpha.api.md | 1 + packages/core-components/report.api.md | 1 + .../DependencyGraph/DependencyGraph.test.tsx | 25 +- .../DependencyGraph/DependencyGraph.tsx | 202 ++-- .../HeaderIconLinkRow/HeaderIconLinkRow.tsx | 1 + packages/core-components/src/translation.ts | 3 + packages/core-plugin-api/report.api.md | 5 + .../src/apis/definitions/auth.ts | 17 + packages/create-app/CHANGELOG.md | 6 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 8 + packages/dev-utils/package.json | 2 +- .../frontend-defaults/src/createApp.test.tsx | 1 + packages/frontend-plugin-api/report.api.md | 3 + .../src/apis/definitions/auth.ts | 1 + ...eInstanceGithubCredentialsProvider.test.ts | 36 + ...SingleInstanceGithubCredentialsProvider.ts | 10 +- packages/repo-tools/CHANGELOG.md | 7 + packages/repo-tools/package.json | 2 +- packages/ui/CHANGELOG.md | 7 + packages/ui/package.json | 2 +- packages/yarn-plugin/package.json | 1 + .../util/getCurrentBackstageVersion.test.ts | 16 +- .../src/util/getCurrentBackstageVersion.ts | 14 +- plugins/api-docs/CHANGELOG.md | 10 + plugins/api-docs/package.json | 2 +- plugins/app-backend/CHANGELOG.md | 10 + plugins/app-backend/package.json | 2 +- plugins/app-backend/src/index.ts | 1 - plugins/app-node/CHANGELOG.md | 7 + plugins/app-node/package.json | 2 +- plugins/app/report.api.md | 15 + plugins/app/src/defaultApis.ts | 22 + .../.eslintrc.js | 1 + .../README.md | 5 + .../catalog-info.yaml | 10 + .../config.d.ts | 44 + .../dev/index.ts | 26 + .../package.json | 55 ++ .../report.api.md | 28 + .../src/authenticator.test.ts | 226 +++++ .../src/authenticator.ts | 184 ++++ .../src/index.ts | 25 + .../src/module.ts | 45 + .../src/resolvers.ts | 68 ++ plugins/auth-backend/CHANGELOG.md | 10 + ...20250909120000_oidc_client_registration.js | 159 ++++ plugins/auth-backend/package.json | 4 +- plugins/auth-backend/report.sql.md | 53 ++ plugins/auth-backend/src/authPlugin.ts | 3 + .../src/database/OidcDatabase.test.ts | 293 ++++++ .../auth-backend/src/database/OidcDatabase.ts | 397 ++++++++ plugins/auth-backend/src/migrations.test.ts | 118 +++ .../src/service/OidcRouter.test.ts | 875 +++++++++++++++--- .../auth-backend/src/service/OidcRouter.ts | 383 +++++++- .../src/service/OidcService.test.ts | 789 ++++++++++++++++ .../auth-backend/src/service/OidcService.ts | 347 ++++++- plugins/auth-backend/src/service/router.ts | 11 + plugins/auth-node/CHANGELOG.md | 8 + plugins/auth-node/package.json | 3 +- plugins/auth-react/CHANGELOG.md | 8 + plugins/auth-react/package.json | 3 +- plugins/auth/.eslintrc.js | 1 + plugins/auth/CHANGELOG.md | 12 + plugins/auth/README.md | 16 + plugins/auth/catalog-info.yaml | 9 + plugins/auth/dev/index.tsx | 28 + plugins/auth/package.json | 84 ++ plugins/auth/report.api.md | 52 ++ .../components/ConsentPage/ConsentPage.tsx | 246 +++++ .../auth/src/components/ConsentPage/index.ts | 16 + .../ConsentPage/useConsentSession.ts | 144 +++ plugins/auth/src/components/Router.tsx | 29 + plugins/auth/src/index.ts | 16 + plugins/auth/src/plugin.test.ts | 22 + plugins/auth/src/plugin.tsx | 36 + plugins/auth/src/routes.ts | 18 + plugins/auth/src/setupTests.ts | 16 + .../catalog-backend-module-aws/CHANGELOG.md | 10 + .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/CHANGELOG.md | 8 + .../catalog-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../catalog-backend-module-gcp/CHANGELOG.md | 7 + .../catalog-backend-module-gcp/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../catalog-backend-module-gitea/CHANGELOG.md | 8 + .../catalog-backend-module-gitea/package.json | 34 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../catalog-backend-module-ldap/CHANGELOG.md | 7 + .../catalog-backend-module-ldap/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 6 +- .../report-alpha.api.md | 13 - .../report.api.md | 5 + .../src/index.ts | 1 + .../catalogModulePuppetDbEntityProvider.ts | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 10 + plugins/catalog-backend/package.json | 2 +- .../stitcher/markForStitching.test.ts | 135 +++ .../operations/stitcher/markForStitching.ts | 118 ++- plugins/catalog-graph/CHANGELOG.md | 10 + plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/CHANGELOG.md | 11 + plugins/catalog-import/package.json | 2 +- plugins/catalog-node/CHANGELOG.md | 28 + plugins/catalog-node/package.json | 2 +- plugins/catalog-react/CHANGELOG.md | 30 + plugins/catalog-react/package.json | 2 +- plugins/catalog/CHANGELOG.md | 11 + plugins/catalog/package.json | 2 +- plugins/catalog/src/routes.ts | 2 +- plugins/devtools-backend/CHANGELOG.md | 8 + plugins/devtools-backend/package.json | 2 +- plugins/home/CHANGELOG.md | 12 + plugins/home/package.json | 2 +- .../CustomHomepage/CustomHomepageGrid.tsx | 19 +- plugins/kubernetes-backend/CHANGELOG.md | 11 + plugins/kubernetes-backend/package.json | 2 +- .../src/service/KubernetesInitializer.ts | 5 +- plugins/kubernetes-cluster/CHANGELOG.md | 8 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 9 + plugins/kubernetes/package.json | 2 +- plugins/mcp-actions-backend/CHANGELOG.md | 10 + plugins/mcp-actions-backend/package.json | 2 +- plugins/mcp-actions-backend/src/plugin.ts | 36 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 9 + plugins/notifications-backend/package.json | 2 +- plugins/notifications-node/CHANGELOG.md | 7 + plugins/notifications-node/package.json | 2 +- plugins/notifications/src/alpha.tsx | 7 +- plugins/org-react/CHANGELOG.md | 9 + plugins/org-react/package.json | 2 +- plugins/org/CHANGELOG.md | 9 + plugins/org/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 12 + plugins/scaffolder-backend/package.json | 2 +- .../notifications-demo/template.yaml | 4 +- .../src/service/router.test.ts | 42 +- .../scaffolder-backend/src/service/router.ts | 9 +- plugins/scaffolder-react/CHANGELOG.md | 10 + plugins/scaffolder-react/package.json | 2 +- .../TaskLogStream/TaskLogStream.tsx | 1 + plugins/scaffolder/CHANGELOG.md | 13 + plugins/scaffolder/package.json | 5 +- .../TemplateListPage.test.tsx | 129 ++- .../TemplateListPage/TemplateListPage.tsx | 24 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/search/CHANGELOG.md | 9 + plugins/search/package.json | 2 +- .../techdocs-addons-test-utils/CHANGELOG.md | 9 + .../techdocs-addons-test-utils/package.json | 2 +- .../src/test-utils.tsx | 13 + plugins/techdocs-backend/CHANGELOG.md | 11 + .../documented-component/catalog-info.yaml | 14 + .../docs/inner-component-docs/index.md | 5 + .../examples/documented-component/mkdocs.yml | 1 + plugins/techdocs-backend/package.json | 2 +- plugins/techdocs-react/src/helpers.ts | 4 +- plugins/techdocs/CHANGELOG.md | 12 + plugins/techdocs/package.json | 4 +- .../TechDocsReaderPage.test.tsx | 155 +++- .../TechDocsReaderPage/TechDocsReaderPage.tsx | 104 ++- .../TechDocsReaderPageHeader.test.tsx | 33 +- .../TechDocsReaderPageHeader.tsx | 5 +- .../transformers/html/hooks/attributes.ts | 32 + .../reader/transformers/html/hooks/iframes.ts | 7 +- .../reader/transformers/html/hooks/index.ts | 2 + .../reader/transformers/html/hooks/links.ts | 8 +- .../transformers/html/hooks/metatags.ts | 41 + .../reader/transformers/html/transformer.ts | 30 +- .../src/reader/transformers/html/utils.ts | 19 + plugins/user-settings/CHANGELOG.md | 10 + plugins/user-settings/package.json | 2 +- plugins/user-settings/report-alpha.api.md | 2 +- .../AuthProviders/DefaultProviderSettings.tsx | 9 + plugins/user-settings/src/translation.ts | 2 +- yarn.lock | 213 +++-- 301 files changed, 8375 insertions(+), 583 deletions(-) create mode 100644 .changeset/angry-heads-design.md create mode 100644 .changeset/better-eagles-tickle.md create mode 100644 .changeset/big-cameras-turn.md create mode 100644 .changeset/brave-jars-speak.md create mode 100644 .changeset/chilly-llamas-attend.md create mode 100644 .changeset/clear-houses-wonder.md create mode 100644 .changeset/cold-garlics-care.md create mode 100644 .changeset/create-app-1757429965.md create mode 100644 .changeset/dirty-spies-drop.md create mode 100644 .changeset/eighty-numbers-act.md create mode 100644 .changeset/eleven-doors-down.md create mode 100644 .changeset/eleven-doors-own.md create mode 100644 .changeset/funny-eagles-try.md create mode 100644 .changeset/giant-buttons-flash.md create mode 100644 .changeset/gold-words-smoke.md create mode 100644 .changeset/heavy-cats-unite.md create mode 100644 .changeset/heavy-lies-listen.md create mode 100644 .changeset/hot-friends-act.md create mode 100644 .changeset/hungry-carrots-grow.md create mode 100644 .changeset/itchy-moons-start.md create mode 100644 .changeset/kind-eyes-worry.md create mode 100644 .changeset/late-swans-press.md create mode 100644 .changeset/lemon-jobs-create.md create mode 100644 .changeset/lovely-actors-love.md create mode 100644 .changeset/quiet-papayas-mate.md create mode 100644 .changeset/red-shrimps-fall.md create mode 100644 .changeset/silent-results-stick.md create mode 100644 .changeset/stupid-areas-share.md create mode 100644 .changeset/tangy-squids-film.md create mode 100644 .changeset/ten-boxes-lie.md create mode 100644 .changeset/thin-phones-press.md create mode 100644 .changeset/tiny-spoons-mix.md create mode 100644 .changeset/tired-cobras-fly.md create mode 100644 .changeset/warm-emus-itch.md create mode 100644 .changeset/wet-onions-sneeze.md create mode 100644 .changeset/young-doodles-enter.md create mode 100644 docs/auth/openshift/provider.md create mode 100644 docs/releases/v1.43.0-next.2-changelog.md delete mode 100644 lerna.json create mode 100644 microsite/data/plugins/datacontract.yaml create mode 100644 microsite/src/components/pluginsSearch/pluginsSearch.tsx create mode 100644 packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts rename plugins/catalog-backend-module-puppetdb/src/alpha.ts => packages/core-app-api/src/apis/implementations/auth/openshift/index.ts (84%) create mode 100644 plugins/auth-backend-module-openshift-provider/.eslintrc.js create mode 100644 plugins/auth-backend-module-openshift-provider/README.md create mode 100644 plugins/auth-backend-module-openshift-provider/catalog-info.yaml create mode 100644 plugins/auth-backend-module-openshift-provider/config.d.ts create mode 100644 plugins/auth-backend-module-openshift-provider/dev/index.ts create mode 100644 plugins/auth-backend-module-openshift-provider/package.json create mode 100644 plugins/auth-backend-module-openshift-provider/report.api.md create mode 100644 plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/authenticator.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/index.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/module.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/resolvers.ts create mode 100644 plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js create mode 100644 plugins/auth-backend/src/database/OidcDatabase.test.ts create mode 100644 plugins/auth-backend/src/database/OidcDatabase.ts create mode 100644 plugins/auth-backend/src/service/OidcService.test.ts create mode 100644 plugins/auth/.eslintrc.js create mode 100644 plugins/auth/CHANGELOG.md create mode 100644 plugins/auth/README.md create mode 100644 plugins/auth/catalog-info.yaml create mode 100644 plugins/auth/dev/index.tsx create mode 100644 plugins/auth/package.json create mode 100644 plugins/auth/report.api.md create mode 100644 plugins/auth/src/components/ConsentPage/ConsentPage.tsx create mode 100644 plugins/auth/src/components/ConsentPage/index.ts create mode 100644 plugins/auth/src/components/ConsentPage/useConsentSession.ts create mode 100644 plugins/auth/src/components/Router.tsx create mode 100644 plugins/auth/src/index.ts create mode 100644 plugins/auth/src/plugin.test.ts create mode 100644 plugins/auth/src/plugin.tsx create mode 100644 plugins/auth/src/routes.ts create mode 100644 plugins/auth/src/setupTests.ts delete mode 100644 plugins/catalog-backend-module-puppetdb/report-alpha.api.md create mode 100644 plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md create mode 100644 plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts create mode 100644 plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts create mode 100644 plugins/techdocs/src/reader/transformers/html/utils.ts diff --git a/.changeset/angry-heads-design.md b/.changeset/angry-heads-design.md new file mode 100644 index 0000000000..0b4a866fb1 --- /dev/null +++ b/.changeset/angry-heads-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Disallow import fallback of critical shared dependencies in module federation. diff --git a/.changeset/better-eagles-tickle.md b/.changeset/better-eagles-tickle.md new file mode 100644 index 0000000000..418f0bcae0 --- /dev/null +++ b/.changeset/better-eagles-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Allow using `BACKSTAGE_ENV` for loading environment specific config files diff --git a/.changeset/big-cameras-turn.md b/.changeset/big-cameras-turn.md new file mode 100644 index 0000000000..6226b17806 --- /dev/null +++ b/.changeset/big-cameras-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. diff --git a/.changeset/brave-jars-speak.md b/.changeset/brave-jars-speak.md new file mode 100644 index 0000000000..2060239326 --- /dev/null +++ b/.changeset/brave-jars-speak.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-puppetdb': patch +--- + +**BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. diff --git a/.changeset/chilly-llamas-attend.md b/.changeset/chilly-llamas-attend.md new file mode 100644 index 0000000000..0a0876ccac --- /dev/null +++ b/.changeset/chilly-llamas-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Updated dependency `dompurify` to `^3.2.4`. diff --git a/.changeset/clear-houses-wonder.md b/.changeset/clear-houses-wonder.md new file mode 100644 index 0000000000..46ada62fbe --- /dev/null +++ b/.changeset/clear-houses-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs page titles have been improved, especially for deeply nested pages. diff --git a/.changeset/cold-garlics-care.md b/.changeset/cold-garlics-care.md new file mode 100644 index 0000000000..960ebe6bd4 --- /dev/null +++ b/.changeset/cold-garlics-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Fixed missing app context when rendering the notifications view diff --git a/.changeset/create-app-1757429965.md b/.changeset/create-app-1757429965.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1757429965.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/dirty-spies-drop.md b/.changeset/dirty-spies-drop.md new file mode 100644 index 0000000000..88bff91892 --- /dev/null +++ b/.changeset/dirty-spies-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/app-defaults': minor +--- + +Add and configure the OpenShift authentication provider to the default APIs. diff --git a/.changeset/eighty-numbers-act.md b/.changeset/eighty-numbers-act.md new file mode 100644 index 0000000000..dc7d504adc --- /dev/null +++ b/.changeset/eighty-numbers-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-react': patch +--- + +Update to documentation regarding TechDocs redirects. diff --git a/.changeset/eleven-doors-down.md b/.changeset/eleven-doors-down.md new file mode 100644 index 0000000000..0d380c8ddf --- /dev/null +++ b/.changeset/eleven-doors-down.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. diff --git a/.changeset/eleven-doors-own.md b/.changeset/eleven-doors-own.md new file mode 100644 index 0000000000..1da0297e5c --- /dev/null +++ b/.changeset/eleven-doors-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. diff --git a/.changeset/funny-eagles-try.md b/.changeset/funny-eagles-try.md new file mode 100644 index 0000000000..c5550756e0 --- /dev/null +++ b/.changeset/funny-eagles-try.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-react': patch +'@backstage/plugin-auth-node': patch +--- + +Updating plugin metadata diff --git a/.changeset/giant-buttons-flash.md b/.changeset/giant-buttons-flash.md new file mode 100644 index 0000000000..97564d37ac --- /dev/null +++ b/.changeset/giant-buttons-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth': minor +--- + +Initial publish of the `auth` frontend package diff --git a/.changeset/gold-words-smoke.md b/.changeset/gold-words-smoke.md new file mode 100644 index 0000000000..85141e402c --- /dev/null +++ b/.changeset/gold-words-smoke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Internal update to not expose the old `createRouter`. diff --git a/.changeset/heavy-cats-unite.md b/.changeset/heavy-cats-unite.md new file mode 100644 index 0000000000..4875093b0a --- /dev/null +++ b/.changeset/heavy-cats-unite.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-home': patch +--- + +Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent +rendering of the default content. diff --git a/.changeset/heavy-lies-listen.md b/.changeset/heavy-lies-listen.md new file mode 100644 index 0000000000..9f408b1ea8 --- /dev/null +++ b/.changeset/heavy-lies-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value diff --git a/.changeset/hot-friends-act.md b/.changeset/hot-friends-act.md new file mode 100644 index 0000000000..37b2e3f112 --- /dev/null +++ b/.changeset/hot-friends-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Make `openshiftAuthApiRef` available in `@backstage/core-plugin-api`. diff --git a/.changeset/hungry-carrots-grow.md b/.changeset/hungry-carrots-grow.md new file mode 100644 index 0000000000..748ace8d94 --- /dev/null +++ b/.changeset/hungry-carrots-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. diff --git a/.changeset/itchy-moons-start.md b/.changeset/itchy-moons-start.md new file mode 100644 index 0000000000..e5768b58b2 --- /dev/null +++ b/.changeset/itchy-moons-start.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. diff --git a/.changeset/kind-eyes-worry.md b/.changeset/kind-eyes-worry.md new file mode 100644 index 0000000000..5568a00e28 --- /dev/null +++ b/.changeset/kind-eyes-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': minor +--- + +Add implementation of OpenShift authentication provider. diff --git a/.changeset/late-swans-press.md b/.changeset/late-swans-press.md new file mode 100644 index 0000000000..987672bd77 --- /dev/null +++ b/.changeset/late-swans-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Prevent deadlock in catalog deferred stitching diff --git a/.changeset/lemon-jobs-create.md b/.changeset/lemon-jobs-create.md new file mode 100644 index 0000000000..d0e28b9c5c --- /dev/null +++ b/.changeset/lemon-jobs-create.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Add the OpenShift authenticator provider to the default `user-settings` providers page. diff --git a/.changeset/lovely-actors-love.md b/.changeset/lovely-actors-love.md new file mode 100644 index 0000000000..b428a11619 --- /dev/null +++ b/.changeset/lovely-actors-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': minor +--- + +Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects. diff --git a/.changeset/pre.json b/.changeset/pre.json index dcb0a48960..b1a840849d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -201,23 +201,42 @@ "@backstage/plugin-techdocs-react": "1.3.2", "@backstage/plugin-user-settings": "0.8.25", "@backstage/plugin-user-settings-backend": "0.3.5", - "@backstage/plugin-user-settings-common": "0.0.1" + "@backstage/plugin-user-settings-common": "0.0.1", + "@backstage/plugin-auth": "0.0.0" }, "changesets": [ + "better-eagles-tickle", + "big-cameras-turn", "brave-bugs-know", + "brave-jars-speak", + "busy-chairs-itch", "cold-donuts-train", "cool-games-rescue", + "create-app-1757429965", "curvy-sites-rhyme", "easy-wings-turn", + "eleven-doors-down", + "eleven-doors-own", "fine-hands-think", + "fix-select-aria-props", "flat-colts-know", + "funny-eagles-try", + "giant-buttons-flash", "giant-zebras-peel", + "gold-words-smoke", + "heavy-cats-unite", + "heavy-lies-listen", "icy-camels-throw", + "itchy-moons-start", + "late-swans-press", "legal-lemons-attend", "lemon-terms-cheer", "lucky-glasses-slide", "mean-sites-cheer", + "olive-moons-burn", "puny-books-fetch", + "quiet-papayas-mate", + "red-shrimps-fall", "ripe-plants-pump", "sharp-carrots-spend", "sixty-pans-prove", @@ -226,9 +245,12 @@ "slick-worms-drum", "social-beers-unite", "sweet-lemons-wonder", + "tangy-squids-film", "tricky-buses-lead", + "warm-emus-itch", "wet-kiwis-strive", "whole-dingos-lay", - "yellow-dragons-float" + "yellow-dragons-float", + "young-doodles-enter" ] } diff --git a/.changeset/quiet-papayas-mate.md b/.changeset/quiet-papayas-mate.md new file mode 100644 index 0000000000..1d2089f4e8 --- /dev/null +++ b/.changeset/quiet-papayas-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Dependency graph can now be opened in full screen mode diff --git a/.changeset/red-shrimps-fall.md b/.changeset/red-shrimps-fall.md new file mode 100644 index 0000000000..1ee3aca7ed --- /dev/null +++ b/.changeset/red-shrimps-fall.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Tool-tip text correction for the Theme selection in settings page diff --git a/.changeset/silent-results-stick.md b/.changeset/silent-results-stick.md new file mode 100644 index 0000000000..6cb501027c --- /dev/null +++ b/.changeset/silent-results-stick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed a regression that prevented uploads greater than 100KB. Uploads up to 10MB are supported again. diff --git a/.changeset/stupid-areas-share.md b/.changeset/stupid-areas-share.md new file mode 100644 index 0000000000..2238f9d942 --- /dev/null +++ b/.changeset/stupid-areas-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Support Techdocs redirect with dompurify 3.2.6+ diff --git a/.changeset/tangy-squids-film.md b/.changeset/tangy-squids-film.md new file mode 100644 index 0000000000..acb5546031 --- /dev/null +++ b/.changeset/tangy-squids-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix scaffolder task log stream not having a minimum height diff --git a/.changeset/ten-boxes-lie.md b/.changeset/ten-boxes-lie.md new file mode 100644 index 0000000000..751b01e7d9 --- /dev/null +++ b/.changeset/ten-boxes-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-openshift-provider': minor +--- + +Add new `auth-backend-module-openshift-provider`. This authentication provider enables Backstage to sign in with OpenShift. diff --git a/.changeset/thin-phones-press.md b/.changeset/thin-phones-press.md new file mode 100644 index 0000000000..8b3f985772 --- /dev/null +++ b/.changeset/thin-phones-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners diff --git a/.changeset/tiny-spoons-mix.md b/.changeset/tiny-spoons-mix.md new file mode 100644 index 0000000000..bb607aca3f --- /dev/null +++ b/.changeset/tiny-spoons-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-addons-test-utils': minor +--- + +Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. diff --git a/.changeset/tired-cobras-fly.md b/.changeset/tired-cobras-fly.md new file mode 100644 index 0000000000..e2e19803f8 --- /dev/null +++ b/.changeset/tired-cobras-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +Make `openshiftApiRef` available to the new frontend system. diff --git a/.changeset/warm-emus-itch.md b/.changeset/warm-emus-itch.md new file mode 100644 index 0000000000..a7bd51abdb --- /dev/null +++ b/.changeset/warm-emus-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fix incorrect `defaultTarget` on `createComponentRouteRef`. diff --git a/.changeset/wet-onions-sneeze.md b/.changeset/wet-onions-sneeze.md new file mode 100644 index 0000000000..0fd61d764a --- /dev/null +++ b/.changeset/wet-onions-sneeze.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +Add `OpenShiftAuth` helper to create default OAuth flow for OpenShift. diff --git a/.changeset/young-doodles-enter.md b/.changeset/young-doodles-enter.md new file mode 100644 index 0000000000..63e1f33eda --- /dev/null +++ b/.changeset/young-doodles-enter.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +update about card links style for pretty display with other language diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 56167d6f9f..516eec516f 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -146,6 +146,7 @@ Expedia facto failover Fargate +faqs featureful Figma firehydrant @@ -460,6 +461,7 @@ subfolders subheader subheaders subkey +subpage subpath subroutes substring diff --git a/app-config.yaml b/app-config.yaml index 60842ee61a..eacf9a96aa 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -209,6 +209,11 @@ scaffolder: defaultCommitMessage: 'Initial commit' auth: + experimentalDynamicClientRegistration: + enabled: true + allowedRedirectUriPatterns: + - cursor://* + ### Add auth.keyStore.provider to more granularly control how to store JWK data when running # the auth-backend. # diff --git a/docs/auth/index.md b/docs/auth/index.md index 9f4c229e7f..bff1f3df26 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -34,6 +34,7 @@ Backstage comes with many common authentication providers in the core library: - [Okta](okta/provider.md) - [OAuth 2 Custom Proxy](oauth2-proxy/provider.md) - [OneLogin](onelogin/provider.md) +- [OpenShift](openshift/provider.md) - [VMware Cloud](vmware-cloud/provider.md) These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a diff --git a/docs/auth/openshift/provider.md b/docs/auth/openshift/provider.md new file mode 100644 index 0000000000..37c5064580 --- /dev/null +++ b/docs/auth/openshift/provider.md @@ -0,0 +1,130 @@ +--- +id: provider +title: OpenShift Authentication Provider +sidebar_label: OpenShift +description: Adding OpenShift OAuth as an authentication provider in Backstage +--- + +The Backstage `core-plugin-api` package comes with a OpenShift authentication +provider that can authenticate users using OpenShift OAuth. + +## Use Case + +This setup enables the [Kubernetes plugin](../../features/kubernetes/index.md) to access OpenShift clusters using the user's permissions, +leveraging OAuth 2.0 _On-Behalf-Of_ flow via the [Kubernetes Client Side Provider](../../features/kubernetes/authentication.md). + +To make this work, the corresponding `User` entities must exist in the Backstage catalog, +and their names must match the OpenShift users. + +Although the OpenShift authentication provider does not support OIDC natively, +you can still configure it for use with the Kubernetes integration by treating it as an OIDC provider +in the `KubernetesAuthProviders` configuration. + +```ts title="packages/app/src/apis.ts" +import { + KubernetesAuthProviders, + kubernetesAuthProvidersApiRef, +} from '@backstage/plugin-kubernetes'; +import { + googleAuthApiRef, + microsoftAuthApiRef, + openshiftAuthApiRef, +} from '@backstage/core-plugin-api'; + +export const apis: AnyApiFactory[] = [ + // ... + createApiFactory({ + api: kubernetesAuthProvidersApiRef, + deps: { + microsoftAuthApi: microsoftAuthApiRef, + googleAuthApi: googleAuthApiRef, + openshiftAuthApi: openshiftAuthApiRef, + }, + factory({ microsoftAuthApi, googleAuthApi, openshiftAuthApi }) { + return new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi, + oidcProviders: { + openshift: { + async getIdToken(_) { + return await openshiftAuthApi.getAccessToken('user:full'); + }, + }, + }, + }); + }, + }), + //... +]; +``` + +:::note Note + +The OpenShift auth API does **not** implement the `OpenIdConnectApi` interface. In other words, it does **not** return an ID token. +Instead, it returns an **access token**, which is used by the Kubernetes integration in place of an ID token. +This is the only functional difference from the standard OIDC-based authentication flow. + +::: + +## Create an OAuth client in OpenShift + +Make sure that an OAuth client exists in the OpenShift cluster. + +To configure the OpenShift integration, create an [`OAuthClient`](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients). + +The redirect URI must be in the following format: `https:///api/auth/openshift/handler/frame`. + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the +root `auth` configuration: + +```yaml +auth: + environment: development + providers: + openshift: + development: + clientId: ${AUTH_OPENSHIFT_CLIENT_ID} + clientSecret: ${AUTH_OPENSHIFT_CLIENT_SECRET} + authorizationUrl: ${AUTH_OPENSHIFT_AUTHORIZATION_URL} + tokenUrl: ${AUTH_OPENSHIFT_TOKEN_URL} + openshiftApiServerUrl: ${OPENSHIFT_API_SERVER_URL} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code + # sessionDuration: 1d + signIn: + resolvers: + - resolver: displayNameMatchingUserEntityName +``` + +The OpenShift provider is a structure with these configuration keys: + +- `clientId`: The client ID of your OpenShift OAuth client, e.g., `my-backstage` +- `clientSecret`: The client secret tied to the OpenShift OAuth client. +- `authorizationUrl`: The OpenShift OAuth client auth endpoint, format: `https:///oauth/authorize`. +- `tokenUrl`: The OpenShift OAuth client token endpoint, format: `https:///oauth/token`. +- `openshiftApiServerUrl`: The OpenShift API server endpoint, format: `https://`. +- `sessionDuration`: (optional): Lifespan of the user session. +- `signIn`: The configuration for the sign-in process, including the **resolvers** + that should be used to match the user from the auth provider with the user + entity in the Backstage catalog (typically a single resolver is sufficient). + +The provider needs to use the scope **user:full**. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-openshift-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); +/* highlight-add-end */ +``` diff --git a/docs/backend-system/core-services/actions-registry.md b/docs/backend-system/core-services/actions-registry.md index b1e8fe5f3d..9e493e6bb2 100644 --- a/docs/backend-system/core-services/actions-registry.md +++ b/docs/backend-system/core-services/actions-registry.md @@ -45,7 +45,7 @@ When an action is executed, it receives a context object (`ActionsRegistryAction Here's an example of how to register an action with the Actions Registry Service: ```typescript -import { ActionsRegistryService } from '@backstage/backend-plugin-api'; +import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; export function registerMyActions(actionsRegistry: ActionsRegistryService) { // Register a simple read-only action diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md index 0e26e8e1f5..ad4d04b50a 100644 --- a/docs/backend-system/core-services/cache.md +++ b/docs/backend-system/core-services/cache.md @@ -7,6 +7,39 @@ description: Documentation for the Cache service This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. +## Configuration + +The cache service can be configured using the `backend.cache` section in your `app-config.yaml`: + +```yaml +backend: + cache: + store: redis # or 'valkey', 'memcache', 'memory' + connection: redis://localhost:6379 + + # Store-specific configuration (Redis/Valkey only) + redis: + client: + # Optional: Global namespace prefix for all cache keys + namespace: 'my-app' + # Optional: Separator used between namespace and plugin ID (default: ':') + keyPrefixSeparator: ':' + # Other Redis-specific options... + clearBatchSize: 1000 + useUnlink: false +``` + +### Namespace Configuration + +For Redis and Valkey stores, you can configure a global namespace that will be prefixed to all cache keys: + +- **Without namespace**: Cache keys use only the plugin ID (e.g., `catalog:some-key`) +- **With namespace**: Cache keys use the format `namespace:pluginId:key` (e.g., `my-app:catalog:some-key`) + +The `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). + +**Note**: Memory and Memcache stores do not support namespace configuration and will always use the plugin ID directly. + ## Using the service The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. diff --git a/docs/conf/index.md b/docs/conf/index.md index 659ffccfe3..029c5b88fd 100644 --- a/docs/conf/index.md +++ b/docs/conf/index.md @@ -16,10 +16,24 @@ allowing for customization. ## Supplying Configuration Configuration is stored in YAML files where the defaults are `app-config.yaml` -and `app-config.local.yaml` for local overrides. Other sets of files can by -loaded by passing `--config ` flags. The configuration files themselves -contain plain YAML, but with support for loading in data and secrets from -various sources using for example `$env` and `$file` keys. +and `app-config.local.yaml` for local overrides. Additionally, it is possible +to define environment based configuration files with `BACKSTAGE_ENV` +environment variable, which will load `app-config..yaml`. + +Loading order of these files is as follows: + +1. `app-config.yaml` +2. `app-config..yaml` +3. `app-config.local.yaml` +4. `app-config..local.yaml` + +Other sets of files can by loaded by passing `--config ` flags. +Read more about the configuration loading order in the +[Configuration Files](./writing.md#configuration-files) section. + +The configuration files themselves contain plain YAML, but with support for +loading in data and secrets from various sources using for example +`$env` and `$file` keys. It is also possible to supply configuration through environment variables, for example `APP_CONFIG_app_baseUrl=https://staging.example.com`. However these diff --git a/docs/contribute/project-structure.md b/docs/contribute/project-structure.md index 5b86b655b2..f58afedee8 100644 --- a/docs/contribute/project-structure.md +++ b/docs/contribute/project-structure.md @@ -221,7 +221,3 @@ future. - [`catalog-info.yaml`](https://github.com/backstage/backstage/tree/master/catalog-info.yaml) - Description of Backstage in the Backstage Entity format. - -- [`lerna.json`](https://github.com/backstage/backstage/tree/master/lerna.json) - - [Lerna](https://github.com/lerna/lerna) monorepo config. We are using - `yarn workspaces`, so this will only be used for executing scripts. diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index cdd93b50ac..d8a2e1d232 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -126,7 +126,7 @@ metadata: The value of this annotation informs of the path to this component's TechDocs within an external entity that owns the TechDocs. In conjunction with [backstage.io/techdocs-entity](#backstageiotechdocs-entity) this allows for deep linking into the TechDocs of -another entity, not just linking to the root of another entities TechDocs. +another entity, not just linking to the root of another entity's TechDocs. ### backstage.io/view-url, backstage.io/edit-url diff --git a/docs/features/techdocs/FAQ.md b/docs/features/techdocs/FAQ.md index 117471f749..9c78e3e123 100644 --- a/docs/features/techdocs/FAQ.md +++ b/docs/features/techdocs/FAQ.md @@ -12,6 +12,8 @@ This page answers frequently asked questions about [TechDocs](README.md). - [What static site generator is TechDocs using?](#what-static-site-generator-is-techdocs-using) - [What is the mkdocs-techdocs-core plugin?](#what-is-the-mkdocs-techdocs-core-plugin) - [Does TechDocs support file formats other than Markdown (e.g. RST, AsciiDoc)?](#does-techdocs-support-file-formats-other-than-markdown-eg-rst-asciidoc-) +- [What should be the value of `backstage.io/techdocs-ref` when using external build and storage?](#what-should-be-the-value-of-backstageiotechdocs-ref-when-using-external-build-and-storage) +- [Is it possible for users to suggest changes or provide feedback on a TechDocs page?](#is-it-possible-for-users-to-suggest-changes-or-provide-feedback-on-a-techdocs-page) #### What static site generator is TechDocs using? @@ -46,6 +48,10 @@ annotation should still be present in entity descriptor file (e.g. `catalog-info.yaml`) for Backstage to know that TechDocs is enabled for the entity. +#### What happens when you navigate to a TechDocs URL for an entity uses the `backstage.io/techdocs-entity` annotation? + +If you navigate to a TechDocs URL in the format `docs/{namespace}/{kind}/{name}` for an entity that has the `backstage.io/techdocs-entity` annotation (instead of the `backstage.io/techdocs-ref` annotation), then Backstage will redirect to the TechDocs page of the entity referenced in the value of that annotation. + #### Is it possible for users to suggest changes or provide feedback on a TechDocs page? This is supported for TechDocs sites whose source code is hosted in either diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 283c162fa7..03da20ea73 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -185,7 +185,7 @@ export const examplePlugin = createFrontendPlugin({ ## Plugin specific extensions -There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](../../features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. +There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](https://backstage.io/docs/features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. ```tsx title="in src/plugin.ts - An example entity content extension" import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index cd86aa3343..252884dc54 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -31,7 +31,7 @@ This guide also assumes a basic understanding of working on a Linux based operat [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) - A GNU-like build environment available at the command line. For example, on Debian/Ubuntu you will want to have the `make` and `build-essential` packages installed. - On macOS, you will want to have run `xcode-select --install` to get the XCode command line build tooling in place. + On macOS, you will want to run `xcode-select --install` to get the XCode command line build tooling in place. - An account with elevated rights to install the dependencies - `curl` or `wget` installed - Node.js [Active LTS Release](../overview/versioning-policy.md#nodejs-releases) installed using one of these @@ -58,9 +58,9 @@ The Backstage app we'll be creating will only have demo data until we set up int ::: -To install the Backstage Standalone app, we will make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. Before we jump in to running the command, let's chat about what it does. +To install the Backstage Standalone app, we will make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. Before we run the command, let's discuss what it does. -This command will create a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as sub directory in your current working directory. +This command will create a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as subdirectory in your current working directory. ![create app](../assets/getting-started/create-app-output.png) @@ -89,8 +89,8 @@ app - **package.json**: Root package.json for the project. _Note: Be sure that you don't add any npm dependencies here as they probably should be installed in the intended workspace rather than in the root._ -- **packages/**: Lerna leaf packages or "workspaces". Everything here is going - to be a separate package, managed by lerna. +- **packages/**: Yarn workspaces, everything here is going + to be a separate package, managed by Yarn. - **packages/app/**: A fully functioning Backstage frontend app that acts as a good starting point for you to get to know Backstage. - **packages/backend/**: We include a backend that helps power features such as diff --git a/docs/references/glossary.md b/docs/references/glossary.md index c955d9a5ae..ad9e2bbee5 100644 --- a/docs/references/glossary.md +++ b/docs/references/glossary.md @@ -208,7 +208,7 @@ One of the [packages](#package) within a [monorepo](#monorepo). A package may or 1. A single repository for a collection of related software projects, such as all projects belonging to an organization. -2. A project layout that consists of multiple [packages](#package) within a single project, where packages are able to have local dependencies on each other. Often enabled through tooling such as [lerna](https://lerna.js.org/) and [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) +2. A project layout that consists of multiple [packages](#package) within a single project, where packages are able to have local dependencies on each other. Often enabled through tooling such as [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) ## Name diff --git a/docs/releases/v1.43.0-next.2-changelog.md b/docs/releases/v1.43.0-next.2-changelog.md new file mode 100644 index 0000000000..ef7ddb1c1f --- /dev/null +++ b/docs/releases/v1.43.0-next.2-changelog.md @@ -0,0 +1,695 @@ +# Release v1.43.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.43.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.43.0-next.2) + +## @backstage/catalog-client@1.12.0-next.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +## @backstage/plugin-auth@0.1.0-next.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-catalog-node@1.19.0-next.1 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-catalog-react@1.21.0-next.2 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/backend-defaults@0.12.1-next.1 + +### Patch Changes + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/backend-dynamic-feature-service@0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + +## @backstage/cli@0.34.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/integration@1.18.0-next.0 + +## @backstage/config-loader@1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + +## @backstage/core-compat-api@0.5.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + +## @backstage/core-components@0.17.6-next.1 + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- ae7d426: update about card links style for pretty display with other language + +## @backstage/create-app@0.7.4-next.2 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/repo-tools@0.15.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + +## @backstage/ui@0.7.1-next.0 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. + +## @backstage/plugin-api-docs@0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-app-backend@0.5.6-next.1 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + +## @backstage/plugin-app-node@0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + +## @backstage/plugin-auth-backend@0.25.4-next.1 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-auth-node@0.6.7-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-auth-react@0.1.19-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-catalog@1.31.3-next.2 + +### Patch Changes + +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-catalog-backend@3.0.2-next.1 + +### Patch Changes + +- 2204f5b: Prevent deadlock in catalog deferred stitching +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/plugin-catalog-backend-module-azure@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.11.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.14-next.1 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-graph@0.4.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-catalog-import@0.13.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-devtools-backend@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + +## @backstage/plugin-home@0.8.12-next.2 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-kubernetes@0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-kubernetes-backend@0.20.2-next.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-mcp-actions-backend@0.1.3-next.1 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-notifications-backend@0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-backend-module-slack@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-node@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-org@0.6.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-org-react@0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-scaffolder@1.34.1-next.2 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-scaffolder-backend@2.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-scaffolder-react@1.19.1-next.2 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-search@1.4.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-search-backend-module-catalog@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-techdocs@1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.53-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + +## @backstage/plugin-techdocs-backend@2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + +## @backstage/plugin-user-settings@0.8.26-next.2 + +### Patch Changes + +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## example-app@0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + +## example-app-next@0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-auth@0.1.0-next.0 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 diff --git a/lerna.json b/lerna.json deleted file mode 100644 index 4621689664..0000000000 --- a/lerna.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "packages": ["packages/*", "plugins/*"], - "npmClient": "yarn", - "useWorkspaces": true, - "version": "0.0.0" -} diff --git a/microsite/data/plugins/datacontract.yaml b/microsite/data/plugins/datacontract.yaml new file mode 100644 index 0000000000..a267730496 --- /dev/null +++ b/microsite/data/plugins/datacontract.yaml @@ -0,0 +1,10 @@ +--- +title: Data Contract +author: Jan Remunda +authorUrl: https://www.remunda.cz/#backstage +category: Discovery +description: Ingest and visualize Data Contracts to your catalog via API Entities. +documentation: https://www.npmjs.com/package/@remunda/backstage-plugin-datacontract +iconUrl: https://www.remunda.cz/logos/datacontract.png +npmPackageName: '@remunda/backstage-plugin-datacontract' +addedDate: '2025-08-19' diff --git a/microsite/src/components/pluginsSearch/pluginsSearch.tsx b/microsite/src/components/pluginsSearch/pluginsSearch.tsx new file mode 100644 index 0000000000..68f9a2831a --- /dev/null +++ b/microsite/src/components/pluginsSearch/pluginsSearch.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +type Props = { + searchTerm: string; + onSearchTermChange: (newTerm: string) => void; +}; + +export const PluginsSearch = (props: Props) => { + const { searchTerm, onSearchTermChange } = props; + return ( + onSearchTermChange((e.target as HTMLInputElement).value)} + value={searchTerm} + placeholder="Search plugins..." + className="DocSearch-Input search" + /> + ); +}; diff --git a/microsite/src/pages/plugins/index.tsx b/microsite/src/pages/plugins/index.tsx index 2abdbabf4b..1a87f9c0af 100644 --- a/microsite/src/pages/plugins/index.tsx +++ b/microsite/src/pages/plugins/index.tsx @@ -5,10 +5,11 @@ import { truncateDescription } from '@site/src/util/truncateDescription'; import { ChipCategory } from '@site/src/util/types'; import Layout from '@theme/Layout'; import clsx from 'clsx'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { IPluginData, PluginCard } from './_pluginCard'; import pluginsStyles from './plugins.module.scss'; +import { PluginsSearch } from '@site/src/components/pluginsSearch/pluginsSearch'; interface IPluginsList { corePlugins: IPluginData[]; @@ -55,6 +56,7 @@ const Plugins = () => { const [selectedCategories, setSelectedCategories] = useState([]); const [showCoreFeatures, setShowCoreFeatures] = useState(true); const [showOtherPlugins, setShowOtherPlugins] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); const handleChipClick = (categoryName: string) => { const isSelected = @@ -97,6 +99,34 @@ const Plugins = () => { } }; + const matchesSearch = (pluginData: IPluginData, term: string) => { + if (!term) return true; + const lowerTerm = term.toLowerCase(); + return ( + pluginData.title.toLowerCase().includes(lowerTerm) || + pluginData.description.toLowerCase().includes(lowerTerm) || + pluginData.category.toLowerCase().includes(lowerTerm) || + (pluginData.author && pluginData.author.toLowerCase().includes(lowerTerm)) + ); + }; + + const matchesCategory = (pluginData: IPluginData, categories: string[]) => { + if (categories.length === 0) return true; + return categories.includes(pluginData.category); + }; + + const corePlugins = useMemo(() => { + return plugins.corePlugins + .filter(pluginData => matchesCategory(pluginData, selectedCategories)) + .filter(pluginData => matchesSearch(pluginData, searchTerm)); + }, [selectedCategories, searchTerm]); + + const otherPlugins = useMemo(() => { + return plugins.otherPlugins + .filter(pluginData => matchesCategory(pluginData, selectedCategories)) + .filter(pluginData => matchesSearch(pluginData, searchTerm)); + }, [selectedCategories, searchTerm]); + return (
{ categories={categories} handleChipClick={handleChipClick} /> +
- {showCoreFeatures && ( + {corePlugins.length === 0 && otherPlugins.length === 0 && ( +
+

No plugins found

+

+ We couldn't find any plugins matching your criteria. Please try + adjusting your search or filter settings. +

+
+ )} + + {showCoreFeatures && corePlugins.length > 0 && (
-

Core Features

+

Core Features ({corePlugins.length})

- {plugins.corePlugins - .filter( - pluginData => - !selectedCategories.length || - selectedCategories.includes(pluginData.category), - ) - .map(pluginData => ( - - ))} + {corePlugins.map(pluginData => ( + + ))}
)} - {showOtherPlugins && ( + {showOtherPlugins && otherPlugins.length > 0 && (
-

All Plugins

+

All Plugins ({otherPlugins.length})

Friendly reminder: While we love the variety and contributions of our open source plugins, they haven't been fully vetted by the @@ -158,18 +193,9 @@ const Plugins = () => { your due diligence before installing. Happy exploring!

- {plugins.otherPlugins - .filter( - pluginData => - !selectedCategories.length || - selectedCategories.includes(pluginData.category), - ) - .map(pluginData => ( - - ))} + {otherPlugins.map(pluginData => ( + + ))}
)} diff --git a/microsite/src/pages/plugins/plugins.module.scss b/microsite/src/pages/plugins/plugins.module.scss index fb17e90625..a17e4bc5c0 100644 --- a/microsite/src/pages/plugins/plugins.module.scss +++ b/microsite/src/pages/plugins/plugins.module.scss @@ -76,6 +76,20 @@ height: 1rem; } + :global(.search) { + float: right; + margin-bottom: 1rem; + margin-right: 1rem; + font-size: calc(0.875rem * var(--ifm-button-size-multiplier)); + font-weight: var(--ifm-button-font-weight); + background-color: transparent; + border: var(--ifm-button-border-width) solid var(--ifm-color-primary); + border-radius: var(--ifm-button-border-radius); + color: var(--ifm-color-primary); + width: 220px; + height: 2.2rem; + } + :global(.dropdown) { float: right; margin-bottom: 1rem; diff --git a/package.json b/package.json index d4ebc22bc1..7aed18ab74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.43.0-next.1", + "version": "1.43.0-next.2", "backstage": { "cli": { "new": { diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 63ace7b4d5..97989dff91 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -35,6 +35,7 @@ import { FetchMiddlewares, VMwareCloudAuth, FrontendHostDiscovery, + OpenShiftAuth, } from '@backstage/core-app-api'; import { @@ -58,6 +59,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { permissionApiRef, @@ -275,6 +277,22 @@ export const apis = [ }); }, }), + createApiFactory({ + api: openshiftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return OpenShiftAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), createApiFactory({ api: permissionApiRef, deps: { diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 5fc353c09d..a4e2386525 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,32 @@ # example-app-next +## 0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-auth@0.1.0-next.0 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + ## 0.0.27-next.1 ### Patch Changes diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index f83e4d4ba0..35380c961f 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -4,7 +4,6 @@ app: routes: bindings: catalog.viewTechDoc: techdocs.docRoot - catalog.createComponent: catalog-import.importPage org.catalogIndex: catalog.catalogIndex pluginOverrides: diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 44602b2438..49ce264f06 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.27-next.1", + "version": "0.0.27-next.2", "backstage": { "role": "frontend" }, @@ -49,6 +49,7 @@ "@backstage/plugin-api-docs": "workspace:^", "@backstage/plugin-app": "workspace:^", "@backstage/plugin-app-visualizer": "workspace:^", + "@backstage/plugin-auth": "workspace:^", "@backstage/plugin-auth-react": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 93dd1db16a..f3b911accf 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,30 @@ # example-app +## 0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + ## 0.2.113-next.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 580c401a1b..5e38aa6256 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.113-next.1", + "version": "0.2.113-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 66f1460210..372477cd63 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -23,6 +23,7 @@ import { oneloginAuthApiRef, bitbucketAuthApiRef, bitbucketServerAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ @@ -74,4 +75,10 @@ export const providers = [ message: 'Sign In using Bitbucket Server', apiRef: bitbucketServerAuthApiRef, }, + { + id: 'openshift-auth-provider', + title: 'OpenShift', + message: 'Sign In using OpenShift', + apiRef: openshiftAuthApiRef, + }, ]; diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 0375e54232..b4c7393455 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-defaults +## 0.12.1-next.1 + +### Patch Changes + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + ## 0.12.1-next.0 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 09bb4dc85d..1b1f0101c7 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.12.1-next.0", + "version": "0.12.1-next.1", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts index 4e9475b66e..a308caa222 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts @@ -333,4 +333,147 @@ describe('CacheManager store options', () => { keyPrefixSeparator: '!', }); }); + + it('correctly applies namespace configuration to redis and valkey stores', () => { + const testCases = [ + { store: 'redis', namespace: 'test1', separator: ':' }, + { store: 'valkey', namespace: 'test2', separator: '!' }, + ]; + + testCases.forEach(({ store, namespace, separator }) => { + const manager = CacheManager.fromConfig( + mockServices.rootConfig({ + data: { + backend: { + cache: { + store, + connection: 'redis://localhost:6379', + [store]: { + client: { + namespace, + keyPrefixSeparator: separator, + }, + }, + }, + }, + }, + }), + ); + + manager.forPlugin('testPlugin'); + + if (store === 'redis') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { + namespace, + keyPrefixSeparator: separator, + }); + } else if (store === 'valkey') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvValkey).toHaveBeenCalledWith('redis://localhost:6379', { + namespace, + keyPrefixSeparator: separator, + }); + } + }); + }); + + it('falls back to pluginId when no namespace is configured', () => { + const manager = CacheManager.fromConfig( + mockServices.rootConfig({ + data: { + backend: { + cache: { + store: 'redis', + connection: 'redis://localhost:6379', + }, + }, + }, + }), + ); + + manager.forPlugin('testPlugin'); + + expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { + keyPrefixSeparator: ':', + }); + }); + + describe('Namespace construction', () => { + it('returns pluginId when no store options are provided', () => { + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + undefined, + ); + expect(result).toBe('testPlugin'); + }); + + it('returns pluginId when store options have no namespace', () => { + const storeOptions = { + client: { + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }); + + it('combines namespace and pluginId with default separator', () => { + const storeOptions = { + client: { + namespace: 'my-app', + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app:testPlugin'); + }); + + it('combines namespace and pluginId with custom separator', () => { + const storeOptions = { + client: { + namespace: 'my-app', + keyPrefixSeparator: '-', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app-testPlugin'); + }); + + it('uses default separator when keyPrefixSeparator is not provided', () => { + const storeOptions = { + client: { + namespace: 'my-app', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app:testPlugin'); + }); + + it('handles empty namespace by falling back to pluginId', () => { + const storeOptions = { + client: { + namespace: '', + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 1c26083525..06b2b0c57b 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -213,6 +213,26 @@ export class CacheManager { return redisOptions; } + /** + * Construct the full namespace based on the options and pluginId. + * + * @param pluginId - The plugin ID to namespace + * @param storeOptions - Optional cache store configuration options + * @returns The constructed namespace string combining the configured namespace with pluginId + */ + private static constructNamespace( + pluginId: string, + storeOptions: RedisCacheStoreOptions | undefined, + ): string { + const prefix = storeOptions?.client?.namespace + ? `${storeOptions.client.namespace}${ + storeOptions.client.keyPrefixSeparator ?? ':' + }` + : ''; + + return `${prefix}${pluginId}`; + } + /** @internal */ constructor( store: string, @@ -286,7 +306,7 @@ export class CacheManager { }); } return new Keyv({ - namespace: pluginId, + namespace: CacheManager.constructNamespace(pluginId, this.storeOptions), ttl: defaultTtl, store: stores[pluginId], emitErrors: false, @@ -326,7 +346,7 @@ export class CacheManager { }); } return new Keyv({ - namespace: pluginId, + namespace: CacheManager.constructNamespace(pluginId, this.storeOptions), ttl: defaultTtl, store: stores[pluginId], emitErrors: false, diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index 6bd6a7a781..d2e46404ef 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-dynamic-feature-service +## 0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + ## 0.7.4-next.0 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index ff8fc29639..6a7cc2315f 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dynamic-feature-service", - "version": "0.7.4-next.0", + "version": "0.7.4-next.1", "description": "Backstage dynamic feature service", "backstage": { "role": "node-library" diff --git a/packages/backend/package.json b/packages/backend/package.json index a60b6d3940..64c2f1dd47 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,7 @@ "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f46e947b0d..e32d3148b3 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,6 +33,7 @@ const searchLoader = createBackendFeatureLoader({ backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('./authModuleGithubProvider')); backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); +backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); backend.add(import('@backstage/plugin-app-backend')); backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); backend.add( diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 52e2ed04a4..254f37bd69 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/catalog-client +## 1.12.0-next.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + ## 1.11.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index a87ed29302..3b9d1c603a 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.11.0", + "version": "1.12.0-next.0", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 87be49efce..bdaf3a05f6 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/cli +## 0.34.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/integration@1.18.0-next.0 + ## 0.34.2-next.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 51090239d8..8956b50713 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.2-next.1", + "version": "0.34.2-next.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index c4011ede48..bf78d04b4a 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -264,24 +264,30 @@ export async function createConfig( singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, 'react-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, // React Router 'react-router': { singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, 'react-router-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, // MUI v4 + // not setting import: false for MUI packages as this + // will break once Backstage moves to BUI '@material-ui/core/styles': { singleton: true, requiredVersion: '*', @@ -293,6 +299,8 @@ export async function createConfig( eager: !isRemote, }, // MUI v5 + // not setting import: false for MUI packages as this + // will break once Backstage moves to BUI '@mui/material/styles/': { singleton: true, requiredVersion: '*', diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 6fe939f3e6..a7b38c24b9 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config-loader +## 1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + ## 1.10.2 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index f347af8034..b0e7af3b5e 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.2", + "version": "1.10.3-next.0", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/config-loader/src/schema/collect.test.ts b/packages/config-loader/src/schema/collect.test.ts index 53db5c6da7..d48c98fbe4 100644 --- a/packages/config-loader/src/schema/collect.test.ts +++ b/packages/config-loader/src/schema/collect.test.ts @@ -41,16 +41,6 @@ describe('collectConfigSchemas', () => { mockDir.clear(); }); - it('should not find any schemas without packages', async () => { - mockDir.setContent({ - 'lerna.json': JSON.stringify({ - packages: ['packages/*'], - }), - }); - - await expect(collectConfigSchemas([], [])).resolves.toEqual([]); - }); - it('should find schema in a local package', async () => { mockDir.setContent({ node_modules: { diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index e19ac1d51f..a6b0648889 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -89,6 +89,19 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: `${root}app-config.yaml` }, { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, ]); + + process.env = Object.assign(process.env, { BACKSTAGE_ENV: 'test' }); + expect( + mergeSources( + ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), + ), + ).toEqual([ + { name: 'FileConfigSource', path: `${root}app-config.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.test.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.test.local.yaml` }, + ]); + fsSpy.mockRestore(); expect( diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 37c1c6b087..18d0443986 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -182,6 +182,14 @@ export class ConfigSources { if (argSources.length === 0) { const defaultPath = resolvePath(rootDir, 'app-config.yaml'); const localPath = resolvePath(rootDir, 'app-config.local.yaml'); + const envPath = resolvePath( + rootDir, + `app-config.${process.env.BACKSTAGE_ENV}.yaml`, + ); + const envLocalPath = resolvePath( + rootDir, + `app-config.${process.env.BACKSTAGE_ENV}.local.yaml`, + ); const alwaysIncludeDefaultConfigSource = !options.allowMissingDefaultConfig; @@ -195,6 +203,16 @@ export class ConfigSources { ); } + if (process.env.BACKSTAGE_ENV && fs.pathExistsSync(envPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: envPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } + if (fs.pathExistsSync(localPath)) { argSources.push( FileConfigSource.create({ @@ -204,6 +222,16 @@ export class ConfigSources { }), ); } + + if (process.env.BACKSTAGE_ENV && fs.pathExistsSync(envLocalPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: envLocalPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } } return this.merge(argSources); diff --git a/packages/core-app-api/report.api.md b/packages/core-app-api/report.api.md index 445d73392d..0cecc22f4a 100644 --- a/packages/core-app-api/report.api.md +++ b/packages/core-app-api/report.api.md @@ -52,6 +52,7 @@ import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -651,6 +652,12 @@ export type OpenLoginPopupOptions = { height?: number; }; +// @public +export class OpenShiftAuth { + // (undocumented) + static create(options: OAuthApiCreateOptions): typeof openshiftAuthApiRef.T; +} + // @public export type PopupOptions = { size?: diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index e02e07961a..00effb9384 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -26,4 +26,5 @@ export * from './bitbucket'; export * from './bitbucketServer'; export * from './atlassian'; export * from './vmwareCloud'; +export * from './openshift'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts new file mode 100644 index 0000000000..1d820189d2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts @@ -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 { openshiftAuthApiRef } from '@backstage/core-plugin-api'; +import { OAuth2 } from '../oauth2'; +import { OAuthApiCreateOptions } from '../types'; + +const DEFAULT_PROVIDER = { + id: 'openshift', + title: 'OpenShift', + icon: () => null, +}; + +/** + * Implements the OAuth flow to OpenShift + * + * @public + */ +export default class OpenShiftAuth { + static create(options: OAuthApiCreateOptions): typeof openshiftAuthApiRef.T { + const { + configApi, + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['user:info'], + } = options; + + return OAuth2.create({ + configApi, + discoveryApi, + oauthRequestApi, + provider, + environment, + defaultScopes, + }); + } +} diff --git a/plugins/catalog-backend-module-puppetdb/src/alpha.ts b/packages/core-app-api/src/apis/implementations/auth/openshift/index.ts similarity index 84% rename from plugins/catalog-backend-module-puppetdb/src/alpha.ts rename to packages/core-app-api/src/apis/implementations/auth/openshift/index.ts index 01a9b25f7c..65452114ae 100644 --- a/plugins/catalog-backend-module-puppetdb/src/alpha.ts +++ b/packages/core-app-api/src/apis/implementations/auth/openshift/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * 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. @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './module'; -export { default } from './module'; +export { default as OpenShiftAuth } from './OpenShiftAuth'; diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index a966469313..ea40b301bb 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-compat-api +## 0.5.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + ## 0.5.2-next.1 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 1430973106..db51762180 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.5.2-next.1", + "version": "0.5.2-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 515e6eac5b..10e04ae0c6 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-components +## 0.17.6-next.1 + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- ae7d426: update about card links style for pretty display with other language + ## 0.17.6-next.0 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index be91c5dbd5..5cd6b9f270 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.17.6-next.0", + "version": "0.17.6-next.1", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" @@ -80,6 +80,7 @@ "pluralize": "^8.0.0", "qs": "^6.9.4", "rc-progress": "3.5.1", + "react-full-screen": "^1.1.1", "react-helmet": "6.1.0", "react-hook-form": "^7.12.2", "react-idle-timer": "5.7.2", @@ -106,6 +107,7 @@ "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^3.0.1", + "@types/fscreen": "^1", "@types/google-protobuf": "^3.7.2", "@types/react": "^18.0.0", "@types/react-helmet": "^6.1.0", diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index 32bd2f4939..96ddebcd3a 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -61,6 +61,7 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'alertDisplay.message_other': '({{ count }} newer messages)'; readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; + readonly 'dependencyGraph.fullscreenTooltip': 'Toggle fullscreen'; readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; } >; diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 4998accf01..c1c9d4ad5b 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -258,6 +258,7 @@ export interface DependencyGraphProps extends SVGProps { acyclicer?: 'greedy'; align?: DependencyGraphTypes.Alignment; + allowFullscreen?: boolean; curve?: 'curveStepBefore' | 'curveMonotoneX'; defs?: JSX.Element | JSX.Element[]; direction?: DependencyGraphTypes.Direction; diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx index 6535808059..0dbf3a36b9 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { render } from '@testing-library/react'; import { DependencyGraph } from './DependencyGraph'; import { DependencyGraphTypes as Types } from './types'; import { EDGE_TEST_ID, LABEL_TEST_ID, NODE_TEST_ID } from './constants'; +import { renderInTestApp } from '@backstage/test-utils'; describe('', () => { beforeAll(() => { @@ -36,9 +36,8 @@ describe('', () => { const CUSTOM_TEST_ID = 'custom-test-id'; it('renders each node and edge supplied', async () => { - const { getByText, queryAllByTestId, findAllByTestId } = render( - , - ); + const { getByText, queryAllByTestId, findAllByTestId } = + await renderInTestApp(); const renderedNodes = await findAllByTestId(NODE_TEST_ID); expect(renderedNodes).toHaveLength(3); expect(getByText(nodes[0].id)).toBeInTheDocument(); @@ -49,9 +48,10 @@ describe('', () => { }); it('update render if already referenced nodes are added later', async () => { - const { getByText, queryAllByTestId, findAllByTestId, rerender } = render( - , - ); + const { getByText, queryAllByTestId, findAllByTestId, rerender } = + await renderInTestApp( + , + ); let renderedNodes = await findAllByTestId(NODE_TEST_ID); expect(renderedNodes).toHaveLength(2); @@ -75,9 +75,10 @@ describe('', () => { { ...edges[0], label: 'first' }, { ...edges[1], label: 'second' }, ]; - const { getByText, getAllByTestId, findAllByTestId } = render( - , - ); + const { getByText, getAllByTestId, findAllByTestId } = + await renderInTestApp( + , + ); const renderedEdges = await findAllByTestId(EDGE_TEST_ID); expect(renderedEdges).toHaveLength(2); expect(getAllByTestId(LABEL_TEST_ID)).toHaveLength(2); @@ -94,7 +95,7 @@ describe('', () => { ); - const { getByText, findByTestId, container } = render( + const { getByText, findByTestId, container } = await renderInTestApp( , ); const node = await findByTestId(CUSTOM_TEST_ID); @@ -112,7 +113,7 @@ describe('', () => { ); - const { getByText, findByTestId, container } = render( + const { getByText, findByTestId, container } = await renderInTestApp( ({ + root: { + overflow: 'hidden', + minHeight: '100%', + minWidth: '100%', + }, + fullscreen: { + backgroundColor: theme.palette.background.paper, + }, +})); /** * Properties of {@link DependencyGraph} @@ -142,7 +161,6 @@ export interface DependencyGraphProps * Custom edge rendering component */ renderEdge?: Types.RenderEdgeFunction; - /** * Custom node rendering component */ @@ -186,9 +204,18 @@ export interface DependencyGraphProps * Default: 'grow' */ fit?: 'grow' | 'contain'; + /** + * Controls if user can toggle fullscreen mode + * + * @remarks + * + * Default: true + */ + allowFullscreen?: boolean; } const WORKSPACE_ID = 'workspace'; +const DEPENDENCY_GRAPH_SVG = 'dependency-graph'; /** * Graph component used to visualize relations between entities @@ -222,11 +249,15 @@ export function DependencyGraph( curve = 'curveMonotoneX', showArrowHeads = false, fit = 'grow', + allowFullscreen = true, ...svgProps } = props; const theme = useTheme(); const [containerWidth, setContainerWidth] = useState(100); const [containerHeight, setContainerHeight] = useState(100); + const fullScreenHandle = useFullScreenHandle(); + const styles = useStyles(); + const { t } = useTranslationRef(coreComponentsTranslationRef); const graph = useRef>>( new dagre.graphlib.Graph(), @@ -248,11 +279,17 @@ export function DependencyGraph( const containerRef = useMemo( () => - debounce((node: SVGSVGElement) => { - if (!node) { + debounce((root: HTMLDivElement) => { + if (!root) { return; } // Set up zooming + panning + const node: SVGSVGElement = root.querySelector( + `svg#${DEPENDENCY_GRAPH_SVG}`, + ) as SVGSVGElement; + if (!node) { + return; + } const container = d3Selection.select(node); const workspace = d3Selection.select(node.getElementById(WORKSPACE_ID)); @@ -288,7 +325,7 @@ export function DependencyGraph( } const { width: newContainerWidth, height: newContainerHeight } = - node.getBoundingClientRect(); + root.getBoundingClientRect(); if (containerWidth !== newContainerWidth) { setContainerWidth(newContainerWidth); } @@ -412,70 +449,97 @@ export function DependencyGraph( } return ( - - - - - - {defs} - - - - {graphEdges.map(e => { - const edge = graph.current.edge(e) as GraphEdge; - if (!edge) return null; - if (renderEdge) return renderEdge({ edge, id: e }); +
+ + {allowFullscreen && ( + + + {fullScreenHandle.active ? ( + + ) : ( + + )} + + + )} - return ( - + + + - ); - })} - {graphNodes.map((id: string) => { - const node = graph.current.node(id); - if (!node) return null; - return ( - - ); - })} + + {defs} + + + + {graphEdges.map(e => { + const edge = graph.current.edge(e) as GraphEdge; + if (!edge) return null; + if (renderEdge) return renderEdge({ edge, id: e }); + + return ( + + ); + })} + {graphNodes.map((id: string) => { + const node = graph.current.node(id); + + if (!node) return null; + return ( + + ); + })} + + - - + +
); } diff --git a/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx b/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx index 1fcf664bab..0b8fe8cd8c 100644 --- a/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx +++ b/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.tsx @@ -27,6 +27,7 @@ const useStyles = makeStyles( gridAutoFlow: 'column', gridAutoColumns: 'min-content', gridGap: theme.spacing(3), + wordBreak: 'keep-all', }, }), { name: 'BackstageHeaderIconLinkRow' }, diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index b884f0af92..aa644fa909 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -120,6 +120,9 @@ export const coreComponentsTranslationRef = createTranslationRef({ buttonText: "Yes! Don't log me out", }, }, + dependencyGraph: { + fullscreenTooltip: 'Toggle fullscreen', + }, proxiedSignInPage: { title: 'You do not appear to be signed in. Please try reloading the browser page.', diff --git a/packages/core-plugin-api/report.api.md b/packages/core-plugin-api/report.api.md index 54bbfb94cf..b40088dc78 100644 --- a/packages/core-plugin-api/report.api.md +++ b/packages/core-plugin-api/report.api.md @@ -604,6 +604,11 @@ export type OpenIdConnectApi = { getIdToken(options?: AuthRequestOptions): Promise; }; +// @public +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + // @public @deprecated export type OptionalParams< Params extends { diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 9339a3e18a..f8686cb128 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -474,3 +474,20 @@ export const vmwareCloudAuthApiRef: ApiRef< > = createApiRef({ id: 'core.auth.vmware-cloud', }); + +/** + * Provides authentication towards OpenShift APIs and identities. + * + * @public + * @remarks + * + * See {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients} + * on how to configure the OAuth clients and + * {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html-single/authentication_and_authorization/index#tokens-scoping-about_configuring-internal-oauth} + * for available scopes. + */ +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.openshift', +}); diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 261e595b30..0e622c2490 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/create-app +## 0.7.4-next.2 + +### Patch Changes + +- Bumped create-app version. + ## 0.7.4-next.1 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 7f2f9e4cd5..59f8498c54 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.4-next.1", + "version": "0.7.4-next.2", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 503ae406ae..45aaf0a814 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/dev-utils +## 1.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 1.1.14-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 55b4c3fdfe..fd5881af44 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.14-next.1", + "version": "1.1.14-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 351bc8f1b5..7b5e345e12 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -372,6 +372,7 @@ describe('createApp', () => { + diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index e6259c70d7..f9e2336408 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -69,6 +69,7 @@ import { OAuthScope } from '@backstage/core-plugin-api'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -1518,6 +1519,8 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; +export { openshiftAuthApiRef }; + // @public export interface OverridableFrontendPlugin< TRoutes extends { diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index 89509082f0..5a31c1603d 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -37,4 +37,5 @@ export { microsoftAuthApiRef, oneloginAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts index ec07d1b465..0468e96e14 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts @@ -231,6 +231,42 @@ describe('SingleInstanceGithubCredentialsProvider tests', () => { expect(token).toEqual(undefined); }); + it('should not fail to issue tokens for an organization when there is a case mismatch in the organization name', async () => { + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'selected', + account: { + login: 'backstage', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'secret_token', + repository_selection: 'selected', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + octokit.apps.listReposAccessibleToInstallation.mockReturnValue({ + data: [{ name: 'some-repo' }], + } as unknown as RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']); + + const { token, headers } = await github.getCredentials({ + url: 'https://github.com/Backstage', + }); + const expectedToken = 'secret_token'; + expect(headers).toEqual({ Authorization: `Bearer ${expectedToken}` }); + expect(token).toEqual('secret_token'); + }); + it('should not fail to issue tokens for an organization when the app is installed for a single repo', async () => { octokit.apps.listInstallations.mockResolvedValue({ headers: { diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts index fd3c02486f..eaaabe8b0f 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts @@ -102,7 +102,9 @@ class GithubAppManager { private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations constructor(config: GithubAppConfig, baseUrl?: string) { - this.allowedInstallationOwners = config.allowedInstallationOwners; + this.allowedInstallationOwners = config.allowedInstallationOwners?.map( + owner => owner.toLocaleLowerCase('en-US'), + ); this.baseUrl = baseUrl; this.baseAuthConfig = { appId: config.appId, @@ -121,7 +123,11 @@ class GithubAppManager { repo?: string, ): Promise<{ accessToken: string | undefined }> { if (this.allowedInstallationOwners) { - if (!this.allowedInstallationOwners?.includes(owner)) { + if ( + !this.allowedInstallationOwners?.includes( + owner.toLocaleLowerCase('en-US'), + ) + ) { return { accessToken: undefined }; // An empty token allows anonymous access to public repos } } diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 569444b4b8..a5a9bb0505 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/repo-tools +## 0.15.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + ## 0.15.2-next.0 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 9b7c875279..d93785f25e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/repo-tools", - "version": "0.15.2-next.0", + "version": "0.15.2-next.1", "description": "CLI for Backstage repo tooling ", "backstage": { "role": "cli" diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index a88c4593e1..d251516dcd 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/ui +## 0.7.1-next.0 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. + ## 0.7.0 ### Minor Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 2ac399fcee..36b51f0bc8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.7.0", + "version": "0.7.1-next.0", "backstage": { "role": "web-library" }, diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index c534c50c76..4bd43fce40 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@backstage/cli-common": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/release-manifests": "workspace:^", "@yarnpkg/core": "^4.4.1", "@yarnpkg/fslib": "^3.1.2", diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts index 4b1f008fce..a0ce45c18f 100644 --- a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts @@ -73,17 +73,15 @@ describe('getCurrentBackstageVersion', () => { }); it.each` - description | content - ${'is missing'} | ${{}} - ${'is invalid'} | ${{ 'backstage.json': '}{' }} - ${'has missing version'} | ${{ 'backstage.json': '{"a":"b"}' }} - ${'has invalid version'} | ${{ 'backstage.json': '{"version":"foobar"}' }} - `('throws if backstage.json $description', ({ content }) => { + description | content | message + ${'is missing'} | ${{}} | ${/valid version string not found.*no such file/i} + ${'is invalid'} | ${{ 'backstage.json': '}{' }} | ${/valid version string not found.*not valid json/i} + ${'has missing version'} | ${{ 'backstage.json': '{"a":"b"}' }} | ${/valid version string not found.*version field is missing/i} + ${'has invalid version'} | ${{ 'backstage.json': '{"version":"foobar"}' }} | ${/valid version string not found.*exists but is not valid semver/i} + `('throws if backstage.json $description', ({ content, message }) => { mockDir.addContent(content); - expect(() => getCurrentBackstageVersion()).toThrow( - /valid version string not found/i, - ); + expect(() => getCurrentBackstageVersion()).toThrow(message); }); it('caches repeated calls', () => { diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts index 5280854fb3..be80d29376 100644 --- a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts @@ -18,6 +18,7 @@ import assert from 'assert'; import { valid as semverValid } from 'semver'; import { ppath, xfs } from '@yarnpkg/fslib'; import { BACKSTAGE_JSON } from '@backstage/cli-common'; +import { ForwardedError } from '@backstage/errors'; import { memoize } from './memoize'; import { getWorkspaceRoot } from './getWorkspaceRoot'; @@ -26,11 +27,16 @@ export const getCurrentBackstageVersion = memoize(() => { let backstageVersion: string | null = null; try { - backstageVersion = semverValid(xfs.readJsonSync(backstageJsonPath).version); + const backstageVersionRaw = xfs.readJsonSync(backstageJsonPath).version; + assert(backstageVersionRaw !== undefined, 'Version field is missing'); + backstageVersion = semverValid(backstageVersionRaw); - assert(backstageVersion !== null); - } catch { - throw new Error('Valid version string not found in backstage.json'); + assert(backstageVersion !== null, 'Version exists but is not valid semver'); + } catch (err) { + throw new ForwardedError( + 'Valid version string not found in backstage.json', + err, + ); } return backstageVersion; diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 25671d8050..c57a56754c 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-api-docs +## 0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.12.11-next.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 4fcae79fba..17cf78a69a 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.12.11-next.1", + "version": "0.12.11-next.2", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index bd097b0005..ffe2bd1b71 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-app-backend +## 0.5.6-next.1 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index b81e709926..4640a6459b 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.5.6-next.0", + "version": "0.5.6-next.1", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-backend/src/index.ts b/plugins/app-backend/src/index.ts index 9f83f6e30a..3f8a0002a7 100644 --- a/plugins/app-backend/src/index.ts +++ b/plugins/app-backend/src/index.ts @@ -21,4 +21,3 @@ */ export { appPlugin as default } from './service/appPlugin'; -export * from './service/router'; diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index 2063c2db48..14b3a215ee 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-app-node +## 0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + ## 0.1.37-next.0 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 07340379e9..f28b28f461 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.37-next.0", + "version": "0.1.37-next.1", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 24885bed4b..d12ada15b4 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -539,6 +539,21 @@ const appPlugin: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; + 'api:app/openshift-auth': ExtensionDefinition<{ + kind: 'api'; + name: 'openshift-auth'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; 'api:app/permission': ExtensionDefinition<{ kind: 'api'; name: 'permission'; diff --git a/plugins/app/src/defaultApis.ts b/plugins/app/src/defaultApis.ts index 0337f6c4f4..0b4eb5c825 100644 --- a/plugins/app/src/defaultApis.ts +++ b/plugins/app/src/defaultApis.ts @@ -35,6 +35,7 @@ import { createFetchApi, FetchMiddlewares, VMwareCloudAuth, + OpenShiftAuth, } from '../../../packages/core-app-api/src/apis/implementations'; import { @@ -56,6 +57,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { ApiBlueprint, dialogApiRef } from '@backstage/frontend-plugin-api'; import { @@ -353,6 +355,26 @@ export const apis = [ }, }), }), + ApiBlueprint.make({ + name: 'openshift-auth', + params: defineParams => + defineParams({ + api: openshiftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return OpenShiftAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), + }), ApiBlueprint.make({ name: 'permission', params: defineParams => diff --git a/plugins/auth-backend-module-openshift-provider/.eslintrc.js b/plugins/auth-backend-module-openshift-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-openshift-provider/README.md b/plugins/auth-backend-module-openshift-provider/README.md new file mode 100644 index 0000000000..ae4700d561 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-auth-backend-module-openshift-provider + +The openshift-provider backend module for the auth plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/auth-backend-module-openshift-provider/catalog-info.yaml b/plugins/auth-backend-module-openshift-provider/catalog-info.yaml new file mode 100644 index 0000000000..3db615244a --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-openshift-provider + title: '@backstage/plugin-auth-backend-module-openshift-provider' + description: The OpenShift backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: auth-maintainers diff --git a/plugins/auth-backend-module-openshift-provider/config.d.ts b/plugins/auth-backend-module-openshift-provider/config.d.ts new file mode 100644 index 0000000000..1fdaeb1532 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/config.d.ts @@ -0,0 +1,44 @@ +/* + * 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 { HumanDuration } from '@backstage/types'; + +export interface Config { + auth?: { + providers?: { + /** @visibility frontend */ + openshift?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + authorizationUrl: string; + tokenUrl: string; + callbackUrl?: string; + openshiftApiServerUrl: string; + signIn?: { + resolvers: Array<{ + resolver: 'displayNameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + }>; + }; + sessionDuration?: HumanDuration | string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-openshift-provider/dev/index.ts b/plugins/auth-backend-module-openshift-provider/dev/index.ts new file mode 100644 index 0000000000..99f6828292 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/dev/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; +import authPlugin from '@backstage/plugin-auth-backend'; +import authModuleOpenShiftProvider from '../src'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleOpenShiftProvider); + +backend.start(); diff --git a/plugins/auth-backend-module-openshift-provider/package.json b/plugins/auth-backend-module-openshift-provider/package.json new file mode 100644 index 0000000000..9fad0b542e --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-auth-backend-module-openshift-provider", + "version": "0.0.0", + "description": "The OpenShift backend module for the auth plugin.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "auth", + "pluginPackage": "@backstage/plugin-auth-backend" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-openshift-provider" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "config.d.ts" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/types": "workspace:^", + "passport-oauth2": "^1.8.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "express": "^4.18.2", + "msw": "^2.7.3", + "supertest": "^7.1.0" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-openshift-provider/report.api.md b/plugins/auth-backend-module-openshift-provider/report.api.md new file mode 100644 index 0000000000..aa429e1d47 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/report.api.md @@ -0,0 +1,28 @@ +## API Report File for "@backstage/plugin-auth-backend-module-openshift-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleOpenshiftProvider: BackendFeature; +export default authModuleOpenshiftProvider; + +// @public (undocumented) +export const openshiftAuthenticator: OAuthAuthenticator< + OpenShiftAuthenticatorContext, + PassportProfile +>; + +// @public (undocumented) +export interface OpenShiftAuthenticatorContext { + // (undocumented) + helper: PassportOAuthAuthenticatorHelper; + // (undocumented) + openshiftApiServerUrl: string; +} +``` diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..28a7738d21 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts @@ -0,0 +1,226 @@ +/* + * 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 { setupServer } from 'msw/node'; +import { + decodeOAuthState, + encodeOAuthState, +} from '@backstage/plugin-auth-node'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { http, HttpResponse } from 'msw'; +import { openshiftAuthenticator } from './authenticator'; +import { ConfigReader } from '@backstage/config'; +import { + OAuthState, + OAuthAuthenticatorStartInput, + OAuthAuthenticatorAuthenticateInput, +} from '@backstage/plugin-auth-node'; +import express from 'express'; + +describe('openshiftAuthenticator', () => { + let implementation: any; + let oauthState: OAuthState; + + const mswServer = setupServer(); + registerMswTestHooks(mswServer); + + beforeEach(() => { + mswServer.use( + http.post('https://openshift.test/oauth/token', () => { + return HttpResponse.json({ + access_token: 'accessToken', + scope: 'user:full', + expires_in: 60 * 60 * 24, + }); + }), + http.get( + 'https://api.openshift.test/apis/user.openshift.io/v1/users/~', + async () => { + return HttpResponse.json({ + kind: 'User', + apiVersion: 'user.openshift.io/v1', + metadata: { + name: 'alice', + uid: 'ca993628-8817-4a3b-9811-be4a34c60bf4', + resourceVersion: '1', + creationTimestamp: '2022-01-11T13:10:45Z', + managedFields: [], + }, + fullName: 'Alice Adams', + identities: ['SSO:id'], + groups: ['system:authenticated', 'system:authenticated:oauth'], + }); + }, + ), + ); + + implementation = openshiftAuthenticator.initialize({ + callbackUrl: 'https://backstage.test/callback', + config: new ConfigReader({ + clientId: 'clientId', + clientSecret: 'clientSecret', + authorizationUrl: 'https://openshift.test/oauth/authorize', + tokenUrl: 'https://openshift.test/oauth/token', + openshiftApiServerUrl: 'https://api.openshift.test', + }), + }); + + oauthState = { + nonce: 'nonce', + env: 'env', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('#start', () => { + let fakeSession: Record; + let startRequest: OAuthAuthenticatorStartInput; + + beforeEach(() => { + fakeSession = {}; + startRequest = { + state: encodeOAuthState(oauthState), + req: { + method: 'GET', + url: 'test', + session: fakeSession, + }, + } as unknown as OAuthAuthenticatorStartInput; + }); + + it('initiates authorization code grant', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('response_type')).toBe('code'); + }); + + it('passes client ID from config', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('client_id')).toBe('clientId'); + }); + + it('passes callback URL from config', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('redirect_uri')).toBe( + 'https://backstage.test/callback', + ); + }); + + it('encodes OAuth state in query param', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + const stateParam = searchParams.get('state'); + const decodedState = decodeOAuthState(stateParam!); + + expect(decodedState).toMatchObject(oauthState); + }); + }); + + describe('#authenticate', () => { + let handlerRequest: OAuthAuthenticatorAuthenticateInput; + + beforeEach(() => { + handlerRequest = { + req: { + method: 'GET', + query: { + code: 'authorization_code', + state: encodeOAuthState(oauthState), + }, + session: { + 'oauth2:openshift': { + state: encodeOAuthState(oauthState), + }, + }, + } as unknown as express.Request, + }; + }); + + it('exchanges authorization code for access token', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const accessToken = authenticatorResult.session.accessToken; + + expect(accessToken).toEqual('accessToken'); + }); + + it('returns granted scope', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const responseScope = authenticatorResult.session.scope; + + expect(responseScope).toEqual('user:full'); + }); + + it('returns a default session.tokentype field', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const tokenType = authenticatorResult.session.tokenType; + + expect(tokenType).toEqual('bearer'); + }); + + it('returns displayName', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + + expect(authenticatorResult).toMatchObject({ + fullProfile: { + displayName: 'alice', + }, + }); + }); + + it('should store access token as refresh token', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + + expect(authenticatorResult.session.refreshToken).toBe( + authenticatorResult.session.accessToken, + ); + }); + }); +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts new file mode 100644 index 0000000000..0c9acb6287 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts @@ -0,0 +1,184 @@ +/* + * 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 { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { createHash } from 'node:crypto'; +import OAuth2Strategy from 'passport-oauth2'; +import { z } from 'zod'; + +/** @public */ +export interface OpenShiftAuthenticatorContext { + openshiftApiServerUrl: string; + helper: PassportOAuthAuthenticatorHelper; +} + +/** @private + * Schema for user.openshift.io/v1, + * see https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/user_and_group_apis/user-user-openshift-io-v1#user-user-openshift-io-v1 + */ +const OpenShiftUser = z.object({ + metadata: z.object({ + name: z.string(), + }), +}); + +/** @public */ +export const openshiftAuthenticator = createOAuthAuthenticator< + OpenShiftAuthenticatorContext, + PassportProfile +>({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['user:full'], + }, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const authorizationUrl = config.getString('authorizationUrl'); + const tokenUrl = config.getString('tokenUrl'); + const openshiftApiServerUrl = config.getString('openshiftApiServerUrl'); + + // userUrl: `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`, + const strategy = new OAuth2Strategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + authorizationURL: authorizationUrl, + tokenURL: tokenUrl, + passReqToCallback: false, + }, + ( + accessToken: any, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }, { refreshToken }); + }, + ); + + strategy.userProfile = function userProfile( + accessToken: string, + done: (err?: unknown, profile?: any) => void, + ): void { + this._oauth2.useAuthorizationHeaderforGET(true); + + this._oauth2.get( + `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`, + accessToken, + (error, data, _) => { + if (error !== null && error.statusCode !== 200) { + done(new Error(`HTTP error! Status: ${error.statusCode}`)); + return; + } + + if (!data) { + done(new Error('No data provided!')); + return; + } + + if (typeof data !== 'string') { + done(new Error('Data of type Buffer is not supported!')); + return; + } + + const user = OpenShiftUser.parse(JSON.parse(data)); + done(null, { displayName: user.metadata.name }); + }, + ); + }; + + return { + openshiftApiServerUrl, + helper: PassportOAuthAuthenticatorHelper.from(strategy), + }; + }, + async start(input, { helper }) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + async authenticate(input, { helper }) { + // Same workaround as the GitHub provider; see https://github.com/backstage/backstage/issues/25383 + const { fullProfile, session } = await helper.authenticate(input); + session.refreshToken = session.accessToken; + session.refreshTokenExpiresInSeconds = session.expiresInSeconds; + return { fullProfile, session }; + }, + async refresh(input, { helper }) { + // Because the session is refreshed on login, this override is crucial, + // see https://github.com/backstage/backstage/issues/25383 + const accessToken = input.refreshToken; + + const fullProfile = await helper.fetchProfile(accessToken).catch(error => { + if (error.oauthError?.statusCode === 401) { + throw new Error('Invalid access token'); + } + throw error; + }); + + return { + fullProfile, + session: { + accessToken, + tokenType: 'bearer', + scope: input.scope, + refreshToken: input.refreshToken, + }, + }; + }, + async logout(input, { openshiftApiServerUrl, helper }) { + // Due to the implementation of createOAuthRouteHandlers, only the refresh token is set. + // In this provider, the refresh token actually IS the access token. + const accessToken = input.refreshToken; + if (!accessToken) { + throw new Error('access token/refresh token needs to be set for logout'); + } + + // Check if access token is still valid. + try { + await helper.fetchProfile(accessToken); + } catch { + // Invalid token, no need to delete OAuthAccessToken. + return; + } + + // Calculate token name, see: + // https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/oauth_apis/oauthaccesstoken-oauth-openshift-io-v1#apis-oauth-openshift-io-v1-oauthaccesstokens + const tokenName = createHash('sha256') + .update(accessToken.slice('sha256~'.length)) + .digest() + .toString('base64url'); + + const response = await fetch( + `${openshiftApiServerUrl}/apis/oauth.openshift.io/v1/oauthaccesstokens/sha256~${tokenName}`, + { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` } }, + ); + + if (response.status === 401) { + throw new Error('unauthorized'); + } + }, +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/index.ts b/plugins/auth-backend-module-openshift-provider/src/index.ts new file mode 100644 index 0000000000..4c8dd6cb22 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ +/** + * The openshift-provider backend module for the auth plugin. + * + * @packageDocumentation + */ +export { + openshiftAuthenticator, + type OpenShiftAuthenticatorContext, +} from './authenticator'; +export { authModuleOpenshiftProvider as default } from './module'; diff --git a/plugins/auth-backend-module-openshift-provider/src/module.ts b/plugins/auth-backend-module-openshift-provider/src/module.ts new file mode 100644 index 0000000000..18d96c4778 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/module.ts @@ -0,0 +1,45 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { openshiftAuthenticator } from './authenticator'; +import { openshiftSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleOpenshiftProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'openshift-provider', + register(reg) { + reg.registerInit({ + deps: { providers: authProvidersExtensionPoint }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'openshift', + factory: createOAuthProviderFactory({ + authenticator: openshiftAuthenticator, + signInResolverFactories: { + ...openshiftSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/resolvers.ts b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts new file mode 100644 index 0000000000..dee55ec4ca --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts @@ -0,0 +1,68 @@ +/* + * 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 { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { z } from 'zod'; + +export namespace openshiftSignInResolvers { + export const displayNameMatchingUserEntityName = createSignInResolverFactory({ + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { displayName } = info.profile; + + if (!displayName) { + throw new Error( + `OpenShift user profile does not contain a displayName`, + ); + } + + const userRef = stringifyEntityRef({ + kind: 'User', + name: displayName, + namespace: DEFAULT_NAMESPACE, + }); + + return await ctx.signInWithCatalogUser( + { entityRef: userRef }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { entityRef: { name: displayName } } + : undefined, + }, + ); + }; + }, + }); +} diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 54dee8262d..7aa33c315a 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend +## 0.25.4-next.1 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.25.4-next.0 ### Patch Changes diff --git a/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js new file mode 100644 index 0000000000..87391c3467 --- /dev/null +++ b/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js @@ -0,0 +1,159 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // These tables make up the OIDC client registration flow. + // Clients are the top of the tree, that are created by the client registration flow. + await knex.schema.createTable('oidc_clients', table => { + table.comment( + 'OIDC clients that are registered via dynamic client registration', + ); + + table + .string('client_id') + .primary() + .notNullable() + .comment('The unique client ID of the client'); + + table + .string('client_secret') + .notNullable() + .comment('The client secret of the client'); + + table + .string('client_name') + .notNullable() + .comment('The name of the client, should be human readable'); + + table + .text('response_types', 'longtext') + .notNullable() + .comment('JSON array of supported response types'); + + table + .text('grant_types', 'longtext') + .notNullable() + .comment('JSON array of supported grant types'); + + table + .text('redirect_uris', 'longtext') + .notNullable() + .comment('Allowed redirect URIs as JSON array'); + + table.text('scope').nullable().comment('Default scopes for the client'); + + table + .text('metadata', 'longtext') + .nullable() + .comment('Additional client metadata as JSON'); + }); + + await knex.schema.createTable('oauth_authorization_sessions', table => { + table.comment('Core OAuth authorization sessions with shared context'); + + table + .string('id') + .primary() + .notNullable() + .comment('Unique session identifier'); + + table.string('client_id').notNullable().comment('OIDC client identifier'); + + table + .string('user_entity_ref') + .nullable() + .comment('Backstage user entity reference'); + + table + .text('redirect_uri', 'longtext') + .notNullable() + .comment('Client redirect URI'); + + table.text('scope').nullable().comment('Requested scopes space-separated'); + + table.string('state').nullable().comment('Client state parameter'); + + table.string('response_type').notNullable().comment('OAuth2 response type'); + + table.string('code_challenge').nullable().comment('PKCE code challenge'); + + table + .string('code_challenge_method') + .nullable() + .comment('PKCE code challenge method'); + + table.string('nonce').nullable().comment('OIDC nonce parameter'); + + table + .enum('status', ['pending', 'approved', 'rejected', 'expired']) + .defaultTo('pending') + .comment('Authorization session status'); + + table + .timestamp('expires_at', { useTz: true, precision: 0 }) + .notNullable() + .comment('Session expiration timestamp'); + + table.foreign('client_id').references('client_id').inTable('oidc_clients'); + table.index(['client_id', 'user_entity_ref']); + table.index(['status', 'expires_at']); + }); + + await knex.schema.createTable('oidc_authorization_codes', table => { + table.comment('OAuth authorization codes for code exchange flow'); + + table + .string('code') + .primary() + .notNullable() + .comment('Unique authorization code'); + + table + .string('session_id') + .notNullable() + .comment('Authorization session identifier'); + + table + .timestamp('expires_at', { useTz: true, precision: 0 }) + .notNullable() + .comment('Authorization code expiration timestamp'); + + table + .boolean('used') + .defaultTo(false) + .comment('Whether the authorization code has been used'); + + table + .foreign('session_id') + .references('id') + .inTable('oauth_authorization_sessions') + .onDelete('CASCADE'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('oidc_authorization_codes'); + await knex.schema.dropTable('oauth_authorization_sessions'); + await knex.schema.dropTable('oidc_clients'); +}; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 0dddd935e6..e5f048ccae 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,11 +1,12 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.25.4-next.0", + "version": "0.25.4-next.1", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" @@ -60,6 +61,7 @@ "knex": "^3.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", + "matcher": "^4.0.0", "minimatch": "^9.0.0", "passport": "^0.7.0", "uuid": "^11.0.0" diff --git a/plugins/auth-backend/report.sql.md b/plugins/auth-backend/report.sql.md index b135414af6..7622a5750e 100644 --- a/plugins/auth-backend/report.sql.md +++ b/plugins/auth-backend/report.sql.md @@ -5,6 +5,59 @@ > [!WARNING] > Failed to migrate down from '20220321100910_timestamptz_again.js' +## Table `oauth_authorization_sessions` + +| Column | Type | Nullable | Max Length | Default | +| ----------------------- | -------------------------- | -------- | ---------- | ----------------- | +| `client_id` | `character varying` | false | 255 | - | +| `code_challenge` | `character varying` | true | 255 | - | +| `code_challenge_method` | `character varying` | true | 255 | - | +| `expires_at` | `timestamp with time zone` | false | - | - | +| `id` | `character varying` | false | 255 | - | +| `nonce` | `character varying` | true | 255 | - | +| `redirect_uri` | `text` | false | - | - | +| `response_type` | `character varying` | false | 255 | - | +| `scope` | `text` | true | - | - | +| `state` | `character varying` | true | 255 | - | +| `status` | `text` | true | - | `'pending'::text` | +| `user_entity_ref` | `character varying` | true | 255 | - | + +### Indices + +- `oauth_authorization_sessions_client_id_user_entity_ref_index` (`client_id`, `user_entity_ref`) +- `oauth_authorization_sessions_pkey` (`id`) unique primary +- `oauth_authorization_sessions_status_expires_at_index` (`status`, `expires_at`) + +## Table `oidc_authorization_codes` + +| Column | Type | Nullable | Max Length | Default | +| ------------ | -------------------------- | -------- | ---------- | ------- | +| `code` | `character varying` | false | 255 | - | +| `expires_at` | `timestamp with time zone` | false | - | - | +| `session_id` | `character varying` | false | 255 | - | +| `used` | `boolean` | true | - | `false` | + +### Indices + +- `oidc_authorization_codes_pkey` (`code`) unique primary + +## Table `oidc_clients` + +| Column | Type | Nullable | Max Length | Default | +| ---------------- | ------------------- | -------- | ---------- | ------- | +| `client_id` | `character varying` | false | 255 | - | +| `client_name` | `character varying` | false | 255 | - | +| `client_secret` | `character varying` | false | 255 | - | +| `grant_types` | `text` | false | - | - | +| `metadata` | `text` | true | - | - | +| `redirect_uris` | `text` | false | - | - | +| `response_types` | `text` | false | - | - | +| `scope` | `text` | true | - | - | + +### Indices + +- `oidc_clients_pkey` (`client_id`) unique primary + ## Table `sessions` | Column | Type | Nullable | Max Length | Default | diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts index f3877d48cd..025d72fcb7 100644 --- a/plugins/auth-backend/src/authPlugin.ts +++ b/plugins/auth-backend/src/authPlugin.ts @@ -66,6 +66,7 @@ export const authPlugin = createBackendPlugin({ database: coreServices.database, discovery: coreServices.discovery, auth: coreServices.auth, + httpAuth: coreServices.httpAuth, catalog: catalogServiceRef, }, async init({ @@ -75,6 +76,7 @@ export const authPlugin = createBackendPlugin({ database, discovery, auth, + httpAuth, catalog, }) { const router = await createRouter({ @@ -86,6 +88,7 @@ export const authPlugin = createBackendPlugin({ catalog, providerFactories: Object.fromEntries(providers), ownershipResolver, + httpAuth, }); httpRouter.addAuthPolicy({ path: '/', diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts new file mode 100644 index 0000000000..19285aec0a --- /dev/null +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -0,0 +1,293 @@ +/* + * 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 { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { AuthDatabase } from './AuthDatabase'; +import { OidcDatabase } from './OidcDatabase'; +import { resolvePackagePath } from '@backstage/backend-plugin-api'; + +jest.setTimeout(60_000); + +describe('Oidc Database', () => { + const databases = TestDatabases.create(); + + async function createOidcDatabase(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + return { + oidc: await OidcDatabase.create({ + database: AuthDatabase.create({ + getClient: async () => knex, + }), + }), + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('Clients', () => { + it('should create and return a client', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }), + ).resolves.toEqual({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: undefined, + expiresAt: undefined, + metadata: undefined, + }); + }); + + it('should return the client thats created in a list', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + await expect( + oidc.getClient({ clientId: 'test-client' }), + ).resolves.toEqual(client); + }); + + it('should return null if the client does not exist', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.getClient({ clientId: 'test-client' }), + ).resolves.toBeNull(); + }); + }); + + describe('Authorization Sessions', () => { + it('should create and return an authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + expect(session).toEqual( + expect.objectContaining({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: new Date('2025-01-01T00:00:00Z'), + status: 'pending', + }), + ); + }); + + it('should allow updating the authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + await expect( + oidc.updateAuthorizationSession({ + id: 'test-session', + userEntityRef: 'user:default/blam', + status: 'approved', + }), + ).resolves.toEqual({ + ...session, + userEntityRef: 'user:default/blam', + status: 'approved', + }); + }); + }); + + describe('Authorization Codes', () => { + it('should create and return an authorization code', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + expect(authCode).toEqual( + expect.objectContaining({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }), + ); + }); + + it('should return authorization code with session data', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCodeFromDb = await oidc.getAuthorizationCode({ + code: 'test-code', + }); + const sessionFromDb = await oidc.getAuthorizationSession({ + id: authCodeFromDb!.sessionId, + }); + + expect(authCodeFromDb).toEqual(authCode); + expect(sessionFromDb).toEqual(session); + }); + + it('should allow updating the authorization code', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const updatedAuthCode = await oidc.updateAuthorizationCode({ + code: 'test-code', + used: true, + }); + + expect(updatedAuthCode).toEqual({ + ...authCode, + used: true, + }); + }); + }); + }); +}); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts new file mode 100644 index 0000000000..ebb6619400 --- /dev/null +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -0,0 +1,397 @@ +/* + * 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 { Knex } from 'knex'; +import { AuthDatabase } from './AuthDatabase'; + +function toDate(value?: Date | string | number): Date | undefined { + if (!value) { + return undefined; + } + + return typeof value === 'string' || typeof value === 'number' + ? new Date(value) + : value; +} +type OidcClientRow = { + client_id: string; + client_secret: string; + client_name: string; + response_types: string; + grant_types: string; + redirect_uris: string; + scope: string | null; + metadata: string | null; +}; + +type OAuthAuthorizationSessionRow = { + id: string; + client_id: string; + user_entity_ref: string | null; + redirect_uri: string; + scope: string | null; + state: string | null; + response_type: string; + code_challenge: string | null; + code_challenge_method: string | null; + nonce: string | null; + status: 'pending' | 'approved' | 'rejected' | 'expired'; + expires_at: Date | string; +}; + +type OidcAuthorizationCodeRow = { + code: string; + session_id: string; + expires_at: Date | string; + used: boolean; +}; + +export type Client = { + clientId: string; + clientName: string; + clientSecret: string; + redirectUris: string[]; + responseTypes: string[]; + grantTypes: string[]; + scope?: string; + metadata?: Record; +}; + +export type AuthorizationSession = { + id: string; + clientId: string; + userEntityRef?: string; + redirectUri: string; + scope?: string; + state?: string; + responseType: string; + codeChallenge?: string; + codeChallengeMethod?: string; + nonce?: string; + status: 'pending' | 'approved' | 'rejected' | 'expired'; + expiresAt: Date; +}; + +export type ConsentRequest = { + id: string; + sessionId: string; + expiresAt: Date; +}; + +export type AuthorizationCode = { + code: string; + sessionId: string; + expiresAt: Date; + used: boolean; +}; + +export type AccessToken = { + tokenId: string; + sessionId: string; + expiresAt: Date; +}; + +/** + * This class provides database operations for OpenID Connect (OIDC) authentication flows. + * It manages OIDC clients, authorization codes, and access tokens in the database. + */ +export class OidcDatabase { + private constructor(private readonly db: Knex) {} + + static async create(options: { database: AuthDatabase }) { + const client = await options.database.get(); + return new OidcDatabase(client); + } + + async createClient(client: Client) { + await this.db('oidc_clients').insert({ + client_id: client.clientId, + client_secret: client.clientSecret, + client_name: client.clientName, + response_types: JSON.stringify(client.responseTypes), + grant_types: JSON.stringify(client.grantTypes), + redirect_uris: JSON.stringify(client.redirectUris), + scope: client.scope, + metadata: JSON.stringify(client.metadata), + }); + + return client; + } + + async getClient({ clientId }: { clientId: string }) { + const client = await this.db('oidc_clients') + .where('client_id', clientId) + .first(); + + if (!client) { + return null; + } + + return this.rowToClient(client) as Client; + } + + async createAuthorizationSession( + session: Omit, + ) { + await this.db( + 'oauth_authorization_sessions', + ).insert({ + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: 'pending', + expires_at: session.expiresAt, + }); + + return { + ...session, + status: 'pending', + }; + } + + async updateAuthorizationSession( + session: Partial & { id: string }, + ) { + const row = this.authorizationSessionToRow(session); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + + // MySQL and SQLite3 don't support RETURNING + if ( + this.db.client.config.client.includes('sqlite3') || + this.db.client.config.client.includes('mysql') + ) { + return await this.db.transaction(async trx => { + await trx('oauth_authorization_sessions') + .where('id', session.id) + .update(updatedFields); + + const updated = await trx( + 'oauth_authorization_sessions', + ) + .where('id', session.id) + .first(); + + if (!updated) { + throw new Error( + `Failed to retrieve updated authorization session with id ${session.id}`, + ); + } + + return this.rowToAuthorizationSession(updated) as AuthorizationSession; + }); + } + + const returnedRows = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', session.id) + .update(updatedFields) + .returning('*'); + + if (returnedRows.length !== 1) { + throw new Error( + `Failed to retrieve updated authorization session with id ${session.id}`, + ); + } + + const [returnedSession] = returnedRows; + + return this.rowToAuthorizationSession( + returnedSession, + ) as AuthorizationSession; + } + + async getAuthorizationSession({ id }: { id: string }) { + const session = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', id) + .first(); + + if (!session) { + return null; + } + + return this.rowToAuthorizationSession(session) as AuthorizationSession; + } + + async createAuthorizationCode( + authorizationCode: Omit, + ) { + await this.db('oidc_authorization_codes').insert({ + code: authorizationCode.code, + session_id: authorizationCode.sessionId, + expires_at: authorizationCode.expiresAt, + used: false, + }); + + return { + ...authorizationCode, + used: false, + }; + } + + async getAuthorizationCode({ code }: { code: string }) { + const authCode = await this.db( + 'oidc_authorization_codes', + ) + .where('code', code) + .first(); + + if (!authCode) { + return null; + } + + return this.rowToAuthorizationCode(authCode) as AuthorizationCode; + } + + async updateAuthorizationCode( + authorizationCode: Partial & { code: string }, + ) { + const row = this.authorizationCodeToRow(authorizationCode); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + + // MySQL and SQLite3 don't support RETURNING + if ( + this.db.client.config.client.includes('sqlite3') || + this.db.client.config.client.includes('mysql') + ) { + return await this.db.transaction(async trx => { + await trx('oidc_authorization_codes') + .where('code', authorizationCode.code) + .update(updatedFields); + + const updated = await trx( + 'oidc_authorization_codes', + ) + .where('code', authorizationCode.code) + .first(); + + if (!updated) { + throw new Error( + `Failed to retrieve updated authorization code with code ${authorizationCode.code}`, + ); + } + + return this.rowToAuthorizationCode(updated) as AuthorizationCode; + }); + } + + const returnedRows = await this.db( + 'oidc_authorization_codes', + ) + .where('code', authorizationCode.code) + .update(updatedFields) + .returning('*'); + + if (returnedRows.length !== 1) { + throw new Error( + `Failed to retrieve updated authorization code with code ${authorizationCode.code}`, + ); + } + + const [returnedCode] = returnedRows; + + return this.rowToAuthorizationCode(returnedCode) as AuthorizationCode; + } + + private rowToClient(row: OidcClientRow): Client { + return { + clientId: row.client_id, + clientName: row.client_name, + clientSecret: row.client_secret, + redirectUris: row.redirect_uris + ? JSON.parse(row.redirect_uris) + : undefined, + responseTypes: row.response_types + ? JSON.parse(row.response_types) + : undefined, + grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined, + scope: row.scope ?? undefined, + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, + }; + } + + private authorizationSessionToRow( + session: Partial, + ): Partial { + return { + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: session.status, + expires_at: toDate(session.expiresAt), + }; + } + + private rowToAuthorizationSession( + row: OAuthAuthorizationSessionRow, + ): Partial { + return { + id: row.id, + clientId: row.client_id, + userEntityRef: row.user_entity_ref ?? undefined, + redirectUri: row.redirect_uri, + scope: row.scope ?? undefined, + state: row.state ?? undefined, + responseType: row.response_type, + codeChallenge: row.code_challenge ?? undefined, + codeChallengeMethod: row.code_challenge_method ?? undefined, + nonce: row.nonce ?? undefined, + status: row.status, + expiresAt: toDate(row.expires_at), + }; + } + + private authorizationCodeToRow( + authorizationCode: Partial, + ): Partial { + return { + code: authorizationCode.code, + session_id: authorizationCode.sessionId, + expires_at: toDate(authorizationCode.expiresAt), + used: authorizationCode.used, + }; + } + + private rowToAuthorizationCode( + row: OidcAuthorizationCodeRow, + ): Partial { + return { + code: row.code, + sessionId: row.session_id, + expiresAt: toDate(row.expires_at), + used: Boolean(row.used), + }; + } +} diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts index f9575c70fd..df7063351a 100644 --- a/plugins/auth-backend/src/migrations.test.ts +++ b/plugins/auth-backend/src/migrations.test.ts @@ -186,4 +186,122 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20250909120000_oidc_client_registration.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore( + knex, + '20250909120000_oidc_client_registration.js', + ); + await migrateUpOnce(knex); + + await knex + .insert({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + client_name: 'Test Client', + response_types: JSON.stringify(['code']), + grant_types: JSON.stringify(['authorization_code']), + redirect_uris: JSON.stringify(['https://example.com/callback']), + scope: 'openid profile', + metadata: JSON.stringify({ description: 'Test client' }), + }) + .into('oidc_clients'); + + await expect( + knex('oidc_clients').where('client_id', 'test-client-id').first(), + ).resolves.toEqual({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + client_name: 'Test Client', + response_types: JSON.stringify(['code']), + grant_types: JSON.stringify(['authorization_code']), + redirect_uris: JSON.stringify(['https://example.com/callback']), + scope: 'openid profile', + metadata: JSON.stringify({ description: 'Test client' }), + }); + + await knex + .insert({ + id: 'test-session-id', + client_id: 'test-client-id', + user_entity_ref: 'user:default/test-user', + redirect_uri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + response_type: 'code', + code_challenge: 'test-challenge', + code_challenge_method: 'S256', + nonce: 'test-nonce', + status: 'pending', + expires_at: new Date(Date.now() + 3600000), + }) + .into('oauth_authorization_sessions'); + + await expect( + knex('oauth_authorization_sessions') + .where('id', 'test-session-id') + .first(), + ).resolves.toEqual( + expect.objectContaining({ + id: 'test-session-id', + client_id: 'test-client-id', + user_entity_ref: 'user:default/test-user', + redirect_uri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + response_type: 'code', + code_challenge: 'test-challenge', + code_challenge_method: 'S256', + nonce: 'test-nonce', + status: 'pending', + }), + ); + + await knex + .insert({ + code: 'test-auth-code', + session_id: 'test-session-id', + expires_at: new Date(Date.now() + 600000), + used: false, + }) + .into('oidc_authorization_codes'); + + await expect( + knex('oidc_authorization_codes') + .where('code', 'test-auth-code') + .first(), + ).resolves.toEqual( + expect.objectContaining({ + code: 'test-auth-code', + session_id: 'test-session-id', + }), + ); + + await knex('oauth_authorization_sessions') + .where('id', 'test-session-id') + .del(); + + await expect( + knex('oidc_authorization_codes').where('session_id', 'test-session-id'), + ).resolves.toHaveLength(0); + + await migrateDownOnce(knex); + + const tables = [ + 'oidc_clients', + 'oauth_authorization_sessions', + 'oidc_authorization_codes', + ]; + + for (const table of tables) { + await expect(knex.schema.hasTable(table)).resolves.toBe(false); + } + + await knex.destroy(); + }, + ); }); diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index dbc6c88f93..de865a9c43 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -17,135 +17,794 @@ import { coreServices, createBackendPlugin, + resolvePackagePath, } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; -import Router from 'express-promise-router'; +import { + mockServices, + startTestBackend, + TestDatabases, + TestDatabaseId, + mockCredentials, +} from '@backstage/backend-test-utils'; import request from 'supertest'; +import crypto from 'crypto'; import { OidcRouter } from './OidcRouter'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { AuthDatabase } from '../database/AuthDatabase'; +import { OidcService } from '../service/OidcService'; +import { TokenIssuer } from '../identity/types'; + +jest.setTimeout(60_000); describe('OidcRouter', () => { - describe('/v1/userinfo', () => { - it('should return user info for full tokens', async () => { - const auth = mockServices.auth.mock(); - const mockUserInfo = { - getUserInfo: jest.fn().mockResolvedValue({ - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], - }, - }), - } as unknown as UserInfoDatabase; + const MOCK_USER_TOKEN = 'mock-user-token'; + const MOCK_USER_ENTITY_REF = 'user:default/test-user'; + const databases = TestDatabases.create(); - const { server } = await startTestBackend({ - features: [ - createBackendPlugin({ - pluginId: 'auth', - register(reg) { - reg.registerInit({ - deps: { httpRouter: coreServices.httpRouter }, - async init({ httpRouter }) { - const router = Router(); + async function createRouter(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); - router.use( - OidcRouter.create({ - auth, - tokenIssuer: {} as any, - baseUrl: 'http://localhost:7000', - userInfo: mockUserInfo, - }).getRouter(), - ); - httpRouter.use(router); - httpRouter.addAuthPolicy({ - path: '/', - allow: 'unauthenticated', - }); - }, - }); - }, - }), - ], - }); - - auth.authenticate.mockResolvedValueOnce({} as any); - auth.isPrincipal.mockReturnValueOnce(true); - - await request(server) - .get('/api/auth/v1/userinfo') - .set( - 'Authorization', - `Bearer h.${btoa( - JSON.stringify({ sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'] }), - )}.s`, - ) - .expect(200, { - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], - }, - }); - - expect(mockUserInfo.getUserInfo).toHaveBeenCalledWith('k/ns:n'); + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), }); - it('should return user info for limited tokens', async () => { - const auth = mockServices.auth.mock(); - const mockUserInfo = { - getUserInfo: jest.fn().mockResolvedValue({ - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], + const authDatabase = AuthDatabase.create({ + getClient: async () => knex, + }); + + const oidcDatabase = await OidcDatabase.create({ + database: authDatabase, + }); + + const userInfoDatabase = await UserInfoDatabase.create({ + database: authDatabase, + }); + + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as unknown as jest.Mocked; + + const mockAuth = mockServices.auth.mock(); + const mockHttpAuth = mockServices.httpAuth.mock(); + const mockConfig = mockServices.rootConfig({ + data: { + auth: { + experimentalDynamicClientRegistration: { + enabled: true, }, - }), - } as unknown as UserInfoDatabase; + }, + }, + }); - const { server } = await startTestBackend({ - features: [ - createBackendPlugin({ - pluginId: 'auth', - register(reg) { - reg.registerInit({ - deps: { httpRouter: coreServices.httpRouter }, - async init({ httpRouter }) { - const router = Router(); + const oidcService = OidcService.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7000', + userInfo: userInfoDatabase, + oidc: oidcDatabase, + config: mockConfig, + }); - router.use( - OidcRouter.create({ - auth, - tokenIssuer: {} as any, - baseUrl: 'http://localhost:7000', - userInfo: mockUserInfo, - }).getRouter(), - ); - httpRouter.use(router); - httpRouter.addAuthPolicy({ - path: '/', - allow: 'unauthenticated', - }); - }, - }); - }, - }), - ], - }); + const oidcRouter = OidcRouter.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7000', + appUrl: 'http://localhost:3000', + logger: mockServices.logger.mock(), + userInfo: userInfoDatabase, + oidc: oidcDatabase, + httpAuth: mockHttpAuth, + config: mockConfig, + }); - auth.authenticate.mockResolvedValueOnce({} as any); - auth.isPrincipal.mockReturnValueOnce(true); + return { + router: oidcRouter, + mocks: { + httpAuth: mockHttpAuth, + auth: mockAuth, + oidc: oidcDatabase, + userInfo: userInfoDatabase, + service: oidcService, + tokenIssuer: mockTokenIssuer, + }, + }; + } - await request(server) - .get('/api/auth/v1/userinfo') - .set( - 'Authorization', - `Bearer h.${btoa(JSON.stringify({ sub: 'k/ns:n' }))}.s`, - ) - .expect(200, { + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('/v1/userinfo', () => { + it('should return user info for full tokens', async () => { + const { + mocks: { auth, userInfo }, + router, + } = await createRouter(databaseId); + + await userInfo.addUserInfo({ claims: { sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'], + exp: Math.floor(Date.now() / 1000) + 3600, }, }); - expect(mockUserInfo.getUserInfo).toHaveBeenCalledWith('k/ns:n'); + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .get('/api/auth/v1/userinfo') + .set( + 'Authorization', + `Bearer h.${btoa( + JSON.stringify({ sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'] }), + )}.s`, + ) + .expect(200); + + expect(response.body).toEqual({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: expect.any(Number), + }, + }); + }); + + it('should return user info for limited tokens', async () => { + const { + mocks: { auth, userInfo }, + router, + } = await createRouter(databaseId); + + await userInfo.addUserInfo({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: Math.floor(Date.now() / 1000) + 3600, + }, + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .get('/api/auth/v1/userinfo') + .set( + 'Authorization', + `Bearer h.${btoa(JSON.stringify({ sub: 'k/ns:n' }))}.s`, + ) + .expect(200); + + expect(response.body).toEqual({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: expect.any(Number), + }, + }); + }); + }); + + describe('auth flow', () => { + it('should register a client', async () => { + const { router } = await createRouter(databaseId); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .post('/api/auth/v1/register') + .send({ + client_name: 'Test Client', + redirect_uris: ['https://example.com/callback'], + response_types: ['code'], + grant_types: ['authorization_code'], + scope: 'openid', + }) + .expect(201); + + expect(response.body).toEqual({ + client_id: expect.any(String), + client_secret: expect.any(String), + redirect_uris: ['https://example.com/callback'], + }); + }); + + it('should create an authorization session via authorization endpoint', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get('/api/auth/v1/authorize') + .query({ + client_id: client.clientId, + redirect_uri: 'https://example.com/callback', + response_type: 'code', + scope: 'openid', + state: 'test-state', + }) + .expect(302); + + expect(response.header.location).toMatch( + /^http:\/\/localhost:3000\/oauth2\/authorize\/[a-f0-9-]+$/, + ); + }); + + it('should get auth session details', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get(`/api/auth/v1/sessions/${authSession.id}`) + .expect(200); + + expect(response.body).toEqual({ + id: authSession.id, + clientName: 'Test Client', + scope: 'openid', + redirectUri: 'https://example.com/callback', + }); + }); + + it('should approve authorization session', async () => { + const { + mocks: { auth, service, httpAuth }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + expect(response.body).toEqual({ + redirectUrl: expect.stringMatching( + /^https:\/\/example\.com\/callback\?code=[\w-]+&state=test-state$/, + ), + }); + }); + + it('should reject auth session', async () => { + const { + mocks: { service, httpAuth, auth }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/reject`) + .expect(200); + + expect(response.body).toEqual({ + redirectUrl: expect.stringMatching( + /^https:\/\/example\.com\/callback\?error=access_denied&error_description=User\+denied\+the\+request&state=test-state$/, + ), + }); + }); + }); + + describe('token exchange', () => { + it('should exchange authorization code for tokens', async () => { + const { + mocks: { auth, service, tokenIssuer, httpAuth }, + router, + } = await createRouter(databaseId); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token', + }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: 'https://example.com/callback', + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: MOCK_USER_ENTITY_REF, + }, + }); + }); + + it('should exchange authorization code for tokens with PKCE', async () => { + const { + mocks: { auth, service, tokenIssuer, httpAuth }, + router, + } = await createRouter(databaseId); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token-pkce', + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user-pkce'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const codeVerifier = + 'test-code-verifier-123456789012345678901234567890123456789012345'; + const codeChallenge = codeVerifier; + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge, + codeChallengeMethod: 'plain', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: 'https://example.com/callback', + code_verifier: codeVerifier, + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token-pkce', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token-pkce', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/test-user-pkce', + }, + }); + }); + + it('should reject token exchange with invalid authorization code', async () => { + const { router } = await createRouter(databaseId); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: 'invalid-code', + redirect_uri: 'https://example.com/callback', + }) + .expect(401); + + expect(tokenResponse.body).toEqual({ + error: 'invalid_client', + error_description: 'Invalid authorization code', + }); + }); + + it('should exchange authorization code for tokens with PKCE S256', async () => { + const { + mocks: { auth, service, tokenIssuer, httpAuth }, + router, + } = await createRouter(databaseId); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token-s256', + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user-s256'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const codeVerifier = + 'test-code-verifier-s256-123456789012345678901234567890123456789'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: 'https://example.com/callback', + code_verifier: codeVerifier, + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token-s256', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token-s256', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/test-user-s256', + }, + }); + }); }); }); }); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 6ada071c44..9a3308b4eb 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -15,42 +15,72 @@ */ import Router from 'express-promise-router'; import { OidcService } from './OidcService'; -import { AuthenticationError } from '@backstage/errors'; -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthenticationError, isError } from '@backstage/errors'; +import { + AuthService, + HttpAuthService, + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { json } from 'express'; export class OidcRouter { - private constructor(private readonly oidc: OidcService) {} + private constructor( + private readonly oidc: OidcService, + private readonly logger: LoggerService, + private readonly auth: AuthService, + private readonly appUrl: string, + private readonly httpAuth: HttpAuthService, + private readonly config: RootConfigService, + ) {} static create(options: { auth: AuthService; tokenIssuer: TokenIssuer; baseUrl: string; + appUrl: string; + logger: LoggerService; userInfo: UserInfoDatabase; + oidc: OidcDatabase; + httpAuth: HttpAuthService; + config: RootConfigService; }) { - return new OidcRouter(OidcService.create(options)); + return new OidcRouter( + OidcService.create(options), + options.logger, + options.auth, + options.appUrl, + options.httpAuth, + options.config, + ); } public getRouter() { const router = Router(); + router.use(json()); + + // OpenID Provider Configuration endpoint + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig + // Returns the OpenID Provider Configuration document containing metadata about the provider router.get('/.well-known/openid-configuration', (_req, res) => { res.json(this.oidc.getConfiguration()); }); + // JSON Web Key Set endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.10.1.1 + // Returns the public keys used to verify JWTs issued by this provider router.get('/.well-known/jwks.json', async (_req, res) => { const { keys } = await this.oidc.listPublicKeys(); res.json({ keys }); }); - router.get('/v1/token', (_req, res) => { - res.status(501).send('Not Implemented'); - }); - - // This endpoint doesn't use the regular HttpAuthoidc, since the contract - // is specifically for the header to be communicated in the Authorization - // header, regardless of token type + // UserInfo endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + // Returns claims about the authenticated user using an access token router.get('/v1/userinfo', async (req, res) => { const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i); const token = matches?.[1]; @@ -68,6 +98,337 @@ export class OidcRouter { res.json(userInfo); }); + if ( + this.config.getOptionalBoolean( + 'auth.experimentalDynamicClientRegistration.enabled', + ) + ) { + // Authorization endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // Handles the initial authorization request from the client, validates parameters, + // and redirects to the Authorization Session page for user approval + router.get('/v1/authorize', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + scope, + state, + nonce, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + } = req.query; + + if (!clientId || !redirectUri || !responseType) { + this.logger.error(`Failed to authorize: Missing required parameters`); + return res.status(400).json({ + error: 'invalid_request', + error_description: + 'Missing required parameters: client_id, redirect_uri, response_type', + }); + } + + try { + const result = await this.oidc.createAuthorizationSession({ + clientId: clientId as string, + redirectUri: redirectUri as string, + responseType: responseType as string, + scope: scope as string | undefined, + state: state as string | undefined, + nonce: nonce as string | undefined, + codeChallenge: codeChallenge as string | undefined, + codeChallengeMethod: codeChallengeMethod as string | undefined, + }); + + // todo(blam): maybe this URL could be overridable by config if + // the plugin is mounted somewhere else? + // support slashes in baseUrl? + const authSessionRedirectUrl = new URL( + `./oauth2/authorize/${result.id}`, + ensureTrailingSlash(this.appUrl), + ); + + return res.redirect(authSessionRedirectUrl.toString()); + } catch (error) { + const errorParams = new URLSearchParams(); + errorParams.append( + 'error', + isError(error) ? error.name : 'server_error', + ); + errorParams.append( + 'error_description', + isError(error) ? error.message : 'Unknown error', + ); + if (state) { + errorParams.append('state', state as string); + } + + const redirectUrl = new URL(redirectUri as string); + redirectUrl.search = errorParams.toString(); + return res.redirect(redirectUrl.toString()); + } + }); + + // Authorization Session request details endpoint + // Returns Authorization Session request details for the frontend + router.get('/v1/sessions/:sessionId', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing Authorization Session ID', + }); + } + + try { + const session = await this.oidc.getAuthorizationSession({ + sessionId, + }); + + return res.json({ + id: session.id, + clientName: session.clientName, + scope: session.scope, + redirectUri: session.redirectUri, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to get authorization session: ${description}`, + error, + ); + return res.status(404).json({ + error: 'not_found', + error_description: description, + }); + } + }); + + // Authorization Session approval endpoint + // Handles user approval of Authorization Session requests and generates authorization codes + router.post('/v1/sessions/:sessionId/approve', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing authorization session ID', + }); + } + + try { + const httpCredentials = await this.httpAuth.credentials(req); + + if (!this.auth.isPrincipal(httpCredentials, 'user')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Authentication required', + }); + } + + const { userEntityRef } = httpCredentials.principal; + + const result = await this.oidc.approveAuthorizationSession({ + sessionId, + userEntityRef, + }); + + return res.json({ + redirectUrl: result.redirectUrl, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to approve authorization session: ${description}`, + error, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: description, + }); + } + }); + + // Authorization Session rejection endpoint + // Handles user rejection of Authorization Session requests and redirects with error + router.post('/v1/sessions/:sessionId/reject', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing authorization session ID', + }); + } + + const httpCredentials = await this.httpAuth.credentials(req); + + if (!this.auth.isPrincipal(httpCredentials, 'user')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Authentication required', + }); + } + + const { userEntityRef } = httpCredentials.principal; + try { + const session = await this.oidc.getAuthorizationSession({ + sessionId, + }); + + await this.oidc.rejectAuthorizationSession({ + sessionId, + userEntityRef, + }); + + const errorParams = new URLSearchParams(); + errorParams.append('error', 'access_denied'); + errorParams.append('error_description', 'User denied the request'); + if (session.state) { + errorParams.append('state', session.state); + } + + const redirectUrl = new URL(session.redirectUri); + redirectUrl.search = errorParams.toString(); + + return res.json({ + redirectUrl: redirectUrl.toString(), + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to reject authorization session: ${description}`, + error, + ); + + return res.status(400).json({ + error: 'invalid_request', + error_description: description, + }); + } + }); + + // Token endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest + // Exchanges authorization codes for access tokens and ID tokens + router.post('/v1/token', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + grant_type: grantType, + code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + } = req.body; + + if (!grantType || !code || !redirectUri) { + this.logger.error( + `Failed to exchange code for token: Missing required parameters`, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing required parameters', + }); + } + + try { + const result = await this.oidc.exchangeCodeForToken({ + code, + redirectUri, + codeVerifier, + grantType, + }); + + return res.json({ + access_token: result.accessToken, + token_type: result.tokenType, + expires_in: result.expiresIn, + id_token: result.idToken, + scope: result.scope, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to exchange code for token: ${description}`, + error, + ); + + if (isError(error)) { + if (error.name === 'AuthenticationError') { + return res.status(401).json({ + error: 'invalid_client', + error_description: error.message, + }); + } + if (error.name === 'InputError') { + return res.status(400).json({ + error: 'invalid_request', + error_description: error.message, + }); + } + } + + return res.status(500).json({ + error: 'server_error', + error_description: description, + }); + } + }); + + // Dynamic Client Registration endpoint + // https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration + // Allows clients to register themselves dynamically with the provider + router.post('/v1/register', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + client_name: clientName, + redirect_uris: redirectUris, + response_types: responseTypes, + grant_types: grantTypes, + scope, + } = req.body; + + if (!redirectUris?.length) { + res.status(400).json({ + error: 'invalid_request', + error_description: 'redirect_uris is required', + }); + return; + } + + try { + const client = await this.oidc.registerClient({ + clientName, + redirectUris, + responseTypes, + grantTypes, + scope, + }); + + res.status(201).json({ + client_id: client.clientId, + redirect_uris: client.redirectUris, + client_secret: client.clientSecret, + }); + } catch (e) { + const description = isError(e) ? e.message : 'Unknown error'; + this.logger.error(`Failed to register client: ${description}`, e); + + res.status(500).json({ + error: 'server_error', + error_description: `Failed to register client: ${description}`, + }); + } + }); + } + return router; } } +function ensureTrailingSlash(appUrl: string): string | URL | undefined { + if (appUrl.endsWith('/')) { + return appUrl; + } + return `${appUrl}/`; +} diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts new file mode 100644 index 0000000000..e4e1673397 --- /dev/null +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -0,0 +1,789 @@ +/* + * 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 { + mockServices, + TestDatabaseId, + TestDatabases, +} from '@backstage/backend-test-utils'; +import { OidcService } from './OidcService'; +import { + BackstageCredentials, + BackstageServicePrincipal, + BackstageUserPrincipal, + resolvePackagePath, +} from '@backstage/backend-plugin-api'; +import { AuthDatabase } from '../database/AuthDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import crypto from 'crypto'; +import { AnyJWK, TokenIssuer } from '../identity/types'; + +jest.setTimeout(60_000); + +describe('OidcService', () => { + const databases = TestDatabases.create(); + + async function createOidcService(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + const oidcDatabase = await OidcDatabase.create({ + database: AuthDatabase.create({ + getClient: async () => knex, + }), + }); + + const mockAuth = mockServices.auth.mock(); + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as jest.Mocked; + + const mockUserInfo = { + addUserInfo: jest.fn(), + getUserInfo: jest.fn(), + } as unknown as jest.Mocked; + + const mockConfig = mockServices.rootConfig.mock(); + + return { + service: OidcService.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://mock-base-url', + userInfo: mockUserInfo, + oidc: oidcDatabase, + config: mockConfig, + }), + mocks: { + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + userInfo: mockUserInfo, + config: mockConfig, + }, + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('getConfiguration', () => { + it('should return OIDC configuration', async () => { + const { service } = await createOidcService(databaseId); + + const config = service.getConfiguration(); + + expect(config).toEqual({ + issuer: 'http://mock-base-url', + token_endpoint: 'http://mock-base-url/v1/token', + userinfo_endpoint: 'http://mock-base-url/v1/userinfo', + jwks_uri: 'http://mock-base-url/.well-known/jwks.json', + response_types_supported: ['code', 'id_token'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: [ + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA', + ], + scopes_supported: ['openid'], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + ], + claims_supported: ['sub', 'ent'], + grant_types_supported: ['authorization_code'], + authorization_endpoint: 'http://mock-base-url/v1/authorize', + registration_endpoint: 'http://mock-base-url/v1/register', + code_challenge_methods_supported: ['S256', 'plain'], + }); + }); + }); + + describe('listPublicKeys', () => { + it('should return public keys from token issuer', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockKeys = [{ kid: 'key-1', use: 'sig' }] as AnyJWK[]; + mocks.tokenIssuer.listPublicKeys.mockResolvedValue({ keys: mockKeys }); + + const { keys } = await service.listPublicKeys(); + + expect(keys).toEqual(mockKeys); + expect(mocks.tokenIssuer.listPublicKeys).toHaveBeenCalledTimes(1); + }); + }); + + describe('getUserInfo', () => { + it('should return user info for valid token', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockCredentials: BackstageCredentials = { + principal: { + type: 'user', + userEntityRef: 'user:default/test', + }, + $$type: '@backstage/BackstageCredentials', + }; + const mockUserInfo = { sub: 'user:default/test', name: 'Test User' }; + + mocks.auth.authenticate.mockResolvedValue(mockCredentials); + mocks.auth.isPrincipal.mockReturnValue(true); + mocks.userInfo.getUserInfo.mockResolvedValue({ + claims: mockUserInfo, + }); + + const mockToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvdGVzdCJ9.signature'; + + const userInfo = await service.getUserInfo({ token: mockToken }); + + expect(userInfo).toEqual({ + claims: mockUserInfo, + }); + + expect(mocks.auth.authenticate).toHaveBeenCalledWith(mockToken, { + allowLimitedAccess: true, + }); + + expect(mocks.userInfo.getUserInfo).toHaveBeenCalledWith( + 'user:default/test', + ); + }); + + it('should throw error for non-user principal', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockCredentials: BackstageCredentials = + { + principal: { + type: 'service', + subject: 'test-service', + }, + $$type: '@backstage/BackstageCredentials', + }; + + mocks.auth.authenticate.mockResolvedValue(mockCredentials); + mocks.auth.isPrincipal.mockReturnValue(false); + + const mockToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvdGVzdCJ9.signature'; + + await expect(service.getUserInfo({ token: mockToken })).rejects.toThrow( + 'Userinfo endpoint must be called with a token that represents a user principal', + ); + }); + }); + + describe('registerClient', () => { + it('should create a new client with generated credentials', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + expect(client).toEqual( + expect.objectContaining({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }), + ); + expect(client.clientId).toBeDefined(); + expect(client.clientSecret).toBeDefined(); + }); + + it('should throw an error for invalid redirect URI', async () => { + const { + service, + mocks: { config }, + } = await createOidcService(databaseId); + + config.getOptionalStringArray.mockReturnValue([ + 'https://example.com/*', + ]); + + await expect( + service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://invalid.com/callback'], + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + + it('should create a new client with valid redirect URI', async () => { + const { + service, + mocks: { config }, + } = await createOidcService(databaseId); + + config.getOptionalStringArray.mockReturnValue(['cursor:*']); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['cursor://callback/asd?asd=asd'], + }); + + expect(client).toEqual( + expect.objectContaining({ + redirectUris: ['cursor://callback/asd?asd=asd'], + }), + ); + }); + + it('should create a client with default values', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + }); + + expect(client).toEqual( + expect.objectContaining({ + clientName: 'Test Client', + redirectUris: [], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }), + ); + }); + }); + + describe('createAuthorizationSession', () => { + it('should create a authorization session for valid client', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + expect(authSession).toEqual({ + id: expect.any(String), + clientName: 'Test Client', + scope: 'openid', + redirectUri: 'https://example.com/callback', + }); + }); + + it('should throw error for invalid client', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.createAuthorizationSession({ + clientId: 'invalid-client', + redirectUri: 'https://example.com/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid client_id'); + }); + + it('should throw error for invalid redirect URI', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://invalid.com/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + + it('should throw error for unsupported response type', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'token', + }), + ).rejects.toThrow('Only authorization code flow is supported'); + }); + + it('should handle PKCE parameters', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + }); + + expect(authSession.id).toBeDefined(); + }); + + it('should throw error for invalid PKCE method', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'invalid', + }), + ).rejects.toThrow('Invalid code_challenge_method'); + }); + }); + + describe('approveAuthorizationSession', () => { + it('should approve a valid authorization session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + state: 'test-state', + }); + + const result = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + expect(result.redirectUrl).toMatch( + /^https:\/\/example\.com\/callback\?code=.+&state=test-state$/, + ); + }); + + it('should throw error for invalid authorization session', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.approveAuthorizationSession({ + sessionId: 'invalid-session', + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Invalid authorization session'); + }); + + it('should throw error when trying to approve an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to approve an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + }); + + describe('getAuthorizationSession', () => { + it('should return authorization session details', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const details = await service.getAuthorizationSession({ + sessionId: authSession.id, + }); + + expect(details).toEqual( + expect.objectContaining({ + id: authSession.id, + clientId: client.clientId, + clientName: 'Test Client', + redirectUri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + responseType: 'code', + }), + ); + }); + + it('should throw error when trying to get an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to get an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + }); + + describe('rejectAuthorizationSession', () => { + it('should reject a authorization session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error for invalid authorization session', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.rejectAuthorizationSession({ + sessionId: 'invalid-session', + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Invalid authorization session'); + }); + + it('should throw error when trying to reject an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to reject an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + }); + + describe('exchangeCodeForToken', () => { + it('should exchange valid code for tokens', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + }); + + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + const tokenResult = await service.exchangeCodeForToken({ + code, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + }); + + expect(tokenResult).toEqual({ + accessToken: mockToken, + tokenType: 'Bearer', + expiresIn: 3600, + idToken: mockToken, + scope: 'openid', + }); + }); + + it('should throw error for invalid grant type', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.exchangeCodeForToken({ + code: 'test-code', + redirectUri: 'https://example.com/callback', + grantType: 'client_credentials', + }), + ).rejects.toThrow('Unsupported grant type'); + }); + + it('should handle PKCE verification', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const codeVerifier = 'test-code-verifier'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + const tokenResult = await service.exchangeCodeForToken({ + code, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + codeVerifier, + }); + + expect(tokenResult.accessToken).toBe(mockToken); + }); + + it('should throw error for invalid PKCE verifier', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const codeChallenge = 'test-challenge'; + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + await expect( + service.exchangeCodeForToken({ + code, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + codeVerifier: 'invalid-verifier', + }), + ).rejects.toThrow('Invalid code verifier'); + }); + }); + }); +}); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 5024b2cc8c..b4c6bb122b 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -13,11 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthService, RootConfigService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; -import { InputError } from '@backstage/errors'; +import { + InputError, + AuthenticationError, + NotFoundError, +} from '@backstage/errors'; import { decodeJwt } from 'jose'; +import crypto from 'crypto'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { DateTime } from 'luxon'; +import matcher from 'matcher'; export class OidcService { private constructor( @@ -25,6 +33,8 @@ export class OidcService { private readonly tokenIssuer: TokenIssuer, private readonly baseUrl: string, private readonly userInfo: UserInfoDatabase, + private readonly oidc: OidcDatabase, + private readonly config: RootConfigService, ) {} static create(options: { @@ -32,12 +42,16 @@ export class OidcService { tokenIssuer: TokenIssuer; baseUrl: string; userInfo: UserInfoDatabase; + oidc: OidcDatabase; + config: RootConfigService; }) { return new OidcService( options.auth, options.tokenIssuer, options.baseUrl, options.userInfo, + options.oidc, + options.config, ); } @@ -47,7 +61,7 @@ export class OidcService { token_endpoint: `${this.baseUrl}/v1/token`, userinfo_endpoint: `${this.baseUrl}/v1/userinfo`, jwks_uri: `${this.baseUrl}/.well-known/jwks.json`, - response_types_supported: ['id_token'], + response_types_supported: ['code', 'id_token'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: [ 'RS256', @@ -62,9 +76,15 @@ export class OidcService { 'EdDSA', ], scopes_supported: ['openid'], - token_endpoint_auth_methods_supported: [], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + ], claims_supported: ['sub', 'ent'], - grant_types_supported: [], + grant_types_supported: ['authorization_code'], + authorization_endpoint: `${this.baseUrl}/v1/authorize`, + registration_endpoint: `${this.baseUrl}/v1/register`, + code_challenge_methods_supported: ['S256', 'plain'], }; } @@ -89,4 +109,321 @@ export class OidcService { } return await this.userInfo.getUserInfo(userEntityRef); } + + public async registerClient(opts: { + responseTypes?: string[]; + grantTypes?: string[]; + clientName: string; + redirectUris?: string[]; + scope?: string; + }) { + const generatedClientId = crypto.randomUUID(); + const generatedClientSecret = crypto.randomUUID(); + + const allowedRedirectUriPatterns = this.config.getOptionalStringArray( + 'auth.experimentalDynamicClientRegistration.allowedRedirectUriPatterns', + ) ?? ['*']; + + for (const redirectUri of opts.redirectUris ?? []) { + if ( + !allowedRedirectUriPatterns.some(pattern => + matcher.isMatch(redirectUri, pattern), + ) + ) { + throw new InputError('Invalid redirect_uri'); + } + } + + return await this.oidc.createClient({ + clientId: generatedClientId, + clientName: opts.clientName, + clientSecret: generatedClientSecret, + redirectUris: opts.redirectUris ?? [], + responseTypes: opts.responseTypes ?? ['code'], + grantTypes: opts.grantTypes ?? ['authorization_code'], + scope: opts.scope, + }); + } + + public async createAuthorizationSession(opts: { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + }) { + const { + clientId, + redirectUri, + responseType, + scope, + state, + nonce, + codeChallenge, + codeChallengeMethod, + } = opts; + + if (responseType !== 'code') { + throw new InputError('Only authorization code flow is supported'); + } + + const client = await this.oidc.getClient({ clientId }); + if (!client) { + throw new InputError('Invalid client_id'); + } + + if (!client.redirectUris.includes(redirectUri)) { + throw new InputError('Invalid redirect_uri'); + } + + if (codeChallenge) { + if ( + !codeChallengeMethod || + !['S256', 'plain'].includes(codeChallengeMethod) + ) { + throw new InputError('Invalid code_challenge_method'); + } + } + + const sessionId = crypto.randomUUID(); + const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toJSDate(); + + await this.oidc.createAuthorizationSession({ + id: sessionId, + clientId, + redirectUri, + responseType, + scope, + state, + codeChallenge, + codeChallengeMethod, + nonce, + expiresAt: sessionExpiresAt, + }); + + return { + id: sessionId, + clientName: client.clientName, + scope, + redirectUri, + }; + } + + public async approveAuthorizationSession(opts: { + sessionId: string; + userEntityRef: string; + }) { + const { sessionId, userEntityRef } = opts; + + const session = await this.oidc.getAuthorizationSession({ + id: sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + + await this.oidc.updateAuthorizationSession({ + id: session.id, + userEntityRef, + status: 'approved', + }); + + const authorizationCode = crypto.randomBytes(32).toString('base64url'); + const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toJSDate(); + + await this.oidc.createAuthorizationCode({ + code: authorizationCode, + sessionId: session.id, + expiresAt: codeExpiresAt, + }); + + const redirectUrl = new URL(session.redirectUri); + + redirectUrl.searchParams.append('code', authorizationCode); + if (session.state) { + redirectUrl.searchParams.append('state', session.state); + } + + return { + redirectUrl: redirectUrl.toString(), + }; + } + + public async getAuthorizationSession(opts: { sessionId: string }) { + const session = await this.oidc.getAuthorizationSession({ + id: opts.sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + + const client = await this.oidc.getClient({ clientId: session.clientId }); + if (!client) { + throw new InputError('Invalid client_id'); + } + + return { + id: session.id, + clientId: session.clientId, + clientName: client.clientName, + redirectUri: session.redirectUri, + scope: session.scope, + state: session.state, + responseType: session.responseType, + codeChallenge: session.codeChallenge, + codeChallengeMethod: session.codeChallengeMethod, + nonce: session.nonce, + expiresAt: session.expiresAt, + status: session.status, + }; + } + + public async rejectAuthorizationSession(opts: { + sessionId: string; + userEntityRef: string; + }) { + const { sessionId, userEntityRef } = opts; + + const session = await this.oidc.getAuthorizationSession({ + id: sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + + await this.oidc.updateAuthorizationSession({ + id: session.id, + status: 'rejected', + userEntityRef, + }); + } + + public async exchangeCodeForToken(params: { + code: string; + redirectUri: string; + codeVerifier?: string; + grantType: string; + }) { + const { code, redirectUri, codeVerifier, grantType } = params; + + if (grantType !== 'authorization_code') { + throw new InputError('Unsupported grant type'); + } + + const authCode = await this.oidc.getAuthorizationCode({ code }); + if (!authCode) { + throw new AuthenticationError('Invalid authorization code'); + } + + if (DateTime.fromJSDate(authCode.expiresAt) < DateTime.now()) { + throw new AuthenticationError('Authorization code expired'); + } + + if (authCode.used) { + throw new AuthenticationError('Authorization code already used'); + } + + const session = await this.oidc.getAuthorizationSession({ + id: authCode.sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (session.redirectUri !== redirectUri) { + throw new AuthenticationError('Redirect URI mismatch'); + } + + if (session.status !== 'approved') { + throw new AuthenticationError('Authorization not approved'); + } + + if (!session.userEntityRef) { + throw new AuthenticationError('No user associated with authorization'); + } + + if (session.codeChallenge) { + if (!codeVerifier) { + throw new AuthenticationError('Code verifier required for PKCE'); + } + + if ( + !this.verifyPkce( + session.codeChallenge, + codeVerifier, + session.codeChallengeMethod, + ) + ) { + throw new AuthenticationError('Invalid code verifier'); + } + } + + await this.oidc.updateAuthorizationCode({ + code, + used: true, + }); + + const { token } = await this.tokenIssuer.issueToken({ + claims: { + sub: session.userEntityRef, + }, + }); + + return { + accessToken: token, + tokenType: 'Bearer', + expiresIn: 3600, + idToken: token, + scope: session.scope || 'openid', + }; + } + + private verifyPkce( + codeChallenge: string, + codeVerifier: string, + method?: string, + ): boolean { + if (!method || method === 'plain') { + return codeChallenge === codeVerifier; + } + + if (method === 'S256') { + const hash = crypto.createHash('sha256').update(codeVerifier).digest(); + const base64urlHash = hash.toString('base64url'); + return codeChallenge === base64urlHash; + } + + return false; + } } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 5012c90106..0f0d5b2830 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -21,6 +21,7 @@ import { AuthService, DatabaseService, DiscoveryService, + HttpAuthService, LoggerService, RootConfigService, } from '@backstage/backend-plugin-api'; @@ -40,6 +41,7 @@ import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { bindProviderRouters, ProviderFactories } from '../providers/router'; import { OidcRouter } from './OidcRouter'; +import { OidcDatabase } from '../database/OidcDatabase'; interface RouterOptions { logger: LoggerService; @@ -51,6 +53,7 @@ interface RouterOptions { providerFactories?: ProviderFactories; catalog: CatalogService; ownershipResolver?: AuthOwnershipResolver; + httpAuth: HttpAuthService; } export async function createRouter( @@ -63,6 +66,7 @@ export async function createRouter( database: db, tokenFactoryAlgorithm, providerFactories = {}, + httpAuth, } = options; const router = Router(); @@ -147,11 +151,18 @@ export async function createRouter( userInfo, }); + const oidc = await OidcDatabase.create({ database }); + const oidcRouter = OidcRouter.create({ auth: options.auth, tokenIssuer, baseUrl: authUrl, + appUrl, userInfo, + oidc, + logger, + httpAuth, + config, }); router.use(oidcRouter.getRouter()); diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index 48554f6932..3c2ceea7c0 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-node +## 0.6.7-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 0.6.7-next.0 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 23cff9720c..64c12ae977 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,10 +1,11 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.6.7-next.0", + "version": "0.6.7-next.1", "backstage": { "role": "node-library", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index 1dc720ab67..9b314d3e4f 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-react +## 0.1.19-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + ## 0.1.19-next.0 ### Patch Changes diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 4335bbc03a..2bfee0dd44 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,11 +1,12 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.19-next.0", + "version": "0.1.19-next.1", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" diff --git a/plugins/auth/.eslintrc.js b/plugins/auth/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth/CHANGELOG.md b/plugins/auth/CHANGELOG.md new file mode 100644 index 0000000000..d28fef1c9e --- /dev/null +++ b/plugins/auth/CHANGELOG.md @@ -0,0 +1,12 @@ +# @backstage/plugin-auth + +## 0.1.0-next.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 diff --git a/plugins/auth/README.md b/plugins/auth/README.md new file mode 100644 index 0000000000..440d7d294a --- /dev/null +++ b/plugins/auth/README.md @@ -0,0 +1,16 @@ +# @backstage/plugin-auth + +A Backstage frontend plugin that provides user interface components for authentication flows, specifically for OpenID Connect (OIDC) consent management. + +## Installation + +This plugin is designed to work with the `@backstage/plugin-auth-backend` package that provides OIDC provider functionality. + +```bash +# From your Backstage app directory +yarn --cwd packages/app add @backstage/plugin-auth +``` + +## Usage + +The plugin provides the route `/oauth2/authorize/:sessionId` for approving of oauth2 sessions for clients. You should see an approval flow for any sessions created through the `auth-backend`. diff --git a/plugins/auth/catalog-info.yaml b/plugins/auth/catalog-info.yaml new file mode 100644 index 0000000000..66d835a198 --- /dev/null +++ b/plugins/auth/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth + title: '@backstage/plugin-auth' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: auth-maintainers diff --git a/plugins/auth/dev/index.tsx b/plugins/auth/dev/index.tsx new file mode 100644 index 0000000000..b0762416c6 --- /dev/null +++ b/plugins/auth/dev/index.tsx @@ -0,0 +1,28 @@ +/* + * 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 { createApp } from '@backstage/frontend-defaults'; +import { createRoot } from 'react-dom/client'; + +import plugin from '../src'; + +const app = createApp({ + features: [plugin], +}); + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render(app.createRoot()); diff --git a/plugins/auth/package.json b/plugins/auth/package.json new file mode 100644 index 0000000000..6c8184db35 --- /dev/null +++ b/plugins/auth/package.json @@ -0,0 +1,84 @@ +{ + "name": "@backstage/plugin-auth", + "version": "0.1.0-next.0", + "backstage": { + "role": "frontend-plugin", + "pluginId": "auth", + "pluginPackages": [ + "@backstage/plugin-auth", + "@backstage/plugin-auth-backend", + "@backstage/plugin-auth-node", + "@backstage/plugin-auth-react" + ] + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth" + }, + "license": "Apache-2.0", + "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.61", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/frontend-defaults": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.0.0", + "@types/react": "^18.0.0", + "msw": "^1.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router-dom": "^6.3.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "react-router-dom": "^6.3.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } +} diff --git a/plugins/auth/report.api.md b/plugins/auth/report.api.md new file mode 100644 index 0000000000..4c7d2d4a11 --- /dev/null +++ b/plugins/auth/report.api.md @@ -0,0 +1,52 @@ +## API Report File for "@backstage/plugin-auth" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { RouteRef } from '@backstage/frontend-plugin-api'; + +// @public (undocumented) +const _default: OverridableFrontendPlugin< + { + root: RouteRef; + }, + {}, + { + 'page:auth': ExtensionDefinition<{ + kind: 'page'; + name: undefined; + config: { + path: string | undefined; + }; + configInput: { + path?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + loader: () => Promise; + routeRef?: RouteRef; + }; + }>; + } +>; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx new file mode 100644 index 0000000000..0f071025a2 --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx @@ -0,0 +1,246 @@ +/* + * 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 { useParams } from 'react-router-dom'; + +import { + Box, + Button, + Card, + CardContent, + CardActions, + Typography, + makeStyles, + Divider, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import CancelIcon from '@material-ui/icons/Cancel'; +import AppsIcon from '@material-ui/icons/Apps'; +import WarningIcon from '@material-ui/icons/Warning'; +import { + Header, + Page, + Content, + Progress, + EmptyState, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { useConsentSession } from './useConsentSession'; + +const useStyles = makeStyles(theme => ({ + authCard: { + maxWidth: 600, + margin: '0 auto', + marginTop: theme.spacing(4), + }, + appHeader: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(2), + }, + appIcon: { + marginRight: theme.spacing(2), + fontSize: 40, + }, + appName: { + fontSize: '1.5rem', + fontWeight: 'bold', + }, + securityWarning: { + margin: theme.spacing(2, 0), + }, + buttonContainer: { + display: 'flex', + justifyContent: 'space-between', + gap: theme.spacing(2), + padding: theme.spacing(2), + }, + callbackUrl: { + fontFamily: 'monospace', + backgroundColor: theme.palette.background.default, + padding: theme.spacing(1), + borderRadius: theme.shape.borderRadius, + wordBreak: 'break-all', + fontSize: '0.875rem', + }, + scopeList: { + backgroundColor: theme.palette.background.default, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(1), + }, +})); + +const ConsentPageLayout = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => ( + +
+ {children} + +); + +export const ConsentPage = () => { + const classes = useStyles(); + const { sessionId } = useParams<{ sessionId: string }>(); + const { state, handleAction } = useConsentSession({ sessionId }); + + if (!sessionId) { + return ( + + + + ); + } + + if (state.status === 'loading') { + return ( + + + + + + ); + } + + if (state.status === 'error') { + return ( + + + + ); + } + + if (state.status === 'completed') { + return ( + + + + + {state.action === 'approve' ? ( + + ) : ( + + )} + + {state.action === 'approve' + ? 'Authorization Approved' + : 'Authorization Denied'} + + + {state.action === 'approve' + ? 'You have successfully authorized the application to access your Backstage account.' + : 'You have denied the application access to your Backstage account.'} + + + Redirecting to the application... + + + + + + ); + } + + const session = state.session; + const isSubmitting = state.status === 'submitting'; + const appName = session.clientName ?? session.clientId; + + return ( + + + + + + + {appName} + + wants to access your Backstage account + + + + + + + } + className={classes.securityWarning} + > + + Security Notice: By authorizing this application, + you are granting it access to your Backstage account. The + application will receive an access token that allows it to act on + your behalf. + + + + Callback URL: + + {session.redirectUri} + + + + + + Make sure you trust this application and recognize the callback + URL above. Only authorize applications you trust. + + + + + + + + + + + ); +}; diff --git a/plugins/auth/src/components/ConsentPage/index.ts b/plugins/auth/src/components/ConsentPage/index.ts new file mode 100644 index 0000000000..9fdd44c96d --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/index.ts @@ -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 { ConsentPage } from './ConsentPage'; diff --git a/plugins/auth/src/components/ConsentPage/useConsentSession.ts b/plugins/auth/src/components/ConsentPage/useConsentSession.ts new file mode 100644 index 0000000000..a19faae6fe --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/useConsentSession.ts @@ -0,0 +1,144 @@ +/* + * 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 { + useApi, + alertApiRef, + fetchApiRef, + discoveryApiRef, +} from '@backstage/frontend-plugin-api'; +import { useCallback } from 'react'; +import useAsync from 'react-use/esm/useAsync'; +import useAsyncFn from 'react-use/esm/useAsyncFn'; +import { isError } from '@backstage/errors'; + +interface Session { + id: string; + clientName?: string; + clientId: string; + redirectUri: string; + scopes?: string[]; + responseType?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + expiresAt?: string; +} + +type ConsentState = + | { status: 'loading' } + | { status: 'error'; error: string } + | { status: 'loaded'; session: Session } + | { status: 'submitting'; session: Session; action: 'approve' | 'reject' } + | { status: 'completed'; action: 'approve' | 'reject' }; + +export const useConsentSession = (opts: { sessionId?: string }) => { + const alertApi = useApi(alertApiRef); + const fetchApi = useApi(fetchApiRef); + const discoveryApi = useApi(discoveryApiRef); + const { sessionId } = opts; + + const sessionState = useAsync(async () => { + if (!sessionId) { + throw new Error('Session ID is missing'); + } + + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${sessionId}`, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return (await response.json()) as Session; + }, [sessionId]); + + const [actionState, handleActionInternal] = useAsyncFn( + async (action: 'approve' | 'reject', session: Session) => { + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${session.id}/${action}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.redirectUrl) { + window.location.href = result.redirectUrl; + } + + return { action, redirectUrl: result.redirectUrl }; + }, + [discoveryApi, fetchApi], + ); + + const getConsentState = (): ConsentState => { + if (actionState.value) { + return { status: 'completed', action: actionState.value.action }; + } + if (actionState.loading && sessionState.value) { + return { + status: 'submitting', + session: sessionState.value, + action: 'approve', // This will be set properly when called + }; + } + if (sessionState.error) { + return { + status: 'error', + error: isError(sessionState.error) + ? sessionState.error.message + : 'Failed to load consent request', + }; + } + if (sessionState.value) { + return { status: 'loaded', session: sessionState.value }; + } + return { status: 'loading' }; + }; + + const state = getConsentState(); + return { + state, + handleAction: useCallback( + async (action: 'approve' | 'reject') => { + if (state.status !== 'loaded') return; + + try { + await handleActionInternal(action, state.session); + } catch (err) { + alertApi.post({ + message: isError(err) ? err.message : `Failed to ${action} consent`, + severity: 'error', + }); + } + }, + [state, handleActionInternal, alertApi], + ), + }; +}; diff --git a/plugins/auth/src/components/Router.tsx b/plugins/auth/src/components/Router.tsx new file mode 100644 index 0000000000..fbb5f9aaa9 --- /dev/null +++ b/plugins/auth/src/components/Router.tsx @@ -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 { Routes, Route } from 'react-router-dom'; +import { ConsentPage } from './ConsentPage'; + +/** + * Router component for the auth plugin + * @public + */ +export const Router = () => { + return ( + + } /> + + ); +}; diff --git a/plugins/auth/src/index.ts b/plugins/auth/src/index.ts new file mode 100644 index 0000000000..717cdd4672 --- /dev/null +++ b/plugins/auth/src/index.ts @@ -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 { default } from './plugin'; diff --git a/plugins/auth/src/plugin.test.ts b/plugins/auth/src/plugin.test.ts new file mode 100644 index 0000000000..a50b718e5d --- /dev/null +++ b/plugins/auth/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { default as authPlugin } from './plugin'; + +describe('auth', () => { + it('should export plugin', () => { + expect(authPlugin).toBeDefined(); + }); +}); diff --git a/plugins/auth/src/plugin.tsx b/plugins/auth/src/plugin.tsx new file mode 100644 index 0000000000..4ed5d57223 --- /dev/null +++ b/plugins/auth/src/plugin.tsx @@ -0,0 +1,36 @@ +/* + * 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 { + createFrontendPlugin, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; +import { rootRouteRef } from './routes'; + +export const AuthPage = PageBlueprint.make({ + params: { + path: '/oauth2', + routeRef: rootRouteRef, + loader: () => import('./components/Router').then(m => ), + }, +}); + +export default createFrontendPlugin({ + pluginId: 'auth', + extensions: [AuthPage], + routes: { + root: rootRouteRef, + }, +}); diff --git a/plugins/auth/src/routes.ts b/plugins/auth/src/routes.ts new file mode 100644 index 0000000000..09e6a48da4 --- /dev/null +++ b/plugins/auth/src/routes.ts @@ -0,0 +1,18 @@ +/* + * 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 { createRouteRef } from '@backstage/frontend-plugin-api'; + +export const rootRouteRef = createRouteRef(); diff --git a/plugins/auth/src/setupTests.ts b/plugins/auth/src/setupTests.ts new file mode 100644 index 0000000000..b57590b525 --- /dev/null +++ b/plugins/auth/src/setupTests.ts @@ -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. + */ +import '@testing-library/jest-dom'; diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 3ba5538fc5..7ceff47a80 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + ## 0.4.15-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 9f98b788a7..4dfe5b990d 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.4.15-next.0", + "version": "0.4.15-next.1", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 31364b56b3..0962c2aa28 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.3.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 51956ca1a5..dad34fec2a 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.3.9-next.0", + "version": "0.3.9-next.1", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 7f31323ddd..d40c4e03e4 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index db55d563de..f004e1bb16 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.5.6-next.0", + "version": "0.5.6-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index edb0a14cf3..c462210097 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.5.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 56ab813052..97eea0897f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.5.3-next.0", + "version": "0.5.3-next.1", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 42964bad70..66965f3009 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.5.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index a3cf31ee9a..98d28131a9 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.5.3-next.0", + "version": "0.5.3-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 9cf3bcb98f..fa30682d37 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.3.12-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index ac095bfc4a..5459221795 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.3.12-next.0", + "version": "0.3.12-next.1", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index eaf94c852a..ea0a8a0bbb 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.3.6-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 62f17f1b26..9dac6c9526 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.3.6-next.0", + "version": "0.3.6-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gitea/CHANGELOG.md b/plugins/catalog-backend-module-gitea/CHANGELOG.md index 036dd6b477..f8b03e496c 100644 --- a/plugins/catalog-backend-module-gitea/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gitea +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json index 3aaf419c82..8389ee70ef 100644 --- a/plugins/catalog-backend-module-gitea/package.json +++ b/plugins/catalog-backend-module-gitea/package.json @@ -1,10 +1,12 @@ { "name": "@backstage/plugin-catalog-backend-module-gitea", - "version": "0.1.4-next.0", - "license": "Apache-2.0", + "version": "0.1.4-next.1", "description": "The gitea backend module for the catalog plugin.", - "main": "src/index.ts", - "types": "src/index.ts", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "catalog", + "pluginPackage": "@backstage/plugin-catalog-backend" + }, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -15,19 +17,20 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/catalog-backend-module-gitea" }, - "backstage": { - "role": "backend-plugin-module", - "pluginId": "catalog", - "pluginPackage": "@backstage/plugin-catalog-backend" - }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], "scripts": { - "start": "backstage-cli package start", "build": "backstage-cli package build", - "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", "prepack": "backstage-cli package prepack", - "postpack": "backstage-cli package postpack" + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", @@ -42,8 +45,5 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^" - }, - "files": [ - "dist" - ] + } } diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index d6110b1f5a..9007c5ab97 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + ## 0.3.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 04029f57b9..ff2b78abc1 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.3.14-next.0", + "version": "0.3.14-next.1", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index b412ef8780..444d602783 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-github +## 0.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.11.0-next.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index a61deeb214..42c8446a75 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.11.0-next.0", + "version": "0.11.0-next.1", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index afd3317620..88d11fb806 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 05b9e75a0c..98d45ebe61 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.2.13-next.0", + "version": "0.2.13-next.1", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 958c3b364a..adbc95ee65 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.7.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 7d8fe59327..14d62a2ad8 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.7.3-next.0", + "version": "0.7.3-next.1", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 9139c3e3f1..1d93406c49 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.7.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 5a2acc53fc..7a83dd50af 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.7.4-next.0", + "version": "0.7.4-next.1", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 83b25b4f4c..343a87c514 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.11.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.11.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 9e32b21bb3..02e2216d09 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.11.9-next.0", + "version": "0.11.9-next.1", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index b48d58fbd2..0ecb111c7a 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.8.0-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index facfd228a3..12fcfa0585 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.8.0-next.1", + "version": "0.8.0-next.2", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 6c82466060..4a1c9eca0a 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.2.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 3902aafcb7..fec08415cd 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.2.14-next.0", + "version": "0.2.14-next.1", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 141e2d52ad..dee1e885fc 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.2.14-next.1 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.2.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index bcbdcbef48..51c43d475f 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.2.14-next.0", + "version": "0.2.14-next.1", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", @@ -24,16 +24,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/plugins/catalog-backend-module-puppetdb/report-alpha.api.md b/plugins/catalog-backend-module-puppetdb/report-alpha.api.md deleted file mode 100644 index 211add3895..0000000000 --- a/plugins/catalog-backend-module-puppetdb/report-alpha.api.md +++ /dev/null @@ -1,13 +0,0 @@ -## API Report File for "@backstage/plugin-catalog-backend-module-puppetdb" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; - -// @alpha -const catalogModulePuppetDbEntityProvider: BackendFeature; -export default catalogModulePuppetDbEntityProvider; - -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/catalog-backend-module-puppetdb/report.api.md b/plugins/catalog-backend-module-puppetdb/report.api.md index f6fa5bfc83..6b846b29dd 100644 --- a/plugins/catalog-backend-module-puppetdb/report.api.md +++ b/plugins/catalog-backend-module-puppetdb/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; @@ -16,6 +17,10 @@ import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugi // @public export const ANNOTATION_PUPPET_CERTNAME = 'puppet.com/certname'; +// @public +const catalogModulePuppetDbEntityProvider: BackendFeature; +export default catalogModulePuppetDbEntityProvider; + // @public export const DEFAULT_PROVIDER_ID = 'default'; diff --git a/plugins/catalog-backend-module-puppetdb/src/index.ts b/plugins/catalog-backend-module-puppetdb/src/index.ts index b5c5f9d209..9b10bd630f 100644 --- a/plugins/catalog-backend-module-puppetdb/src/index.ts +++ b/plugins/catalog-backend-module-puppetdb/src/index.ts @@ -22,3 +22,4 @@ export * from './providers'; export * from './puppet'; +export { default } from './module'; diff --git a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts index aa524f2409..1ab95c90d8 100644 --- a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts +++ b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts @@ -24,7 +24,7 @@ import { PuppetDbEntityProvider } from '../providers/PuppetDbEntityProvider'; /** * Registers the `PuppetDbEntityProvider` with the catalog processing extension point. * - * @alpha + * @public */ export const catalogModulePuppetDbEntityProvider = createBackendModule({ pluginId: 'catalog', diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 4a6c195039..9c17f17eaa 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 6e040f8078..7019619a6c 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.2.12-next.0", + "version": "0.2.12-next.1", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index 28436537b0..ede26e78d1 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.6.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.6.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index a3de0c75a4..e949240c64 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.6.4-next.0", + "version": "0.6.4-next.1", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index ad535f16fd..52ebffab31 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend +## 3.0.2-next.1 + +### Patch Changes + +- 2204f5b: Prevent deadlock in catalog deferred stitching +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 3.0.2-next.0 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index d825f29356..7a2df294fd 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "3.0.2-next.0", + "version": "3.0.2-next.1", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts index 1afa0b9c30..ff450e9ccf 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts @@ -435,4 +435,139 @@ describe('markForStitching', () => { } }, ); + + it.each(databases.eachSupportedId())( + 'reproduces deadlock scenario when concurrent transactions update overlapping entity sets %p', + async databaseId => { + const knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + + // Setup test data with multiple entities + const entityRefs = [ + 'k:ns/entity-a', + 'k:ns/entity-b', + 'k:ns/entity-c', + 'k:ns/entity-d', + 'k:ns/entity-e', + 'k:ns/entity-f', + ]; + + await knex('refresh_state').insert( + entityRefs.map((ref, i) => ({ + entity_id: `${i + 1}`, + entity_ref: ref, + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + next_stitch_at: null, + next_stitch_ticket: null, + })), + ); + + // This test attempts to reproduce the deadlock by running concurrent transactions + // that update overlapping sets of entities in different orders + const errorResults = []; + + for (let attempt = 0; attempt < 10; attempt++) { + // Transaction 1: Update entities A, B, C, D, E + const transaction1 = knex.transaction(async trx => { + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: [ + 'k:ns/entity-a', + 'k:ns/entity-b', + 'k:ns/entity-c', + 'k:ns/entity-d', + 'k:ns/entity-e', + ], + }); + + // Add a small delay to increase chance of collision + await new Promise(resolve => setTimeout(resolve, 10)); + + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: ['k:ns/entity-f'], + }); + }); + + // Transaction 2: Update entities F, E, D, C, B (reverse order) + const transaction2 = knex.transaction(async trx => { + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: [ + 'k:ns/entity-f', + 'k:ns/entity-e', + 'k:ns/entity-d', + 'k:ns/entity-c', + 'k:ns/entity-b', + ], + }); + + // Add a small delay to increase chance of collision + await new Promise(resolve => setTimeout(resolve, 10)); + + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: ['k:ns/entity-a'], + }); + }); + + // Run both transactions concurrently to create potential deadlock + errorResults.push( + Promise.allSettled([transaction1, transaction2]).then(results => + results + .filter(r => r.status === 'rejected') + .map(r => (r as PromiseRejectedResult).reason), + ), + ); + } + + const allResults = await Promise.all(errorResults); + + const deadlockErrors = allResults + .flat() + .filter( + error => + error?.code === '40P01' || + error?.message?.includes('deadlock detected') || + error?.message?.includes('deadlock'), + ); + expect(deadlockErrors).toEqual([]); + + // Verify final state - all entities should have been marked for stitching + const finalState = await knex('refresh_state') + .select('entity_ref', 'next_stitch_at', 'next_stitch_ticket') + .whereNotNull('next_stitch_at') + .orderBy('entity_ref'); + + expect(finalState.length).toBeGreaterThan(0); + finalState.forEach(row => { + expect(row.next_stitch_at).not.toBeNull(); + expect(row.next_stitch_ticket).not.toBeNull(); + }); + }, + ); }); diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts index ecc364a9cc..484d6e4ed6 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts @@ -17,9 +17,34 @@ import { Knex } from 'knex'; import splitToChunks from 'lodash/chunk'; import { v4 as uuid } from 'uuid'; +import { ErrorLike, isError } from '@backstage/errors'; import { StitchingStrategy } from '../../../stitching/types'; +import { setTimeout as sleep } from 'timers/promises'; import { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables'; +const UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention +const DEADLOCK_RETRY_ATTEMPTS = 3; +const DEADLOCK_BASE_DELAY_MS = 25; + +// PostgreSQL deadlock error code +const POSTGRES_DEADLOCK_SQLSTATE = '40P01'; + +/** + * Checks if the given error is a deadlock error for the database engine in use. + */ +function isDeadlockError( + knex: Knex | Knex.Transaction, + e: unknown, +): e is ErrorLike { + if (knex.client.config.client.includes('pg')) { + // PostgreSQL deadlock detection + return isError(e) && e.code === POSTGRES_DEADLOCK_SQLSTATE; + } + + // Add more database engine checks here as needed + return false; +} + /** * Marks a number of entities for stitching some time in the near * future. @@ -32,9 +57,8 @@ export async function markForStitching(options: { entityRefs?: Iterable; entityIds?: Iterable; }): Promise { - // Splitting to chunks just to cover pathological cases that upset the db - const entityRefs = split(options.entityRefs); - const entityIds = split(options.entityIds); + const entityRefs = sortSplit(options.entityRefs); + const entityIds = sortSplit(options.entityIds); const knex = options.knex; const mode = options.strategy.mode; @@ -51,13 +75,15 @@ export async function markForStitching(options: { .select('entity_id') .whereIn('entity_ref', chunk), ); - await knex - .table('refresh_state') - .update({ - result_hash: 'force-stitching', - next_update_at: knex.fn.now(), - }) - .whereIn('entity_ref', chunk); + await retryOnDeadlock(async () => { + await knex + .table('refresh_state') + .update({ + result_hash: 'force-stitching', + next_update_at: knex.fn.now(), + }) + .whereIn('entity_ref', chunk); + }, knex); } for (const chunk of entityIds) { @@ -67,44 +93,74 @@ export async function markForStitching(options: { hash: 'force-stitching', }) .whereIn('entity_id', chunk); - await knex - .table('refresh_state') - .update({ - result_hash: 'force-stitching', - next_update_at: knex.fn.now(), - }) - .whereIn('entity_id', chunk); + await retryOnDeadlock(async () => { + await knex + .table('refresh_state') + .update({ + result_hash: 'force-stitching', + next_update_at: knex.fn.now(), + }) + .whereIn('entity_id', chunk); + }, knex); } } else if (mode === 'deferred') { // It's OK that this is shared across refresh state rows; it just needs to // be uniquely generated for every new stitch request. const ticket = uuid(); + // Update by primary key in deterministic order to avoid deadlocks for (const chunk of entityRefs) { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) - .whereIn('entity_ref', chunk); + await retryOnDeadlock(async () => { + await knex('refresh_state') + .update({ + next_stitch_at: knex.fn.now(), + next_stitch_ticket: ticket, + }) + .whereIn('entity_ref', chunk); + }, knex); } for (const chunk of entityIds) { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) - .whereIn('entity_id', chunk); + await retryOnDeadlock(async () => { + await knex('refresh_state') + .update({ + next_stitch_at: knex.fn.now(), + next_stitch_ticket: ticket, + }) + .whereIn('entity_id', chunk); + }, knex); } } else { throw new Error(`Unknown stitching strategy mode ${mode}`); } } -function split(input: Iterable | undefined): string[][] { +function sortSplit(input: Iterable | undefined): string[][] { if (!input) { return []; } - return splitToChunks(Array.isArray(input) ? input : [...input], 200); + const array = Array.isArray(input) ? input.slice() : [...input]; + array.sort(); + return splitToChunks(array, UPDATE_CHUNK_SIZE); +} + +async function retryOnDeadlock( + fn: () => Promise, + knex: Knex | Knex.Transaction, + retries = DEADLOCK_RETRY_ATTEMPTS, + baseMs = DEADLOCK_BASE_DELAY_MS, +): Promise { + let attempt = 0; + for (;;) { + try { + return await fn(); + } catch (e: unknown) { + if (isDeadlockError(knex, e) && attempt < retries) { + await sleep(baseMs * Math.pow(2, attempt)); + attempt++; + continue; + } + throw e; + } + } } diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index a756916c38..5fa578c1ad 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-graph +## 0.4.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.4.23-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 7cea775bbe..1167fe111b 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.23-next.1", + "version": "0.4.23-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 77ad27ced9..ecd0d8bc46 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-import +## 0.13.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.13.5-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 050e8be908..39ed5bcef0 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.13.5-next.1", + "version": "0.13.5-next.2", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 18f44ba88f..14ac349b6d 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-node +## 1.19.0-next.1 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 1.18.1-next.0 ### Patch Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index a61c4b801b..3111765897 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.18.1-next.0", + "version": "1.19.0-next.1", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index d6eb0dc5fe..7ab8dcd967 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-catalog-react +## 1.21.0-next.2 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.20.2-next.1 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 3912d9ce9d..0b072f51ce 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.20.2-next.1", + "version": "1.21.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 19c35e2137..cddfdf18bc 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog +## 1.31.3-next.2 + +### Patch Changes + +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.31.3-next.1 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 9e1b0fcc05..fd0c1c4375 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.31.3-next.1", + "version": "1.31.3-next.2", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 4f7f0427b1..6043ecd1a1 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -22,7 +22,7 @@ import { export const createComponentRouteRef = createExternalRouteRef({ id: 'create-component', optional: true, - defaultTarget: 'scaffolder.createComponent', + defaultTarget: 'scaffolder.root', }); export const viewTechDocRouteRef = createExternalRouteRef({ diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 4da746e10a..dbd27d37d9 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-devtools-backend +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + ## 0.5.9-next.0 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index 038fcbce82..5bd90bdd41 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.5.9-next.0", + "version": "0.5.9-next.1", "backstage": { "role": "backend-plugin", "pluginId": "devtools", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index ea7326d9b3..1d2709648f 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-home +## 0.8.12-next.2 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.8.12-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 21469df7cd..bc8d71d23b 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.12-next.1", + "version": "0.8.12-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index 5d85e834e8..fec7895170 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -34,7 +34,11 @@ import { } from '@material-ui/core/styles'; import { compact } from 'lodash'; import useObservable from 'react-use/esm/useObservable'; -import { ContentHeader, ErrorBoundary } from '@backstage/core-components'; +import { + ContentHeader, + ErrorBoundary, + Progress, +} from '@backstage/core-components'; import Typography from '@material-ui/core/Typography'; import { WidgetSettingsOverlay } from './WidgetSettingsOverlay'; import { AddWidgetDialog } from './AddWidgetDialog'; @@ -90,7 +94,7 @@ const useStyles = makeStyles((theme: Theme) => function useHomeStorage( defaultWidgets: GridWidget[], -): [GridWidget[], (value: GridWidget[]) => void] { +): [GridWidget[], (value: GridWidget[]) => void, boolean] { const key = 'home'; const storageApi = useApi(storageApiRef).forBucket('home.customHomepage'); // TODO: Support multiple home pages @@ -110,6 +114,9 @@ function useHomeStorage( storageApi.observe$(key), storageApi.snapshot(key), ); + + const isStorageLoading = homeSnapshot.presence === 'unknown' || !homeSnapshot; + const widgets: GridWidget[] = useMemo(() => { if (homeSnapshot.presence === 'absent') { return defaultWidgets; @@ -122,7 +129,7 @@ function useHomeStorage( } }, [homeSnapshot, defaultWidgets]); - return [widgets, setWidgets]; + return [widgets, setWidgets, isStorageLoading]; } const convertConfigToDefaultWidgets = ( @@ -213,7 +220,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ? convertConfigToDefaultWidgets(props.config, availableWidgets) : []; }, [props.config, availableWidgets]); - const [widgets, setWidgets] = useHomeStorage(defaultLayout); + const [widgets, setWidgets, isStorageLoading] = useHomeStorage(defaultLayout); const [addWidgetDialogOpen, setAddWidgetDialogOpen] = useState(false); const editModeOn = widgets.find(w => w.layout.isResizable) !== undefined; const [editMode, setEditMode] = useState(editModeOn); @@ -322,6 +329,10 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ); }; + if (isStorageLoading) { + return ; + } + return ( <> diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 761e87e5e0..0986e613f8 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kubernetes-backend +## 0.20.2-next.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + ## 0.20.2-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index bae94407b5..15722c373d 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.20.2-next.1", + "version": "0.20.2-next.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", diff --git a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts index c32023235f..1b0b9a31e1 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts @@ -183,8 +183,9 @@ export class KubernetesInitializer { async init() { const fetcher = - (await this.opts.fetcher?.({ getDefault: this.defaultFetcher })) ?? - (await this.defaultFetcher()); + (await this.opts.fetcher?.({ + getDefault: () => this.defaultFetcher(), + })) ?? (await this.defaultFetcher()); const authStrategyMap = this.opts.authStrategyMap ?? (await this.defaultAuthStrategy()); diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 63ee864a46..af3a23232d 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 0.0.29-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 6488b959f6..0f9ab60401 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.29-next.1", + "version": "0.0.29-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 6f76f6bc2a..5a1643287e 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes +## 0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.12.11-next.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 3fd141319c..d0b553a4eb 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.11-next.1", + "version": "0.12.11-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/mcp-actions-backend/CHANGELOG.md b/plugins/mcp-actions-backend/CHANGELOG.md index 0ca964fbd1..e7578dff07 100644 --- a/plugins/mcp-actions-backend/CHANGELOG.md +++ b/plugins/mcp-actions-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-mcp-actions-backend +## 0.1.3-next.1 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 2a7fbdf8a3..4ed2250925 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mcp-actions-backend", - "version": "0.1.3-next.0", + "version": "0.1.3-next.1", "backstage": { "role": "backend-plugin", "pluginId": "mcp-actions", diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index f1e89417e9..d04df6cdf5 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -17,7 +17,8 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { json, Router } from 'express'; +import { json } from 'express'; +import Router from 'express-promise-router'; import { McpService } from './services/McpService'; import { createStreamableRouter } from './routers/createStreamableRouter'; import { createSseRouter } from './routers/createSseRouter'; @@ -42,8 +43,19 @@ export const mcpPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, actions: actionsServiceRef, registry: actionsRegistryServiceRef, + rootRouter: coreServices.rootHttpRouter, + discovery: coreServices.discovery, + config: coreServices.rootConfig, }, - async init({ actions, logger, httpRouter, httpAuth }) { + async init({ + actions, + logger, + httpRouter, + httpAuth, + rootRouter, + discovery, + config, + }) { const mcpService = await McpService.create({ actions, }); @@ -66,6 +78,26 @@ export const mcpPlugin = createBackendPlugin({ router.use('/v1', streamableRouter); httpRouter.use(router); + + if ( + config.getOptionalBoolean( + 'auth.experimentalDynamicClientRegistration.enabled', + ) + ) { + // This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by + // many of the MCP client as of yet. So this seems to be the oldest version of the spec thats implemented. + rootRouter.use( + '/.well-known/oauth-authorization-server', + async (_, res) => { + const authBaseUrl = await discovery.getBaseUrl('auth'); + const oidcResponse = await fetch( + `${authBaseUrl}/.well-known/openid-configuration`, + ); + + res.json(await oidcResponse.json()); + }, + ); + } }, }); }, diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 9a46dad0bc..8619edd6f4 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-notifications-backend-module-email +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.3.13-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 602edf0e66..6844a77d3c 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.3.13-next.0", + "version": "0.3.13-next.1", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-slack/CHANGELOG.md b/plugins/notifications-backend-module-slack/CHANGELOG.md index e38e8f9d07..a9d8074e5f 100644 --- a/plugins/notifications-backend-module-slack/CHANGELOG.md +++ b/plugins/notifications-backend-module-slack/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-notifications-backend-module-slack +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 227201984b..6b32e1b31d 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-slack", - "version": "0.1.5-next.0", + "version": "0.1.5-next.1", "description": "The slack backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 41905ec918..3391980f44 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-notifications-backend +## 0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.5.10-next.0 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 94c42005c0..a7933b0fc8 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.5.10-next.0", + "version": "0.5.10-next.1", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index af3c020fd3..77e35d0996 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-notifications-node +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 0.2.19-next.0 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index c20cf4c820..234d9d342f 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.19-next.0", + "version": "0.2.19-next.1", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index 6d32150c35..ca4be58209 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -23,6 +23,7 @@ import { } from '@backstage/frontend-plugin-api'; import { rootRouteRef } from './routes'; import { + compatWrapper, convertLegacyRouteRef, convertLegacyRouteRefs, } from '@backstage/core-compat-api'; @@ -33,9 +34,9 @@ const page = PageBlueprint.make({ path: '/notifications', routeRef: convertLegacyRouteRef(rootRouteRef), loader: () => - import('./components/NotificationsPage').then(m => ( - - )), + import('./components/NotificationsPage').then(m => + compatWrapper(), + ), }, }); diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 410b9da982..6cf5609da0 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-org-react +## 0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 0.1.42-next.1 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index f8410a3f77..06c4135396 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.42-next.1", + "version": "0.1.42-next.2", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index ee5def4fcf..9df2d654e3 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-org +## 0.6.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.6.44-next.1 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index df5b58446c..96a4ab9e62 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.44-next.1", + "version": "0.6.44-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index 9e99278912..2c396607d5 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.8.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.8.3-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 15dedf5ac8..2d28caf5c6 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.8.3-next.0", + "version": "0.8.3-next.1", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 5fbb8edcc2..305afa351c 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend +## 2.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + ## 2.2.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 6b2f9ef0cb..2e77b90177 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "2.2.1-next.0", + "version": "2.2.1-next.1", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml index abd18b1d42..56a7e7f5c6 100644 --- a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml @@ -32,7 +32,7 @@ spec: title: Title type: string description: Notification title - description: + info: title: Description type: string description: Notification longer description @@ -67,7 +67,7 @@ spec: recipients: ${{ parameters.recipients }} entityRefs: ${{ parameters.entityRefs }} title: ${{ parameters.title }} - description: ${{ parameters.description }} + info: ${{ parameters.info }} link: ${{ parameters.link }} severity: ${{ parameters.severity }} topic: ${{ parameters.topic }} diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 467b590bed..4144c552fb 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -1385,34 +1385,24 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect.anything(), ); }); - it('disallows users from seeing tasks they do not own', async () => { - const { permissions, router, taskBroker } = await createTestRouter(); - jest - .spyOn(permissions, 'authorizeConditional') - .mockImplementationOnce(async () => [ - { - conditions: { - resourceType: 'scaffolder-task', - rule: 'IS_TASK_OWNER', - params: { createdBy: ['user'] }, - }, - pluginId: 'scaffolder', - resourceType: 'scaffolder-task', - result: AuthorizeResult.CONDITIONAL, + it('allows payloads up to 10MB', async () => { + const { unwrappedRouter } = await createTestRouter(); + const mockToken = mockCredentials.user.token(); + const mockTemplate = generateMockTemplate(); + + const response = await request(unwrappedRouter) + .post('/v2/dry-run') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + template: mockTemplate, + values: { + requiredParameter1: 'A'.repeat(9 * 1024 * 1024), // ~9MB + requiredParameter2: 'required-value-2', }, - ]); - const response = await request(router).get( - `/v2/tasks?createdBy=not-user`, - ); - expect(taskBroker.list).toHaveBeenCalledWith({ - filters: { createdBy: ['not-user'], status: undefined }, - order: undefined, - pagination: { limit: undefined, offset: undefined }, - permissionFilters: { key: 'created_by', values: ['user'] }, - }); + directoryContents: [], + }); + expect(response.status).toBe(200); - expect(response.body.totalTasks).toBe(0); - expect(response.body.tasks).toEqual([]); }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9315e8ccb2..8296a6003f 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -186,9 +186,12 @@ const readDuration = ( export async function createRouter( options: RouterOptions, ): Promise { - const router = await createOpenApiRouter(); - // Be generous in upload size to support a wide range of templates in dry-run mode. - router.use(express.json({ limit: '10MB' })); + const router = await createOpenApiRouter({ + middleware: [ + // Be generous in upload size to support a wide range of templates in dry-run mode. + express.json({ limit: '10MB' }), + ], + }); const { logger: parentLogger, diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 78d40108f4..00c948a51b 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-react +## 1.19.1-next.2 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 1.19.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 1d8737db72..25c8ea0ec6 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.19.1-next.1", + "version": "1.19.1-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx index a0c4ce94cd..b88d96510e 100644 --- a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx +++ b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx @@ -21,6 +21,7 @@ const useStyles = makeStyles({ width: '100%', height: '100%', position: 'relative', + minHeight: 240, }, }); diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index bbb826965c..420938d6fd 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder +## 1.34.1-next.2 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.34.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index bad13ef8d2..695afd2244 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.34.1-next.1", + "version": "1.34.1-next.2", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", @@ -72,6 +72,8 @@ "@backstage/plugin-permission-react": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", + "@backstage/plugin-techdocs-common": "workspace:^", + "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/types": "workspace:^", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.1.0", @@ -109,6 +111,7 @@ "@backstage/dev-utils": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-techdocs": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.0.0", diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx index a16474a4cf..da544cb2c0 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx @@ -27,13 +27,19 @@ import { TestApiProvider, mockApis, } from '@backstage/test-utils'; -import { rootRouteRef } from '../../../routes'; +import { rootRouteRef, viewTechDocRouteRef } from '../../../routes'; import { TemplateListPage } from './TemplateListPage'; +import { + TECHDOCS_ANNOTATION, + TECHDOCS_EXTERNAL_ANNOTATION, + TECHDOCS_EXTERNAL_PATH_ANNOTATION, +} from '@backstage/plugin-techdocs-common'; const mountedRoutes = { mountedRoutes: { '/': rootRouteRef, '/catalog/:namespace/:kind/:name': entityRouteRef, + '/docs/:namespace/:kind/:name': viewTechDocRouteRef, }, }; @@ -51,6 +57,127 @@ describe('TemplateListPage', () => { ], }); + describe('TechDocs link rendering', () => { + it('shows TechDocs link when template has backstage.io/techdocs-ref', async () => { + const mockCatalogApiWithDocs = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-a', + annotations: { [TECHDOCS_ANNOTATION]: 'dir:.' }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + expect(await findByText('View TechDocs')).toBeInTheDocument(); + }); + + it('shows TechDocs link when template has backstage.io/techdocs-entity', async () => { + const mockCatalogApiWithExternal = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-b', + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other', + }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + expect(await findByText('View TechDocs')).toBeInTheDocument(); + }); + + it('appends path when backstage.io/techdocs-entity-path is set', async () => { + const mockCatalogApiWithPath = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-c', + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other', + [TECHDOCS_EXTERNAL_PATH_ANNOTATION]: '/guides/start', + }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + const link = (await findByText('View TechDocs')).closest('a')!; + expect(link).toHaveAttribute( + 'href', + expect.stringMatching( + /\/docs\/default\/component\/other\/?(index\.html)?#?\/guides\/start|\/docs\/default\/component\/other\/guides\/start/, + ), + ); + }); + }); + it('should render the search bar for templates', async () => { const { getByPlaceholderText } = await renderInTestApp( { const additionalLinksForEntity = useCallback( (template: TemplateEntityV1beta3) => { - const { kind, namespace, name } = parseEntityRef( - stringifyEntityRef(template), - ); - return template.metadata.annotations?.['backstage.io/techdocs-ref'] && - viewTechDocsLink + if ( + !( + template.metadata.annotations?.[TECHDOCS_ANNOTATION] || + template.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) || + !viewTechDocsLink + ) { + return []; + } + + const url = buildTechDocsURL(template, viewTechDocsLink); + return url ? [ { icon: app.getSystemIcon('docs') ?? DocsIcon, text: t( 'templateListPage.additionalLinksForEntity.viewTechDocsTitle', ), - url: viewTechDocsLink({ kind, namespace, name }), + url, }, ] : []; diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index e9a56fa5f9..8f60690872 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-catalog +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.3.8-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 9bcc2287ff..0ebd03caa6 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.3.8-next.0", + "version": "0.3.8-next.1", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 1938883b9e..ead9c04a7b 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.4.6-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index 1faffa2b90..d1b784a4d8 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.4.6-next.0", + "version": "0.4.6-next.1", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 8449247680..8e9ca88787 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search +## 1.4.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.4.30-next.1 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 7600d61b86..b58993b65c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.30-next.1", + "version": "1.4.30-next.2", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index a6b33f9837..714fd56c8f 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.53-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + ## 1.0.53-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index eebe45a88a..94fd865e02 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.53-next.1", + "version": "1.0.53-next.2", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx index 91b85e7885..a7cb74cb93 100644 --- a/plugins/techdocs-addons-test-utils/src/test-utils.tsx +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -39,10 +39,12 @@ import { } from '@backstage/plugin-techdocs-react'; import { TechDocsReaderPage, techdocsPlugin } from '@backstage/plugin-techdocs'; import { + catalogApiRef, EntityPresentationApi, entityPresentationApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { searchApiRef } from '@backstage/plugin-search-react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; @@ -235,8 +237,19 @@ export class TechDocsAddonTester { }), }; + const catalogApi = catalogApiMock({ + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { namespace: 'default', name: 'docs' }, + }, + ], + }); + const apis: TechdocsAddonTesterApis = [ [fetchApiRef, fetchApi], + [catalogApiRef, catalogApi], [entityPresentationApiRef, entityPresentationApi], [discoveryApiRef, discoveryApi], [techdocsApiRef, techdocsApi], diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 036ba4387c..98b42c0b84 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-backend +## 2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + ## 2.1.0-next.0 ### Minor Changes diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index 6b30213a60..5b90693e23 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -10,3 +10,17 @@ spec: type: service lifecycle: experimental owner: user:guest +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: techdocs-entity-documented-component + title: Example Entity Documented By TechDocs Entity Annotation + description: A Service with TechDocs documentation via the `backstage.io/techdocs-entity` annotation. + annotations: + backstage.io/techdocs-entity: component:default/documented-component + backstage.io/techdocs-entity-path: /inner-component-docs +spec: + type: service + lifecycle: experimental + owner: user:guest diff --git a/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md new file mode 100644 index 0000000000..8cb6498c55 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md @@ -0,0 +1,5 @@ +# Inner Component Docs + +This is a basic example of documentation within a larger suite of TechDocs that can be referenced by whatever entities necessary. It is intended as a showcase of the `backstage.io/techdocs-entity-path` annotation for linking to a "subpage" in TechDocs that are declared by another entity. + +Please review the [How-To Guides - Deep Linking Into TechDocs](../../../../../../docs/features/techdocs/how-to-guides.md#deep-linking-into-techdocs) section for more information and you can view the example usage on the "Example Entity Documented By TechDocs Entity Annotation" component in this [catalog-info.yaml](../../catalog-info.yaml) file. diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index 1a159a4d12..fd954225a3 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -7,6 +7,7 @@ nav: - Subpage: sub-page.md - 'Code Sample': code/code-sample.md - Extensions: extensions.md + - 'Inner Component Docs': inner-component-docs/index.md plugins: - techdocs-core diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index bc6bfd6175..fd1ed4cadf 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "2.1.0-next.0", + "version": "2.1.0-next.1", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs-react/src/helpers.ts b/plugins/techdocs-react/src/helpers.ts index e6daf99b26..aebb0b3e52 100644 --- a/plugins/techdocs-react/src/helpers.ts +++ b/plugins/techdocs-react/src/helpers.ts @@ -64,7 +64,7 @@ export function getEntityRootTechDocsPath(entity: Entity): string { /** * Build the TechDocs URL for the given entity. This helper should be used anywhere there - * is a link to an entities TechDocs. + * is a link to an entity's TechDocs. * * @public */ @@ -104,7 +104,7 @@ export const buildTechDocsURL = ( }); // Add on the external entity path to the url if one exists. This allows deep linking into another - // entities TechDocs. + // entity's TechDocs. const path = getEntityRootTechDocsPath(entity); return `${url}${path}`; diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index c4f6ed3cef..02593351ee 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs +## 1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.14.2-next.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 7ecec3cd0a..7777133862 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.14.2-next.1", + "version": "1.14.2-next.2", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", @@ -81,7 +81,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/styles": "^4.10.0", "@microsoft/fetch-event-source": "^2.0.1", - "dompurify": "^3.0.0", + "dompurify": "^3.2.4", "git-url-parse": "^15.0.0", "jss": "~10.10.0", "lodash": "^4.17.21", diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 2af4915ef5..aea45a60fa 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -18,6 +18,7 @@ import { ReactNode } from 'react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { + catalogApiRef, entityPresentationApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; @@ -26,13 +27,15 @@ import { renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; +import { catalogApiMock as catalogApiMockFactory } from '@backstage/plugin-catalog-react/testUtils'; import { techdocsApiRef, techdocsStorageApiRef } from '../../../api'; import { rootRouteRef, rootDocsRouteRef } from '../../../routes'; +import { TECHDOCS_EXTERNAL_ANNOTATION } from '@backstage/plugin-techdocs-common'; import { TechDocsReaderPage } from './TechDocsReaderPage'; -import { Route, useParams } from 'react-router-dom'; +import { Route, useNavigate, useParams } from 'react-router-dom'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { FlatRoutes } from '@backstage/core-app-api'; @@ -94,6 +97,8 @@ const entityPresentationApiMock: jest.Mocked< }), }; +const catalogApiMock = catalogApiMockFactory.mock(); + const fetchApiMock = { fetch: jest.fn().mockResolvedValue({ ok: true, @@ -114,6 +119,11 @@ jest.mock('@backstage/core-components', () => ({ Page: jest.fn(), })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), +})); + const configApi = mockApis.config({ data: { app: { baseUrl: 'http://localhost:3000' } }, }); @@ -129,6 +139,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => { [techdocsApiRef, techdocsApiMock], [techdocsStorageApiRef, techdocsStorageApiMock], [entityPresentationApiRef, entityPresentationApiMock], + [catalogApiRef, catalogApiMock], ]} > {children} @@ -143,6 +154,8 @@ const mountedRoutes = { }; describe('', () => { + const mockNavigate = jest.fn(); + beforeEach(() => { getEntityMetadata.mockResolvedValue(mockEntityMetadata); getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); @@ -150,6 +163,8 @@ describe('', () => { // Expires in 10 minutes expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(), }); + + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); }); afterEach(() => { @@ -285,4 +300,142 @@ describe('', () => { expect(text).toHaveStyle('fontFamily: Comic Sans MS'); }); + + describe('external TechDocs redirect', () => { + beforeEach(() => { + mockNavigate.mockClear(); + catalogApiMock.getEntityByRef.mockReset(); + catalogApiMock.getEntityByRef.mockResolvedValue(mockEntityMetadata); + }); + + it('should navigate to external URL when entity has external techdocs annotation', async () => { + const mockEntityWithExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: + 'component:external-namespace/external-docs', + }, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithExternalAnnotation, + ); + + await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(mockNavigate).toHaveBeenCalledWith( + '/docs/external-namespace/component/external-docs', + { replace: true }, + ); + }); + + it('should render normally when entity has no external techdocs annotation', async () => { + const mockEntityWithoutExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: undefined, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithoutExternalAnnotation, + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should render normally when entity has external annotation but no value', async () => { + const mockEntityWithEmptyExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: '', + }, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithEmptyExternalAnnotation, + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should render normally when catalog API throws an error', async () => { + catalogApiMock.getEntityByRef.mockRejectedValue( + new Error('Catalog API error'), + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index 80545151d9..c67660fd79 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -14,19 +14,26 @@ * limitations under the License. */ -import { Children, ReactElement, ReactNode } from 'react'; -import { useOutlet } from 'react-router-dom'; - +import { + Children, + ReactElement, + ReactNode, + useEffect, + useMemo, + useCallback, +} from 'react'; +import { useOutlet, useNavigate } from 'react-router-dom'; import { Page } from '@backstage/core-components'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { TECHDOCS_ADDONS_KEY, TECHDOCS_ADDONS_WRAPPER_KEY, TechDocsReaderPageProvider, + buildTechDocsURL, } from '@backstage/plugin-techdocs-react'; - +import { TECHDOCS_EXTERNAL_ANNOTATION } from '@backstage/plugin-techdocs-common'; +import useAsync from 'react-use/esm/useAsync'; import { TechDocsReaderPageRenderFunction } from '../../../types'; - import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent'; import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader'; import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader'; @@ -34,9 +41,11 @@ import { rootDocsRouteRef } from '../../../routes'; import { getComponentData, useRouteRefParams, + useApi, + useRouteRef, } from '@backstage/core-plugin-api'; - import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { createTheme, styled, @@ -44,6 +53,7 @@ import { ThemeProvider, useTheme, } from '@material-ui/core/styles'; +import { Progress } from '@backstage/core-components'; /* An explanation for the multiple ways of customizing the TechDocs reader page @@ -177,44 +187,106 @@ const StyledPage = styled(Page)({ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { const currentTheme = useTheme(); - const readerPageTheme = createTheme({ - ...currentTheme, - ...(props.overrideThemeOptions || {}), - }); + const readerPageTheme = useMemo( + () => + createTheme({ + ...currentTheme, + ...(props.overrideThemeOptions || {}), + }), + [currentTheme, props.overrideThemeOptions], + ); + const { kind, name, namespace } = useRouteRefParams(rootDocsRouteRef); const { children, entityRef = { kind, name, namespace } } = props; const outlet = useOutlet(); - if (!children) { + const catalogApi = useApi(catalogApiRef); + const navigate = useNavigate(); + const viewTechdocLink = useRouteRef(rootDocsRouteRef); + + const memoizedEntityRef = useMemo( + () => ({ + kind: entityRef.kind, + name: entityRef.name, + namespace: entityRef.namespace, + }), + [entityRef.kind, entityRef.name, entityRef.namespace], + ); + + const externalEntityTechDocsUrl = useAsync(async () => { + try { + const catalogEntity = await catalogApi.getEntityByRef(memoizedEntityRef); + + if ( + catalogEntity?.metadata?.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) { + return buildTechDocsURL(catalogEntity, viewTechdocLink); + } + } catch (error) { + // Ignore error and allow an attempt at loading the current entity's TechDocs when unable to fetch an external entity from the catalog. + } + + return undefined; + }, [memoizedEntityRef, catalogApi, viewTechdocLink]); + + const handleNavigation = useCallback( + (url: string) => { + navigate(url, { replace: true }); + }, + [navigate], + ); + + useEffect(() => { + if (!externalEntityTechDocsUrl.loading && externalEntityTechDocsUrl.value) { + handleNavigation(externalEntityTechDocsUrl.value); + } + }, [ + externalEntityTechDocsUrl.loading, + externalEntityTechDocsUrl.value, + handleNavigation, + ]); + + const page: ReactNode = useMemo(() => { + if (children) { + return null; + } + const childrenList = outlet ? Children.toArray(outlet.props.children) : []; const grandChildren = childrenList.flatMap( child => (child as ReactElement)?.props?.children ?? [], ); - const page: ReactNode = grandChildren.find( + return grandChildren.find( grandChild => !getComponentData(grandChild, TECHDOCS_ADDONS_WRAPPER_KEY) && !getComponentData(grandChild, TECHDOCS_ADDONS_KEY), ); + }, [children, outlet]); - // As explained above, "page" is configuration 4 and is 1 + if (externalEntityTechDocsUrl.loading || externalEntityTechDocsUrl.value) { + return ; + } + + // As explained above, "page" is configuration 4 and is 1 + if (!children) { return ( - + {(page as JSX.Element) || } ); } + // As explained above, a render function is configuration 3 and React element is 2 return ( - + {({ metadata, entityMetadata, onReady }) => ( { > {children instanceof Function ? children({ - entityRef, + entityRef: memoizedEntityRef, techdocsMetadataValue: metadata.value, entityMetadataValue: entityMetadata.value, onReady, diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx index 6f6ee81c8b..3c10b26f7f 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx @@ -52,13 +52,11 @@ const mockTechDocsMetadata = { site_description: 'test-site-desc', }; -const mockUseParams = jest.fn(); -mockUseParams.mockReturnValue({ '*': 'foo/bar/baz/' }); - +let useParamsPath = '/'; jest.mock('react-router-dom', () => { return { ...(jest.requireActual('react-router-dom') as any), - useParams: () => mockUseParams(), + useParams: () => ({ '*': useParamsPath }), }; }); @@ -189,6 +187,7 @@ describe('', () => { getEntityMetadata.mockResolvedValue(mockEntityMetadata); getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + useParamsPath = 'foo/bar/baz/'; await renderInTestApp( @@ -203,7 +202,31 @@ describe('', () => { await waitFor(() => { expect(document.title).toEqual( - 'Backstage | Test Entity | Foo | Bar | Baz', + 'Test Entity | Foo | Bar | Baz | Backstage', + ); + }); + }); + + it('The header title is abbreviated if path is too long', async () => { + getEntityMetadata.mockResolvedValue(mockEntityMetadata); + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + + useParamsPath = 'foo/bar/baz/qux/quux/'; + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + '/docs': rootRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(document.title).toEqual( + 'Test Entity | Foo | Bar | Baz | Qux | Quux | Backstage', ); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx index bafcdd270c..6328becc73 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx @@ -172,17 +172,16 @@ export const TechDocsReaderPageHeader = ( const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); const normalizeAndSpace = (str: string) => - str.replace(/-/g, ' ').split(' ').map(capitalize).join(' '); + str.replace(/[-_]/g, ' ').split(' ').map(capitalize).join(' '); let techdocsTabTitleItems: string[] = []; if (path !== '') techdocsTabTitleItems = removeTrailingSlash(path) .split('/') - .slice(0, 3) .map(normalizeAndSpace); - const tabTitleItems = [appTitle, entityDisplayName, ...techdocsTabTitleItems]; + const tabTitleItems = [entityDisplayName, ...techdocsTabTitleItems, appTitle]; const tabTitle = tabTitleItems.join(' | '); return ( diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts b/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts new file mode 100644 index 0000000000..ae0b4e2ce9 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts @@ -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 { UponSanitizeAttributeHook } from 'dompurify'; + +/** + * Removes attributes that should only be present on meta tags from other elements. + * This ensures that http-equiv and content attributes are only allowed on meta tags + * where they are required for the redirect feature. + */ +export const removeRestrictedAttributes: UponSanitizeAttributeHook = ( + node, + data, +) => { + if (node.tagName !== 'META') { + if (data.attrName === 'http-equiv' || data.attrName === 'content') { + node.removeAttribute(data.attrName); + } + } +}; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts b/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts index 25259dbc43..23711b52b7 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { isElement } from '../utils'; + /** * Checks whether a node is iframe or not. * @param node - can be any element. @@ -42,9 +44,10 @@ const isSafe = (node: Element, hosts: string[]) => { * @param node - can be any element. * @param hosts - list of allowed hosts. */ -export const removeUnsafeIframes = (hosts: string[]) => (node: Element) => { +export const removeUnsafeIframes = (hosts: string[]) => (node: Node) => { + if (!isElement(node)) return; + if (isIframe(node) && !isSafe(node, hosts)) { node.remove(); } - return node; }; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/index.ts b/plugins/techdocs/src/reader/transformers/html/hooks/index.ts index a4356db2e9..ce237ec6d2 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/index.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/index.ts @@ -16,3 +16,5 @@ export { removeUnsafeLinks } from './links'; export { removeUnsafeIframes } from './iframes'; +export { removeUnsafeMetaTags } from './metatags'; +export { removeRestrictedAttributes } from './attributes'; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/links.ts b/plugins/techdocs/src/reader/transformers/html/hooks/links.ts index 38f775cfbe..eb2cbfc6de 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/links.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/links.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { isElement } from '../utils'; + const MKDOCS_CSS = /main\.[A-Fa-f0-9]{8}\.min\.css$/; const GOOGLE_FONTS = /^https:\/\/fonts\.googleapis\.com/; const GSTATIC_FONTS = /^https:\/\/fonts\.gstatic\.com/; @@ -41,11 +43,11 @@ const isSafe = (node: Element) => { /** * Function that removes unsafe link nodes. * @param node - can be any element. - * @param hosts - list of allowed hosts. */ -export const removeUnsafeLinks = (node: Element) => { +export const removeUnsafeLinks = (node: Node) => { + if (!isElement(node)) return; + if (isLink(node) && !isSafe(node)) { node.remove(); } - return node; }; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts b/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts new file mode 100644 index 0000000000..a207a5bca2 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UponSanitizeElementHook } from 'dompurify'; +import { isElement } from '../utils'; + +/** + * Checks if a meta tag is a refresh redirect tag that should be allowed. + * These tags are required for the TechDocs redirect feature. + */ +const isAllowedMetaRefreshTag = (element: Element): boolean => { + const httpEquiv = element.getAttribute('http-equiv'); + const content = element.getAttribute('content'); + + return httpEquiv === 'refresh' && content?.includes('url=') === true; +}; + +/** + * Removes unsafe meta tags from the DOM while preserving allowed refresh redirect tags. + * Only meta tags used for page refreshing/redirects are allowed as they are required + * for the TechDocs redirect feature. + */ +export const removeUnsafeMetaTags: UponSanitizeElementHook = (node, data) => { + if (!isElement(node)) return; + + if (data.tagName === 'meta' && !isAllowedMetaRefreshTag(node)) { + node.parentNode?.removeChild(node); + } +}; diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 7c341c783e..6ebf53f26e 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -20,7 +20,12 @@ import { useCallback, useMemo } from 'react'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { Transformer } from '../transformer'; -import { removeUnsafeIframes, removeUnsafeLinks } from './hooks'; +import { + removeRestrictedAttributes, + removeUnsafeIframes, + removeUnsafeLinks, + removeUnsafeMetaTags, +} from './hooks'; /** * Returns html sanitizer configuration @@ -51,26 +56,9 @@ export const useSanitizerTransformer = (): Transformer => { DOMPurify.addHook('beforeSanitizeElements', removeUnsafeIframes(hosts)); } - // Only allow meta tags if they are used for refreshing the page. They are required for the redirect feature. - DOMPurify.addHook('uponSanitizeElement', (currNode, data) => { - if (data.tagName === 'meta') { - const isMetaRefreshTag = - currNode.getAttribute('http-equiv') === 'refresh' && - currNode.getAttribute('content')?.includes('url='); - if (!isMetaRefreshTag) { - currNode.parentNode?.removeChild(currNode); - } - } - }); + DOMPurify.addHook('uponSanitizeElement', removeUnsafeMetaTags); - // Only allow http-equiv and content attributes on meta tags. They are required for the redirect feature. - DOMPurify.addHook('uponSanitizeAttribute', (currNode, data) => { - if (currNode.tagName !== 'meta') { - if (data.attrName === 'http-equiv' || data.attrName === 'content') { - currNode.removeAttribute(data.attrName); - } - } - }); + DOMPurify.addHook('uponSanitizeAttribute', removeRestrictedAttributes); const tagNameCheck = config?.getOptionalString( 'allowedCustomElementTagNameRegExp', @@ -122,7 +110,7 @@ export const useSanitizerTransformer = (): Transformer => { ? new RegExp(attributeNameCheck) : undefined, }, - }); + }) as Element; }, [config], ); diff --git a/plugins/techdocs/src/reader/transformers/html/utils.ts b/plugins/techdocs/src/reader/transformers/html/utils.ts new file mode 100644 index 0000000000..b97b63743c --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/utils.ts @@ -0,0 +1,19 @@ +/* + * 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 isElement = (node: Node): node is Element => { + return node.nodeType === Node.ELEMENT_NODE; +}; diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 5d4f80079f..cfb4c0b712 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-user-settings +## 0.8.26-next.2 + +### Patch Changes + +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.8.26-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index d5224ac8b6..3ce96f9146 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.26-next.1", + "version": "0.8.26-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index 5a28aacc02..1c8e844d05 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -123,7 +123,7 @@ export const userSettingsTranslationRef: TranslationRef< readonly 'languageToggle.select': 'Select language {{language}}'; readonly 'languageToggle.title': 'Language'; readonly 'languageToggle.description': 'Change the language'; - readonly 'themeToggle.select': 'Select theme {{theme}}'; + readonly 'themeToggle.select': 'Select {{theme}}'; readonly 'themeToggle.title': 'Theme'; readonly 'themeToggle.description': 'Change the theme mode'; readonly 'themeToggle.names.auto': 'Auto'; diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index b0ddbf31cf..ce9c1093ca 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -26,6 +26,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, oneloginAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { userSettingsTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; @@ -128,6 +129,14 @@ export const DefaultProviderSettings = (props: { icon={Star} /> )} + {configuredProviders.includes('openshift') && ( + + )} ); }; diff --git a/plugins/user-settings/src/translation.ts b/plugins/user-settings/src/translation.ts index 0161508d14..7445a415f2 100644 --- a/plugins/user-settings/src/translation.ts +++ b/plugins/user-settings/src/translation.ts @@ -28,7 +28,7 @@ export const userSettingsTranslationRef = createTranslationRef({ themeToggle: { title: 'Theme', description: 'Change the theme mode', - select: 'Select theme {{theme}}', + select: 'Select {{theme}}', selectAuto: 'Select Auto Theme', names: { light: 'Light', diff --git a/yarn.lock b/yarn.lock index fde37ae4e3..462290b101 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,6 +3182,7 @@ __metadata: "@types/d3-selection": "npm:^3.0.1" "@types/d3-shape": "npm:^3.0.1" "@types/d3-zoom": "npm:^3.0.1" + "@types/fscreen": "npm:^1" "@types/google-protobuf": "npm:^3.7.2" "@types/react": "npm:^18.0.0" "@types/react-helmet": "npm:^6.1.0" @@ -3207,6 +3208,7 @@ __metadata: rc-progress: "npm:3.5.1" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" + react-full-screen: "npm:^1.1.1" react-helmet: "npm:6.1.0" react-hook-form: "npm:^7.12.2" react-idle-timer: "npm:5.7.2" @@ -4120,6 +4122,27 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-openshift-provider@workspace:^, @backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider" + dependencies: + "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" + express: "npm:^4.18.2" + msw: "npm:^2.7.3" + passport-oauth2: "npm:^1.8.0" + supertest: "npm:^7.1.0" + zod: "npm:^3.24.2" + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider" @@ -4196,6 +4219,7 @@ __metadata: knex: "npm:^3.0.0" lodash: "npm:^4.17.21" luxon: "npm:^3.0.0" + matcher: "npm:^4.0.0" minimatch: "npm:^9.0.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" @@ -4263,6 +4287,41 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth@workspace:^, @backstage/plugin-auth@workspace:plugins/auth": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth@workspace:plugins/auth" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/frontend-defaults": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": "npm:^4.12.2" + "@material-ui/icons": "npm:^4.9.1" + "@material-ui/lab": "npm:4.0.0-alpha.61" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^16.0.0" + "@testing-library/user-event": "npm:^14.0.0" + "@types/react": "npm:^18.0.0" + msw: "npm:^1.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.3.0" + react-use: "npm:^17.2.4" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@backstage/plugin-bitbucket-cloud-common@workspace:^, @backstage/plugin-bitbucket-cloud-common@workspace:plugins/bitbucket-cloud-common": version: 0.0.0-use.local resolution: "@backstage/plugin-bitbucket-cloud-common@workspace:plugins/bitbucket-cloud-common" @@ -6576,6 +6635,9 @@ __metadata: "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" + "@backstage/plugin-techdocs": "workspace:^" + "@backstage/plugin-techdocs-common": "workspace:^" + "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@codemirror/language": "npm:^6.0.0" @@ -7229,7 +7291,7 @@ __metadata: "@testing-library/user-event": "npm:^14.0.0" "@types/dompurify": "npm:^3.0.0" "@types/react": "npm:^18.0.0" - dompurify: "npm:^3.0.0" + dompurify: "npm:^3.2.4" git-url-parse: "npm:^15.0.0" jss: "npm:~10.10.0" lodash: "npm:^4.17.21" @@ -11083,9 +11145,9 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.39.1": - version: 0.39.2 - resolution: "@mswjs/interceptors@npm:0.39.2" +"@mswjs/interceptors@npm:^0.37.0": + version: 0.37.1 + resolution: "@mswjs/interceptors@npm:0.37.1" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" @@ -11093,7 +11155,7 @@ __metadata: is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10/faaa95d636363a197f125c32066457fa74d5063d8ccae4c9c0e0510179060d92b1faf8640df45a0623e0bf42a30d610c83364a58e0eb0ca412c87b2e835936c1 + checksum: 10/332d8aa50beb4834ccbda6a800ca00b1204adc0eba23e1c1f7bb9f4e564a92707e563f7a2424d4a8607404ec91424e5d8c34a87c250b191ca7b24dff12eba2c5 languageName: node linkType: hard @@ -20270,6 +20332,13 @@ __metadata: languageName: node linkType: hard +"@types/fscreen@npm:^1": + version: 1.0.4 + resolution: "@types/fscreen@npm:1.0.4" + checksum: 10/78459a457ce7a6b7d72a5f17fdb54bbeb93c58ab77fd2858aac610fed2435bc4be9e5d2fb9883b6669b7f3a1204115cc2be59a027ab937ee8b5186225d2ea53d + languageName: node + linkType: hard + "@types/git-url-parse@npm:^9.0.0": version: 9.0.3 resolution: "@types/git-url-parse@npm:9.0.3" @@ -21470,10 +21539,10 @@ __metadata: languageName: node linkType: hard -"@types/trusted-types@npm:*": - version: 2.0.3 - resolution: "@types/trusted-types@npm:2.0.3" - checksum: 10/4794804bc4a4a173d589841b6d26cf455ff5dc4f3e704e847de7d65d215f2e7043d8757e4741ce3a823af3f08260a8d04a1a6e9c5ec9b20b7b04586956a6b005 +"@types/trusted-types@npm:*, @types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10/8e4202766a65877efcf5d5a41b7dd458480b36195e580a3b1085ad21e948bc417d55d6f8af1fd2a7ad008015d4117d5fdfe432731157da3c68678487174e4ba3 languageName: node linkType: hard @@ -26267,7 +26336,7 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8": +"combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" dependencies: @@ -26395,10 +26464,10 @@ __metadata: languageName: node linkType: hard -"component-emitter@npm:^1.3.1": - version: 1.3.1 - resolution: "component-emitter@npm:1.3.1" - checksum: 10/94550aa462c7bd5a61c1bc480e28554aa306066930152d1b1844a0dd3845d4e5db7e261ddec62ae184913b3e59b55a2ad84093b9d3596a8f17c341514d6c483d +"component-emitter@npm:^1.3.0": + version: 1.3.0 + resolution: "component-emitter@npm:1.3.0" + checksum: 10/dfc1ec2e7aa2486346c068f8d764e3eefe2e1ca0b24f57506cd93b2ae3d67829a7ebd7cc16e2bf51368fac2f45f78fcff231718e40b1975647e4a86be65e1d05 languageName: node linkType: hard @@ -27622,15 +27691,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe + checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 languageName: node linkType: hard @@ -28386,10 +28455,15 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:^3.0.0, dompurify@npm:^3.1.7": - version: 3.1.7 - resolution: "dompurify@npm:3.1.7" - checksum: 10/dc637a064306f83cf911caa267ffe1f973552047602020e3b6723c90f67962813edf8a65a0b62e8c9bc13fcd173a2691212a3719bc116226967f46bcd6181277 +"dompurify@npm:^3.1.7, dompurify@npm:^3.2.4": + version: 3.2.6 + resolution: "dompurify@npm:3.2.6" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10/b91631ed0e4d17fae950ef53613cc009ed7e73adc43ac94a41dd52f35483f7538d13caebdafa7626e0da145fc8184e7ac7935f14f25b7e841b32fda777e40447 languageName: node linkType: hard @@ -29917,6 +29991,7 @@ __metadata: "@backstage/plugin-api-docs": "workspace:^" "@backstage/plugin-app": "workspace:^" "@backstage/plugin-app-visualizer": "workspace:^" + "@backstage/plugin-auth": "workspace:^" "@backstage/plugin-auth-react": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" @@ -30051,6 +30126,7 @@ __metadata: "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^" @@ -31056,13 +31132,16 @@ __metadata: linkType: hard "form-data@npm:^2.3.2, form-data@npm:^2.5.0": - version: 2.5.1 - resolution: "form-data@npm:2.5.1" + version: 2.5.5 + resolution: "form-data@npm:2.5.5" dependencies: asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.6" - mime-types: "npm:^2.1.12" - checksum: 10/2e2e5e927979ba3623f9b4c4bcc939275fae3f2dea9dafc8db3ca656a3d75476605de2c80f0e6f1487987398e056f0b4c738972d6e1edd83392d5686d0952eed + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.35" + safe-buffer: "npm:^5.2.1" + checksum: 10/4b6a8d07bb67089da41048e734215f68317a8e29dd5385a972bf5c458a023313c69d3b5d6b8baafbb7f808fa9881e0e2e030ffe61e096b3ddc894c516401271d languageName: node linkType: hard @@ -31105,7 +31184,7 @@ __metadata: languageName: node linkType: hard -"formidable@npm:^3.5.4": +"formidable@npm:^3.5.1": version: 3.5.4 resolution: "formidable@npm:3.5.4" dependencies: @@ -31309,6 +31388,13 @@ __metadata: languageName: node linkType: hard +"fscreen@npm:^1.0.2": + version: 1.2.0 + resolution: "fscreen@npm:1.2.0" + checksum: 10/ac50f9ac52a157b8fe6aaecdf9efa7c1cfa90b42a76c3bc6b85372fab05c5a9cd72c1b7f4c2e273eba1a0e630e381fd72ae135fcc57acd05a0943d5d0c21b451 + languageName: node + linkType: hard + "fsevents@npm:2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" @@ -37210,6 +37296,15 @@ __metadata: languageName: node linkType: hard +"matcher@npm:^4.0.0": + version: 4.0.0 + resolution: "matcher@npm:4.0.0" + dependencies: + escape-string-regexp: "npm:^4.0.0" + checksum: 10/d338aff31d8dfd3626873e43777f46b123579734d53bb8d18d64b08a822ba5e8d39f5fe2e23403258e6143aa0cbe20a15662720d825cd0d3af961d5a44230328 + languageName: node + linkType: hard + "material-ui-confirm@npm:^3.0.12": version: 3.0.18 resolution: "material-ui-confirm@npm:3.0.18" @@ -38635,15 +38730,15 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.0.0, msw@npm:^2.0.8": - version: 2.10.4 - resolution: "msw@npm:2.10.4" +"msw@npm:^2.0.0, msw@npm:^2.0.8, msw@npm:^2.7.3": + version: 2.7.3 + resolution: "msw@npm:2.7.3" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" "@bundled-es-modules/tough-cookie": "npm:^0.1.6" "@inquirer/confirm": "npm:^5.0.0" - "@mswjs/interceptors": "npm:^0.39.1" + "@mswjs/interceptors": "npm:^0.37.0" "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/until": "npm:^2.1.0" "@types/cookie": "npm:^0.6.0" @@ -38664,7 +38759,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/e2f25dda1aba66c7444c29c41d3157cb15c0332055ab7ebfb74ef4b506e7b90098cf37c577768edb5b2b2dbf0d6ed6a7a3ca8ee6da3d72df5a25823d82f33316 + checksum: 10/f193329a68fc22e477a6f8504aa44a92bd12847f2eeac1dfbd8ec1cc43ff293112ec067de1c7fe312ba02beecb313fb00aeeebf5817432b57af2d796b2dff2fa languageName: node linkType: hard @@ -40614,7 +40709,7 @@ __metadata: languageName: node linkType: hard -"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0": +"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0, passport-oauth2@npm:^1.8.0": version: 1.8.0 resolution: "passport-oauth2@npm:1.8.0" dependencies: @@ -42270,7 +42365,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4": +"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -42849,6 +42944,17 @@ __metadata: languageName: node linkType: hard +"react-full-screen@npm:^1.1.1": + version: 1.1.1 + resolution: "react-full-screen@npm:1.1.1" + dependencies: + fscreen: "npm:^1.0.2" + peerDependencies: + react: ">= 16.8.0" + checksum: 10/70ad927b9d6c485ac46b5bb4b1639ef9a860da28290b3a1c419c42b9c427d78b80e8dba403eb6451458af56838012c81d5e12ef05097395f154defc32fe06c34 + languageName: node + linkType: hard + "react-grid-layout@npm:1.3.4": version: 1.3.4 resolution: "react-grid-layout@npm:1.3.4" @@ -46509,30 +46615,30 @@ __metadata: languageName: node linkType: hard -"superagent@npm:^10.2.3": - version: 10.2.3 - resolution: "superagent@npm:10.2.3" +"superagent@npm:^9.0.1": + version: 9.0.2 + resolution: "superagent@npm:9.0.2" dependencies: - component-emitter: "npm:^1.3.1" + component-emitter: "npm:^1.3.0" cookiejar: "npm:^2.1.4" - debug: "npm:^4.3.7" + debug: "npm:^4.3.4" fast-safe-stringify: "npm:^2.1.1" - form-data: "npm:^4.0.4" - formidable: "npm:^3.5.4" + form-data: "npm:^4.0.0" + formidable: "npm:^3.5.1" methods: "npm:^1.1.2" mime: "npm:2.6.0" - qs: "npm:^6.11.2" - checksum: 10/377bf938e68927dd772169c5285be27872bf6e84fac01c52bcd9396bc5b348c9ded8f8be54649510ec09a67bc5096055847b37cb01b3bca0eb06ff1856170e35 + qs: "npm:^6.11.0" + checksum: 10/d3c0c9051ceec84d5b431eaa410ad81bcd53255cea57af1fc66d683a24c34f3ba4761b411072a9bf489a70e3d5b586a78a0e6f2eac6a561067e7d196ddab0907 languageName: node linkType: hard -"supertest@npm:^7.0.0": - version: 7.1.4 - resolution: "supertest@npm:7.1.4" +"supertest@npm:^7.0.0, supertest@npm:^7.1.0": + version: 7.1.0 + resolution: "supertest@npm:7.1.0" dependencies: methods: "npm:^1.1.2" - superagent: "npm:^10.2.3" - checksum: 10/ecb5d41f2b62b257dbdcabac245c32b8e8fb264fe2636dd85c2c883569d23dc14adc0a471abb84187cbdb49bc36ad870ad355b4a0b85973f510fd57fc229e6cc + superagent: "npm:^9.0.1" + checksum: 10/20069f739a44821dfa4f7f397b9086ef31a358366331138f97945eedb2e231796e7c55b032125d3bd12f9839f089fbb809893dbc0f98edc57e12333b9f42b726 languageName: node linkType: hard @@ -49862,6 +49968,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/release-manifests": "workspace:^" "@yarnpkg/builder": "npm:^4.2.1" "@yarnpkg/core": "npm:^4.4.1" @@ -50031,10 +50138,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.4, zod@npm:^3.23.8": - version: 3.25.76 - resolution: "zod@npm:3.25.76" - checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 +"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.2": + version: 3.25.67 + resolution: "zod@npm:3.25.67" + checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa languageName: node linkType: hard From d0baa442e873bfe028a7f0353f3336205f369596 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 12 Sep 2025 11:53:27 +0100 Subject: [PATCH 017/193] Add CSS variables + class name structure Signed-off-by: Charles de Dreuille --- .../css-classname-structure.png | Bin 0 -> 42743 bytes docs/conf/user-interface/index.md | 154 +++++++++++++++++- microsite/src/theme/customTheme.scss | 5 + 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 docs/assets/user-interface/css-classname-structure.png diff --git a/docs/assets/user-interface/css-classname-structure.png b/docs/assets/user-interface/css-classname-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..c241b182cc49d9044901d7e4c0dcbb30479bf006 GIT binary patch literal 42743 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 + All available CSS variables + +#### Base colors + +These colors are used for special purposes like ring, scrollbar, ... + +| Token Name | Description | +| -------------- | ----------------------------------------------------------------------- | +| `--bui-black` | Pure black color. This one should be the same in light and dark themes. | +| `--bui-white` | Pure white color. This one should be the same in light and dark themes. | +| `--bui-gray-1` | You can use these mostly for backgrounds colors. | +| `--bui-gray-2` | You can use these mostly for backgrounds colors. | +| `--bui-gray-3` | You can use these mostly for backgrounds colors. | +| `--bui-gray-4` | You can use these mostly for backgrounds colors. | +| `--bui-gray-5` | You can use these mostly for backgrounds colors. | +| `--bui-gray-6` | You can use these mostly for backgrounds colors. | +| `--bui-gray-7` | You can use these mostly for backgrounds colors. | +| `--bui-gray-8` | You can use these mostly for backgrounds colors. | + +#### Core background colors + +These colors are used for the background of your application. We are mostly using for now a single elevated background for panels. `--bui-bg` should mostly use as the main background color of your app. + +| Token Name | Description | +| ------------------------- | ------------------------------------------------ | +| `--bui-bg` | The background color of your Backstage instance. | +| `--bui-bg-surface-1` | Use for any panels or elevated surfaces. | +| `--bui-bg-surface-2` | Use for any panels or elevated surfaces. | +| `--bui-bg-solid` | Used for solid background colors. | +| `--bui-bg-solid-hover` | Used for solid background colors when hovered. | +| `--bui-bg-solid-pressed` | Used for solid background colors when pressed. | +| `--bui-bg-solid-disabled` | Used for solid background colors when disabled. | +| `--bui-bg-tint` | Used for tint background colors. | +| `--bui-bg-tint-hover` | Used for tint background colors when hovered. | +| `--bui-bg-tint-focus` | Used for tint background colors when active. | +| `--bui-bg-tint-disabled` | Used for tint background colors when disabled. | +| `--bui-bg-danger` | Used to show errors information. | +| `--bui-bg-warning` | Used to show warnings information. | +| `--bui-bg-success` | Used to show success information. | + +#### Foreground colors + +Foreground colours are meant to work in pair with a background colours. Typeically this would work for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors are prefixed with `fg` to make it easier to identify. + +| Token Name | Description | +| ------------------------ | ----------------------------------------------------------------- | +| `--bui-fg-primary` | It should be used on top of main background surfaces. | +| `--bui-fg-secondary` | It should be used on top of main background surfaces. | +| `--bui-fg-link` | It should be used on top of main background surfaces. | +| `--bui-fg-link-hover` | It should be used on top of main background surfaces. | +| `--bui-fg-disabled` | It should be used on top of main background surfaces. | +| `--bui-fg-solid` | It should be used on top of solid background colors. | +| `--bui-fg-tint` | It should be used on top of tint background colors. | +| `--bui-fg-tint-disabled` | It should be used on top of tint background colors when disabled. | +| `--bui-fg-danger` | It should be used on top of danger background colors. | +| `--bui-fg-warning` | It should be used on top of warning background colors. | +| `--bui-fg-success` | It should be used on top of success background colors. | + +#### Border colors + +These border colors are mostly meant to be used as borders on top of any components with low contrast to help as a separator with the different background colors. + +| Token Name | Description | +| ----------------------- | --------------------------------------------------- | +| `--bui-border` | It should be used on top of `--bui-bg-surface-1`. | +| `--bui-border-hover` | Used when the component is interactive and hovered. | +| `--bui-border-pressed` | Used when the component is interactive and hovered. | +| `--bui-border-disabled` | Used when the component is disabled. | +| `--bui-border-danger` | It should be used on top of `--bui-bg-danger`. | +| `--bui-border-warning` | It should be used on top of `--bui-bg-warning`. | +| `--bui-border-success` | It should be used on top of `--bui-bg-success`. | + +#### Special colors + +These colors are used for special purposes like ring, scrollbar, ... + +| Token Name | Description | +| ----------------------- | --------------------------------- | +| `--bui-ring` | The color of the ring. | +| `--bui-scrollbar` | The color of the scrollbar. | +| `--bui-scrollbar-thumb` | The color of the scrollbar thumb. | + +#### Font families + +We have two fonts that we use across Backstage UI. The first one is the sans-serif font that we use for the body of the application. The second one is the monospace font that we use for code blocks and tables. + +| Token Name | Description | +| -------------------- | ---------------------------------- | +| `--bui-font-regular` | The sans-serif font for the theme. | +| `--bui-font-mono` | The monospace font for the theme. | + +#### Font weights + +We have two font weights that we use across Backstage UI. Regular or Bold. + +| Token Name | Description | +| --------------------------- | -------------------------------------- | +| `--bui-font-weight-regular` | The regular font weight for the theme. | +| `--bui-font-weight-bold` | The bold font weight for the theme. | + +#### Spacing + +We built a spacing system based on a single value `--bui-space`. This value is used to calculate the spacing for all the components. By default if you would like to increase or decrease the spacing between your components you can do it simply by updating `--bui-space` and it will apply to all spacing values. + +`--bui-space` is not used directly in any components but serve as an easy way to calculate the other values. + +| Token Name | Description | +| ------------- | ----------------------------------------------------------------- | +| `--bui-space` | The base unit for the spacing system. Default value is `0.25rem.` | + +#### Radius + +We use a radius system to make sure that the components have a consistent look and feel. + +| Token Name | Description | +| ------------------- | --------------------------------------------------------- | +| `--bui-radius-1` | The radius of the component. Default value is `0.125rem`. | +| `--bui-radius-2` | The radius of the component. Default value is `0.25rem`. | +| `--bui-radius-3` | The radius of the component. Default value is `0.5rem`. | +| `--bui-radius-4` | The radius of the component. Default value is `0.75rem`. | +| `--bui-radius-5` | The radius of the component. Default value is `1rem`. | +| `--bui-radius-6` | The radius of the component. Default value is `1.25rem`. | +| `--bui-radius-full` | The radius of the component. Default value is `9999px`. | + + ### Component class names -### Custom font +All Backstage UI components come with a set of CSS classes that you can use to style them. To make it easier to identify the class name you can use, we use a specific structure for the class names. + +![classname-structure](../../assets/user-interface/css-classname-structure.png) + +Every component has a unique prefix `.bui-` followed by the component name. Component props are represented using the `data-` attribute. That way, class names are easily identifiable. ## Create a theme for MUI (Legacy) diff --git a/microsite/src/theme/customTheme.scss b/microsite/src/theme/customTheme.scss index 707acb4073..823327b28f 100644 --- a/microsite/src/theme/customTheme.scss +++ b/microsite/src/theme/customTheme.scss @@ -171,6 +171,11 @@ html[data-theme='light'] { border-left: 3px solid rgb(255, 255, 255, 0.5); } +table { + width: 100%; + display: table; +} + /* #endregion */ /* For the docusaurus-pushfeedback plugin */ From 1dfaeeebdf23e744c4b494831102a04fc6c6eb31 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 12 Sep 2025 11:58:41 +0100 Subject: [PATCH 018/193] Update mkdocs.yml Signed-off-by: Charles de Dreuille --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index eea5fc9c40..45fdd2c9d6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,7 +28,7 @@ nav: - Database: 'getting-started/config/database.md' - Authentication: 'getting-started/config/authentication.md' - Configuring App with plugins: 'getting-started/configure-app-with-plugins.md' - - Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md' + - Customize the look-and-feel of your App: 'conf/user-interface/index.md' - Customizing your Homepage: 'getting-started/homepage.md' - Deployment: - Deploying Backstage: 'deployment/index.md' From f5f1cece1712ffdf84f477405415db36354ab6ba Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 11:37:57 +0100 Subject: [PATCH 019/193] Update docs/conf/user-interface/icons.md Co-authored-by: Patrik Oldsberg Signed-off-by: Charles de Dreuille --- docs/conf/user-interface/icons.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf/user-interface/icons.md b/docs/conf/user-interface/icons.md index 44b8782430..26a6f11096 100644 --- a/docs/conf/user-interface/icons.md +++ b/docs/conf/user-interface/icons.md @@ -20,7 +20,7 @@ You can change the following [icons](https://github.com/backstage/backstage/blob ### Create React Component -In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `CustomIcons.tsx` file. +In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `customIcons.tsx` file. ```tsx title="customIcons.tsx" import { SvgIcon, SvgIconProps } from '@material-ui/core'; From 95c442a9bdc17e52f273eb6de03604abee6882dc Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 12:39:09 +0100 Subject: [PATCH 020/193] Docs fixes Signed-off-by: Charles de Dreuille --- docs/conf/user-interface/index.md | 2 +- microsite/docusaurus.config.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index d4ecbc6940..aeae086b59 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -38,7 +38,7 @@ We recognize that maintaining two separate theming systems is not ideal. Because ## Creating custom themes -During the transition to Backstage UI, you will need to maintain themes in two places: some components and plugins still rely on MUI, while others use Backstage UI. At this time, there is no automated solution to generate MUI themes from your Backstage UI theme, so you will need to manage both separately until the migration is complete. +During the transition to Backstage UI, you will need to maintain themes in two places: some components and plugins still rely on MUI, while others use Backstage UI. We are working on a plugin that will make help you convert your existing MUI theme into a Backstage UI CSS file you can add to your application. We'll update this page when the plugin is available but for now you can follow progress on this PR [#31140](https://github.com/backstage/backstage/pull/31140). ```tsx title="packages/app/src/App.tsx" /* highlight-add-start */ diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index d7538cd12d..83f2f75689 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -269,6 +269,10 @@ const config: Config = { from: '/docs/plugins/url-reader/', to: '/docs/backend-system/core-services/url-reader', }, + { + from: '/docs/getting-started/app-custom-theme', + to: '/docs/conf/user-interface', + } ], }), [ From 8495b18507b3dca141abacd543db6ca6ce7b25fe Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Thu, 20 Mar 2025 12:00:23 +0100 Subject: [PATCH 021/193] feat: add external token decorator service Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/thirty-rules-press.md | 5 + docs/auth/service-to-service-auth.md | 31 +++- packages/backend-defaults/report-auth.api.md | 20 +++ .../auth/authServiceFactory.test.ts | 63 +++++++- .../entrypoints/auth/authServiceFactory.ts | 26 +++- .../external/ExternalTokenHandler.test.ts | 144 ++++++++++++++++++ .../auth/external/ExternalTokenHandler.ts | 26 +++- .../src/entrypoints/auth/index.ts | 3 + .../auth/plugin/PluginTokenHandler.ts | 2 +- 9 files changed, 309 insertions(+), 11 deletions(-) create mode 100644 .changeset/thirty-rules-press.md diff --git a/.changeset/thirty-rules-press.md b/.changeset/thirty-rules-press.md new file mode 100644 index 0000000000..f4758da67b --- /dev/null +++ b/.changeset/thirty-rules-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': minor +--- + +Add a new `externalTokenHandlerDecoratorServiceRef` to allow custom external token validations diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index d35aed364d..e821aeb0c7 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -414,9 +414,13 @@ Each entry has one or more of the following fields: ## Adding custom or logic for validation and issuing of tokens -The `pluginTokenHandlerDecoratorServiceRef` can be used to decorate the existing token handler without having to re-implement the entire `AuthService` implementation. +The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlerDecoratorServiceRef` can be used to decorate the existing token handler without having to re-implement the entire `AuthService` implementation. This is particularly useful when you want to add additional logic to the handler, such as logging or metrics or custom token validation. +### PluginTokenHandler decoration + +The `pluginTokenHandlerDecoratorServiceRef` can be used to decorate the default PluginTokenHandler used for create and verify tokens from plugins. + The `PluginTokenHandler` interface has two methods: - `issueToken`: This method is used to issue a token for a plugin. It takes in the `pluginId` and `targetPluginId` as arguments, and an optional `limitedUserToken` object which can be used to issue a token on behalf of another user. The method returns a promise that resolves to an object containing the issued token. @@ -439,3 +443,28 @@ const decoratedPluginTokenHandler = createServiceFactory({ }, }); ``` + +### ExternalTokenHandler decoration + +The `externalTokenHandlerDecoratorServiceRef` can be used to decorate the default ExternalTokenHandler used for verify tokens from external callers. + +The `ExternalTokenHandler` interface has one methods: + +- `verifyToken`: This method is used to verify a token. It takes in the token as an argument and returns a promise that resolves to an object containing the subject of the token and an optional limited user token. + +```ts +import { + ExternalTokenHandler, + externalTokenHandlerDecoratorServiceRef, +} from '@backstage/backend-defaults/auth'; +import { createServiceFactory } from '@backstage/backend-plugin-api'; + +const decoratedPluginTokenHandler = createServiceFactory({ + service: externalTokenHandlerDecoratorServiceRef, + deps: {}, + async factory() { + return (defaultImplementation: ExternalTokenHandler) => + new CustomTokenHandler(defaultImplementation); + }, +}); +``` diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 1883e65a6a..56a973813d 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -4,6 +4,7 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; +import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -14,6 +15,25 @@ export const authServiceFactory: ServiceFactory< 'singleton' >; +// @public +export interface ExternalTokenHandler { + // (undocumented) + verifyToken(token: string): Promise< + | { + subject: string; + accessRestrictions?: BackstagePrincipalAccessRestrictions; + } + | undefined + >; +} + +// @public +export const externalTokenHandlerDecoratorServiceRef: ServiceRef< + (defaultImplementation: ExternalTokenHandler) => ExternalTokenHandler, + 'plugin', + 'singleton' +>; + // @public export interface PluginTokenHandler { // (undocumented) diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index a244614135..990668083f 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -21,6 +21,8 @@ import { } from '@backstage/backend-test-utils'; import { authServiceFactory, + externalTokenHandlersServiceRef, + // externalTokenHandlersServiceRef, pluginTokenHandlerDecoratorServiceRef, } from './authServiceFactory'; import { base64url, decodeJwt } from 'jose'; @@ -30,6 +32,11 @@ import { setupServer } from 'msw/node'; import { toInternalBackstageCredentials } from './helpers'; import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { AccessRestriptionsMap, TokenHandler } from './external/types'; +import { Config } from '@backstage/config'; +import { x } from 'tar'; +// import { ExternalTokenHandler } from './external/ExternalTokenHandler'; +// import { TokenHandler } from './external/types'; const server = setupServer(); @@ -58,6 +65,13 @@ const mockDeps = [ subject: 'unlimited-static-subject', }, }, + { + type: 'custom', + options: { + [`custom-config`]: 'custom-config', + foo: 'bar', + }, + }, ], }, }, @@ -450,8 +464,55 @@ describe('authServiceFactory', () => { dependencies: [...mockDeps, customPluginTokenHandler], }); const searchAuth = await tester.getSubject('search'); - searchAuth.authenticate('unlimited-static-token'); + await searchAuth.authenticate('unlimited-static-token'); expect(customLogic).toHaveBeenCalledWith('unlimited-static-token'); }); }); + describe('add custom ExternalTokenHandler', () => { + it('should allow custom logic to be injected into the plugin token handler', async () => { + const customLogic = jest.fn(); + const customAddEntry = jest.fn(); + const customPluginTokenHandler = createServiceFactory({ + service: externalTokenHandlersServiceRef, + deps: {}, + async factory() { + return { + custom: new (class CustomHandler implements TokenHandler { + add(options: Config): void { + customAddEntry(options); + } + async verifyToken(token: string): Promise< + | { + subject: string; + allAccessRestrictions?: AccessRestriptionsMap; + } + | undefined + > { + customLogic(token); + return { + subject: 'foo', + }; + } + })(), + }; + }, + }); + const tester = ServiceFactoryTester.from(authServiceFactory, { + dependencies: [...mockDeps, customPluginTokenHandler], + }); + const searchAuth = await tester.getSubject('search'); + await searchAuth.authenticate('custom-token'); + expect(customAddEntry).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + options: expect.objectContaining({ + [`custom-config`]: 'custom-config', + foo: 'bar', + }), + }), + }), + ); + expect(customLogic).toHaveBeenCalledWith('custom-token'); + }); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index 91910292e6..48f0941b12 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -20,13 +20,18 @@ import { createServiceRef, } from '@backstage/backend-plugin-api'; import { DefaultAuthService } from './DefaultAuthService'; -import { ExternalTokenHandler } from './external/ExternalTokenHandler'; +import { + // DefaultExternalTokenHandler, + ExternalTokenHandler, +} from './external/ExternalTokenHandler'; import { DefaultPluginTokenHandler, PluginTokenHandler, } from './plugin/PluginTokenHandler'; import { createPluginKeySource } from './plugin/keys/createPluginKeySource'; import { UserTokenHandler } from './user/UserTokenHandler'; +import { TokenHandler } from './external/types'; +import { Config } from '@backstage/config'; /** * @public @@ -45,6 +50,22 @@ export const pluginTokenHandlerDecoratorServiceRef = createServiceRef< }, }), }); +/** + * @public + * This service is used to decorate the default plugin token handler with custom logic. + */ +export const externalTokenHandlersServiceRef = createServiceRef<{ + [configKey: string]: (config: Config) => TokenHandler; +}>({ + id: 'core.auth.externalTokenHandlers', + multiton: true, + // defaultFactory: async service => + // createServiceFactory({ + // service, + // deps: {}, + // factory: async () => {}, + // }), +}); /** * Handles token authentication and credentials management. @@ -64,6 +85,7 @@ export const authServiceFactory = createServiceFactory({ plugin: coreServices.pluginMetadata, database: coreServices.database, pluginTokenHandlerDecorator: pluginTokenHandlerDecoratorServiceRef, + externalTokenHandlers: externalTokenHandlersServiceRef, }, async factory({ config, @@ -72,6 +94,7 @@ export const authServiceFactory = createServiceFactory({ logger, database, pluginTokenHandlerDecorator, + externalTokenHandlers, }) { const disableDefaultAuthPolicy = config.getOptionalBoolean( @@ -106,6 +129,7 @@ export const authServiceFactory = createServiceFactory({ ownPluginId: plugin.getId(), config, logger, + externalTokenHandlers, }); return new DefaultAuthService( diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts index 39b01c46b6..de9c9e162b 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts @@ -208,4 +208,148 @@ describe('ExternalTokenHandler', () => { accessRestrictions: { permissionNames: ['catalog.entity.read'] }, }); }); + it('successfully uses custom token handlers', async () => { + const factory = new FakeTokenFactory({ + issuer: 'my-company', + keyDurationSeconds: 100, + }); + + server.use( + rest.get( + 'https://example.com/.well-known/jwks.json', + async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + return res(ctx.json(keys)); + }, + ), + ); + + const customHandler: TokenHandler = { + add: jest.fn(), + verifyToken: jest.fn(), + }; + + const handler = ExternalTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'internal-custom', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + externalTokenHandlers: [{ ['internal-custom']: customHandler }], + }); + + expect(customHandler.add).toHaveBeenCalledWith( + expect.objectContaining({ + data: { + type: 'internal-custom', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + }), + ); + + const customToken = await factory.issueToken({ + claims: { sub: 'internal-subject' }, + }); + + await handler.verifyToken(customToken); + + expect(customHandler.verifyToken).toHaveBeenCalled(); + }); + it('should fail if config contains types not declared', async () => { + const createHandler = () => + ExternalTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'internal-custom', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Unknown type 'internal-custom' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks'"`, + ); + }); + it('should show valid custom types in errors', async () => { + const createHandler = () => + ExternalTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'internal-custom-invalid', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + externalTokenHandlers: [ + { + ['internal-custom']: { + add: jest.fn(), + verifyToken: jest.fn(), + }, + }, + ], + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Unknown type 'internal-custom-invalid' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, + ); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts index 72eeae6fcf..a4c55810c3 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts @@ -24,6 +24,7 @@ import { LegacyTokenHandler } from './legacy'; import { StaticTokenHandler } from './static'; import { JWKSHandler } from './jwks'; import { TokenHandler } from './types'; +import { Config } from '@backstage/config'; const NEW_CONFIG_KEY = 'backend.auth.externalAccess'; const OLD_CONFIG_KEY = 'backend.auth.keys'; @@ -40,17 +41,27 @@ export class ExternalTokenHandler { ownPluginId: string; config: RootConfigService; logger: LoggerService; + externalTokenHandlers?: { + [key: string]: (config: Config) => TokenHandler; + }[]; }): ExternalTokenHandler { - const { ownPluginId, config, logger } = options; + const { + ownPluginId, + config, + logger, + externalTokenHandlers: customHandlers, + } = options; const staticHandler = new StaticTokenHandler(); const legacyHandler = new LegacyTokenHandler(); const jwksHandler = new JWKSHandler(); - const handlers: Record = { - static: staticHandler, - legacy: legacyHandler, - jwks: jwksHandler, + const handlers: Record TokenHandler> = { + static: (handlerConfig: Config) => staticHandler.add(handlerConfig), + legacy: (handlerConfig: Config) => legacyHandler.add(handlerConfig), + jwks: (handlerConfig: Config) => jwksHandler.add(handlerConfig), + ...Object.assign({}, ...(customHandlers ?? [])), }; + const configuredHandlers = []; // Load the new-style handlers const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? []; @@ -65,7 +76,8 @@ export class ExternalTokenHandler { `Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, ); } - handler.add(handlerConfig); + configuredHandlers.push(handler(handlerConfig)); + // handler.add(handlerConfig); } // Load the old keys too @@ -80,7 +92,7 @@ export class ExternalTokenHandler { legacyHandler.addOld(handlerConfig); } - return new ExternalTokenHandler(ownPluginId, Object.values(handlers)); + return new ExternalTokenHandler(ownPluginId, configuredHandlers); } constructor( diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index e48926f0ce..51fe982891 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -17,6 +17,9 @@ export { authServiceFactory, pluginTokenHandlerDecoratorServiceRef, + externalTokenHandlerDecoratorServiceRef, } from './authServiceFactory'; +export type { ExternalTokenHandler } from './external/ExternalTokenHandler'; + export type { PluginTokenHandler } from './plugin/PluginTokenHandler'; diff --git a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts index 9aec52bc08..b692cd2b87 100644 --- a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts @@ -46,7 +46,7 @@ type Options = { /** * @public - * Issues and verifies {@link https://backstage.iceio/docs/auth/service-to-service-auth | service-to-service tokens}. + * Issues and verifies {@link https://backstage.io/docs/auth/service-to-service-auth | service-to-service tokens}. */ export interface PluginTokenHandler { verifyToken( From 7d96f52c41cf9bca8d4ea2ab32468368c0645d40 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 24 Mar 2025 09:08:19 +0100 Subject: [PATCH 022/193] fix some test and types Signed-off-by: Juan Pablo Garcia Ripa --- .../auth/authServiceFactory.test.ts | 103 +++++++++++++----- .../external/ExternalTokenHandler.test.ts | 9 +- .../src/entrypoints/auth/external/jwks.ts | 1 + .../src/entrypoints/auth/external/legacy.ts | 1 + .../src/entrypoints/auth/external/static.ts | 1 + .../src/entrypoints/auth/external/types.ts | 2 +- 6 files changed, 85 insertions(+), 32 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index 990668083f..51df123555 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -22,7 +22,6 @@ import { import { authServiceFactory, externalTokenHandlersServiceRef, - // externalTokenHandlersServiceRef, pluginTokenHandlerDecoratorServiceRef, } from './authServiceFactory'; import { base64url, decodeJwt } from 'jose'; @@ -34,7 +33,6 @@ import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; import { AccessRestriptionsMap, TokenHandler } from './external/types'; import { Config } from '@backstage/config'; -import { x } from 'tar'; // import { ExternalTokenHandler } from './external/ExternalTokenHandler'; // import { TokenHandler } from './external/types'; @@ -65,13 +63,6 @@ const mockDeps = [ subject: 'unlimited-static-subject', }, }, - { - type: 'custom', - options: { - [`custom-config`]: 'custom-config', - foo: 'bar', - }, - }, ], }, }, @@ -472,36 +463,84 @@ describe('authServiceFactory', () => { it('should allow custom logic to be injected into the plugin token handler', async () => { const customLogic = jest.fn(); const customAddEntry = jest.fn(); + const customConfig = jest.fn(); + const deps = [ + discoveryServiceFactory, + mockServices.rootConfig.factory({ + data: { + backend: { + baseUrl: 'http://localhost', + auth: { + keys: [{ secret: 'abc' }], + externalAccess: [ + { + type: 'static', + options: { + token: 'limited-static-token', + subject: 'limited-static-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'do.it' }, + ], + }, + { + type: 'static', + options: { + token: 'unlimited-static-token', + subject: 'unlimited-static-subject', + }, + }, + { + type: 'custom', + options: { + [`custom-config`]: 'custom-config', + foo: 'bar', + }, + }, + ], + }, + }, + }, + }), + ]; + + const customHandler = new (class CustomHandler implements TokenHandler { + add(options: Config): TokenHandler { + customAddEntry(options); + return this; + } + async verifyToken(token: string): Promise< + | { + subject: string; + allAccessRestrictions?: AccessRestriptionsMap; + } + | undefined + > { + customLogic(token); + return { + subject: 'foo', + }; + } + })(); + const customPluginTokenHandler = createServiceFactory({ service: externalTokenHandlersServiceRef, deps: {}, async factory() { return { - custom: new (class CustomHandler implements TokenHandler { - add(options: Config): void { - customAddEntry(options); - } - async verifyToken(token: string): Promise< - | { - subject: string; - allAccessRestrictions?: AccessRestriptionsMap; - } - | undefined - > { - customLogic(token); - return { - subject: 'foo', - }; - } - })(), + custom: (options: Config) => { + customConfig(options); + return customHandler.add(options); + }, }; }, }); const tester = ServiceFactoryTester.from(authServiceFactory, { - dependencies: [...mockDeps, customPluginTokenHandler], + dependencies: [...deps, customPluginTokenHandler], }); const searchAuth = await tester.getSubject('search'); await searchAuth.authenticate('custom-token'); + expect(customAddEntry).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -513,6 +552,16 @@ describe('authServiceFactory', () => { }), ); expect(customLogic).toHaveBeenCalledWith('custom-token'); + expect(customConfig).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + options: expect.objectContaining({ + [`custom-config`]: 'custom-config', + foo: 'bar', + }), + }), + }), + ); }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts index de9c9e162b..3567061f03 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts @@ -228,6 +228,7 @@ describe('ExternalTokenHandler', () => { add: jest.fn(), verifyToken: jest.fn(), }; + const customHandlerCreator = jest.fn(() => customHandler); const handler = ExternalTokenHandler.create({ ownPluginId: 'catalog', @@ -253,10 +254,10 @@ describe('ExternalTokenHandler', () => { }, }, }), - externalTokenHandlers: [{ ['internal-custom']: customHandler }], + externalTokenHandlers: [{ ['internal-custom']: customHandlerCreator }], }); - expect(customHandler.add).toHaveBeenCalledWith( + expect(customHandlerCreator).toHaveBeenCalledWith( expect.objectContaining({ data: { type: 'internal-custom', @@ -340,10 +341,10 @@ describe('ExternalTokenHandler', () => { }), externalTokenHandlers: [ { - ['internal-custom']: { + ['internal-custom']: () => ({ add: jest.fn(), verifyToken: jest.fn(), - }, + }), }, ], }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts index d88dc62a47..32271972fb 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts @@ -68,6 +68,7 @@ export class JWKSHandler implements TokenHandler { url, allAccessRestrictions, }); + return this; } async verifyToken(token: string) { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts index 9c60e70707..fbd96aafd7 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts @@ -40,6 +40,7 @@ export class LegacyTokenHandler implements TokenHandler { config.getString('options.subject'), allAccessRestrictions, ); + return this; } // used only for the old backend.auth.keys array diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts index 1e89d8c6a1..b335b89e58 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.ts @@ -54,6 +54,7 @@ export class StaticTokenHandler implements TokenHandler { } this.#entries.set(token, { subject, allAccessRestrictions }); + return this; } async verifyToken(token: string) { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index 6a1fd084ed..e3cad90e17 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -23,7 +23,7 @@ export type AccessRestriptionsMap = Map< >; export interface TokenHandler { - add(options: Config): void; + add?(options: Config): TokenHandler; verifyToken(token: string): Promise< | { subject: string; From 9377fbfb0f2449e03289f566a5ad0d3b18874a8b Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 24 Mar 2025 10:22:43 +0100 Subject: [PATCH 023/193] feat(docs): add multiton docs and fix the custom handler service Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/thirty-rules-press.md | 2 +- docs/auth/service-to-service-auth.md | 22 +++++----- .../architecture/03-services.md | 29 +++++++++++++ packages/backend-defaults/report-auth.api.md | 41 ++++++++++++------- .../auth/authServiceFactory.test.ts | 4 +- .../entrypoints/auth/authServiceFactory.ts | 6 --- .../src/entrypoints/auth/external/helpers.ts | 6 +-- .../src/entrypoints/auth/external/jwks.ts | 4 +- .../src/entrypoints/auth/external/legacy.ts | 6 +-- .../src/entrypoints/auth/external/static.ts | 4 +- .../src/entrypoints/auth/external/types.ts | 12 +++++- .../src/entrypoints/auth/index.ts | 5 ++- 12 files changed, 93 insertions(+), 48 deletions(-) diff --git a/.changeset/thirty-rules-press.md b/.changeset/thirty-rules-press.md index f4758da67b..d9968317cb 100644 --- a/.changeset/thirty-rules-press.md +++ b/.changeset/thirty-rules-press.md @@ -2,4 +2,4 @@ '@backstage/backend-defaults': minor --- -Add a new `externalTokenHandlerDecoratorServiceRef` to allow custom external token validations +Add a new `externalTokenHandlersServiceRef` to allow custom external token validations diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index e821aeb0c7..8e8ef6a183 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -414,7 +414,7 @@ Each entry has one or more of the following fields: ## Adding custom or logic for validation and issuing of tokens -The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlerDecoratorServiceRef` can be used to decorate the existing token handler without having to re-implement the entire `AuthService` implementation. +The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlersServiceRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation. This is particularly useful when you want to add additional logic to the handler, such as logging or metrics or custom token validation. ### PluginTokenHandler decoration @@ -446,25 +446,27 @@ const decoratedPluginTokenHandler = createServiceFactory({ ### ExternalTokenHandler decoration -The `externalTokenHandlerDecoratorServiceRef` can be used to decorate the default ExternalTokenHandler used for verify tokens from external callers. +The `externalTokenHandlersServiceRef` can be used to add custom external token handlers to the default implementation. -The `ExternalTokenHandler` interface has one methods: +The returned object should be a map of token custom types and their handler factories. The object keys will be matched to the configured `externalAccess` type in the app-config, calling the factory function with the config object for that type. Custom token handlers should implement the `TokenHandler` interface, which provides methods for verifying tokens. -- `verifyToken`: This method is used to verify a token. It takes in the token as an argument and returns a promise that resolves to an object containing the subject of the token and an optional limited user token. +For example, to add a custom token handler for a type called 'custom': ```ts import { - ExternalTokenHandler, - externalTokenHandlerDecoratorServiceRef, + TokenHandler, + externalTokenHandlersServiceRef, } from '@backstage/backend-defaults/auth'; +import { Config } from '@backstage/config'; import { createServiceFactory } from '@backstage/backend-plugin-api'; -const decoratedPluginTokenHandler = createServiceFactory({ - service: externalTokenHandlerDecoratorServiceRef, +const customExternalTokenHandlers = createServiceFactory({ + service: externalTokenHandlersServiceRef, deps: {}, async factory() { - return (defaultImplementation: ExternalTokenHandler) => - new CustomTokenHandler(defaultImplementation); + return { + custom: (config: Config) => new CustomTokenHandler(config); + }; }, }); ``` diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index cfcd606d92..3b168b81e3 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -223,6 +223,35 @@ export const customFooServiceFactory = createServiceFactory({ This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. +## Multiton + +By default the service reference will point to a singleton instance of the service. This meand if a new service factory uses this reference will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service. +For some services, is desirable to extend the functionality instead of overriding it. For example, some services could have many handler to address a specific event, and you may want to add a new handler instead of overriding the previous one. In this case, you can use the `multiton` option when creating the service reference: + +```ts +// example-service-ref.ts +import { createServiceRef } from '@backstage/backend-plugin-api'; + +export interface FooService { + foo(options: FooOptions): Promise; +} + +export const fooServiceRef = createServiceRef({ + id: 'example.foo', + multiton: true, // this service ref will be an array of instances +}); +``` + +When adding this serviceRef ad dependency to a factory, the factory will receive an array of instances instead of a single instance: + +```ts +deps: {fooServices: fooServiceRef}, + factory(fooServices) { + // fooServices is an array of instances + return new Bar(fooServices); + }, +``` + ## Service Factory Options Pattern :::note Note diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 56a973813d..9b7a56cff5 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -5,9 +5,16 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; +// @public (undocumented) +export type AccessRestrictionsMap = Map< + string, // plugin ID + BackstagePrincipalAccessRestrictions +>; + // @public export const authServiceFactory: ServiceFactory< AuthService, @@ -16,22 +23,12 @@ export const authServiceFactory: ServiceFactory< >; // @public -export interface ExternalTokenHandler { - // (undocumented) - verifyToken(token: string): Promise< - | { - subject: string; - accessRestrictions?: BackstagePrincipalAccessRestrictions; - } - | undefined - >; -} - -// @public -export const externalTokenHandlerDecoratorServiceRef: ServiceRef< - (defaultImplementation: ExternalTokenHandler) => ExternalTokenHandler, +export const externalTokenHandlersServiceRef: ServiceRef< + { + [configKey: string]: (config: Config) => TokenHandler; + }, 'plugin', - 'singleton' + 'multiton' >; // @public @@ -64,5 +61,19 @@ export const pluginTokenHandlerDecoratorServiceRef: ServiceRef< 'singleton' >; +// @public +export interface TokenHandler { + // (undocumented) + add?(options: Config): TokenHandler; + // (undocumented) + verifyToken(token: string): Promise< + | { + subject: string; + allAccessRestrictions?: AccessRestrictionsMap; + } + | undefined + >; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index 51df123555..d6cc0023a8 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -31,7 +31,7 @@ import { setupServer } from 'msw/node'; import { toInternalBackstageCredentials } from './helpers'; import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; -import { AccessRestriptionsMap, TokenHandler } from './external/types'; +import { AccessRestrictionsMap, TokenHandler } from './external/types'; import { Config } from '@backstage/config'; // import { ExternalTokenHandler } from './external/ExternalTokenHandler'; // import { TokenHandler } from './external/types'; @@ -512,7 +512,7 @@ describe('authServiceFactory', () => { async verifyToken(token: string): Promise< | { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; } | undefined > { diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index 48f0941b12..ae8ad205b7 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -59,12 +59,6 @@ export const externalTokenHandlersServiceRef = createServiceRef<{ }>({ id: 'core.auth.externalTokenHandlers', multiton: true, - // defaultFactory: async service => - // createServiceFactory({ - // service, - // deps: {}, - // factory: async () => {}, - // }), }); /** diff --git a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts index d6aa0a01ff..4a01117e28 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { AccessRestriptionsMap } from './types'; +import { AccessRestrictionsMap } from './types'; /** * Parses and returns the `accessRestrictions` configuration from an @@ -25,12 +25,12 @@ import { AccessRestriptionsMap } from './types'; */ export function readAccessRestrictionsFromConfig( externalAccessEntryConfig: Config, -): AccessRestriptionsMap | undefined { +): AccessRestrictionsMap | undefined { const configs = externalAccessEntryConfig.getOptionalConfigArray('accessRestrictions') ?? []; - const result: AccessRestriptionsMap = new Map(); + const result: AccessRestrictionsMap = new Map(); for (const config of configs) { const validKeys = ['plugin', 'permission', 'permissionAttribute']; for (const key of config.keys()) { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts index 32271972fb..49167bcd35 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts @@ -20,7 +20,7 @@ import { readAccessRestrictionsFromConfig, readStringOrStringArrayFromConfig, } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap, TokenHandler } from './types'; /** * Handles `type: jwks` access. @@ -35,7 +35,7 @@ export class JWKSHandler implements TokenHandler { subjectPrefix?: string; url: URL; jwks: JWTVerifyGetKey; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; }> = []; add(config: Config) { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts index fbd96aafd7..09d759a09e 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap, TokenHandler } from './types'; /** * Handles `type: legacy` access. @@ -29,7 +29,7 @@ export class LegacyTokenHandler implements TokenHandler { key: Uint8Array; result: { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; }; }>(); @@ -52,7 +52,7 @@ export class LegacyTokenHandler implements TokenHandler { #doAdd( secret: string, subject: string, - allAccessRestrictions?: AccessRestriptionsMap, + allAccessRestrictions?: AccessRestrictionsMap, ) { if (!secret.match(/^\S+$/)) { throw new Error('Illegal secret, must be a valid base64 string'); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts index b335b89e58..e003f0530b 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap, TokenHandler } from './types'; const MIN_TOKEN_LENGTH = 8; @@ -30,7 +30,7 @@ export class StaticTokenHandler implements TokenHandler { string, { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; } >(); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index e3cad90e17..6b882ef16b 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -17,17 +17,25 @@ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; -export type AccessRestriptionsMap = Map< +/** + * @public + */ +export type AccessRestrictionsMap = Map< string, // plugin ID BackstagePrincipalAccessRestrictions >; +/** + * @public + * This interface is used to handle external tokens. + * It is used by the auth service to verify tokens and extract the subject. + */ export interface TokenHandler { add?(options: Config): TokenHandler; verifyToken(token: string): Promise< | { subject: string; - allAccessRestrictions?: AccessRestriptionsMap; + allAccessRestrictions?: AccessRestrictionsMap; } | undefined >; diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index 51fe982891..96db6c0535 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -17,9 +17,10 @@ export { authServiceFactory, pluginTokenHandlerDecoratorServiceRef, - externalTokenHandlerDecoratorServiceRef, + externalTokenHandlersServiceRef, } from './authServiceFactory'; -export type { ExternalTokenHandler } from './external/ExternalTokenHandler'; +export type { TokenHandler } from './external/types'; +export type { AccessRestrictionsMap } from './external/types'; export type { PluginTokenHandler } from './plugin/PluginTokenHandler'; From 410b81afd70627923e795a43269a2daec265a62d Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Fri, 25 Apr 2025 08:46:55 +0200 Subject: [PATCH 024/193] feat: use multiple Config factory Signed-off-by: Juan Pablo Garcia Ripa --- packages/backend-defaults/report-auth.api.md | 7 +- .../auth/authServiceFactory.test.ts | 38 ++--- .../entrypoints/auth/authServiceFactory.ts | 16 +- .../external/ExternalTokenHandler.test.ts | 100 +++++++++--- .../auth/external/ExternalTokenHandler.ts | 117 ++++++++++---- .../entrypoints/auth/external/jwks.test.ts | 45 +++--- .../src/entrypoints/auth/external/jwks.ts | 7 +- .../entrypoints/auth/external/legacy.test.ts | 143 ++++++++++-------- .../src/entrypoints/auth/external/legacy.ts | 20 +++ .../entrypoints/auth/external/static.test.ts | 129 ++++++++-------- .../src/entrypoints/auth/external/static.ts | 7 +- .../src/entrypoints/auth/external/types.ts | 2 - .../src/entrypoints/auth/index.ts | 3 +- 13 files changed, 392 insertions(+), 242 deletions(-) diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 9b7a56cff5..bcc5c4a073 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -23,9 +23,10 @@ export const authServiceFactory: ServiceFactory< >; // @public -export const externalTokenHandlersServiceRef: ServiceRef< +export const externalTokenTypeHandlersRef: ServiceRef< { - [configKey: string]: (config: Config) => TokenHandler; + type: string; + factory: (config: Config[]) => TokenHandler; }, 'plugin', 'multiton' @@ -63,8 +64,6 @@ export const pluginTokenHandlerDecoratorServiceRef: ServiceRef< // @public export interface TokenHandler { - // (undocumented) - add?(options: Config): TokenHandler; // (undocumented) verifyToken(token: string): Promise< | { diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index d6cc0023a8..b808973016 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -21,7 +21,6 @@ import { } from '@backstage/backend-test-utils'; import { authServiceFactory, - externalTokenHandlersServiceRef, pluginTokenHandlerDecoratorServiceRef, } from './authServiceFactory'; import { base64url, decodeJwt } from 'jose'; @@ -33,8 +32,7 @@ import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; import { AccessRestrictionsMap, TokenHandler } from './external/types'; import { Config } from '@backstage/config'; -// import { ExternalTokenHandler } from './external/ExternalTokenHandler'; -// import { TokenHandler } from './external/types'; +import { externalTokenTypeHandlersRef } from './external/ExternalTokenHandler'; const server = setupServer(); @@ -504,10 +502,11 @@ describe('authServiceFactory', () => { }), ]; - const customHandler = new (class CustomHandler implements TokenHandler { - add(options: Config): TokenHandler { - customAddEntry(options); - return this; + class CustomHandler implements TokenHandler { + constructor(options: Config[]) { + for (const option of options) { + customAddEntry(option); + } } async verifyToken(token: string): Promise< | { @@ -521,16 +520,17 @@ describe('authServiceFactory', () => { subject: 'foo', }; } - })(); + } const customPluginTokenHandler = createServiceFactory({ - service: externalTokenHandlersServiceRef, + service: externalTokenTypeHandlersRef, deps: {}, async factory() { return { - custom: (options: Config) => { - customConfig(options); - return customHandler.add(options); + type: 'custom', + factory: (configs: Config[]) => { + customConfig(configs); + return new CustomHandler(configs); }, }; }, @@ -553,14 +553,16 @@ describe('authServiceFactory', () => { ); expect(customLogic).toHaveBeenCalledWith('custom-token'); expect(customConfig).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - options: expect.objectContaining({ - [`custom-config`]: 'custom-config', - foo: 'bar', + expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + options: expect.objectContaining({ + [`custom-config`]: 'custom-config', + foo: 'bar', + }), }), }), - }), + ]), ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index ae8ad205b7..43a3c9da50 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -21,8 +21,8 @@ import { } from '@backstage/backend-plugin-api'; import { DefaultAuthService } from './DefaultAuthService'; import { - // DefaultExternalTokenHandler, ExternalTokenHandler, + externalTokenTypeHandlersRef, } from './external/ExternalTokenHandler'; import { DefaultPluginTokenHandler, @@ -30,8 +30,6 @@ import { } from './plugin/PluginTokenHandler'; import { createPluginKeySource } from './plugin/keys/createPluginKeySource'; import { UserTokenHandler } from './user/UserTokenHandler'; -import { TokenHandler } from './external/types'; -import { Config } from '@backstage/config'; /** * @public @@ -50,16 +48,6 @@ export const pluginTokenHandlerDecoratorServiceRef = createServiceRef< }, }), }); -/** - * @public - * This service is used to decorate the default plugin token handler with custom logic. - */ -export const externalTokenHandlersServiceRef = createServiceRef<{ - [configKey: string]: (config: Config) => TokenHandler; -}>({ - id: 'core.auth.externalTokenHandlers', - multiton: true, -}); /** * Handles token authentication and credentials management. @@ -79,7 +67,7 @@ export const authServiceFactory = createServiceFactory({ plugin: coreServices.pluginMetadata, database: coreServices.database, pluginTokenHandlerDecorator: pluginTokenHandlerDecoratorServiceRef, - externalTokenHandlers: externalTokenHandlersServiceRef, + externalTokenHandlers: externalTokenTypeHandlersRef, }, async factory({ config, diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts index 3567061f03..3348d84e93 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts @@ -85,12 +85,10 @@ describe('ExternalTokenHandler', () => { it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { const handler1: TokenHandler = { - add: jest.fn(), verifyToken: jest.fn().mockResolvedValue(undefined), }; const handler2: TokenHandler = { - add: jest.fn(), verifyToken: jest.fn().mockResolvedValue({ subject: 'sub', allAccessRestrictions: new Map( @@ -225,10 +223,9 @@ describe('ExternalTokenHandler', () => { ); const customHandler: TokenHandler = { - add: jest.fn(), verifyToken: jest.fn(), }; - const customHandlerCreator = jest.fn(() => customHandler); + const customHandlerFactory = jest.fn(() => customHandler); const handler = ExternalTokenHandler.create({ ownPluginId: 'catalog', @@ -254,23 +251,30 @@ describe('ExternalTokenHandler', () => { }, }, }), - externalTokenHandlers: [{ ['internal-custom']: customHandlerCreator }], + externalTokenHandlers: [ + { + type: 'internal-custom', + factory: customHandlerFactory, + }, + ], }); - expect(customHandlerCreator).toHaveBeenCalledWith( - expect.objectContaining({ - data: { - type: 'internal-custom', - options: { - issuer: 'my-company', - subject: 'internal-subject', - audience: 'backstage', + expect(customHandlerFactory).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + data: { + type: 'internal-custom', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], - }, - }), + }), + ]), ); const customToken = await factory.issueToken({ @@ -310,9 +314,10 @@ describe('ExternalTokenHandler', () => { }); expect(createHandler).toThrowErrorMatchingInlineSnapshot( - `"Unknown type 'internal-custom' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks'"`, + `"Unknown type(s) 'internal-custom' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks'"`, ); }); + it('should show valid custom types in errors', async () => { const createHandler = () => ExternalTokenHandler.create({ @@ -341,8 +346,8 @@ describe('ExternalTokenHandler', () => { }), externalTokenHandlers: [ { - ['internal-custom']: () => ({ - add: jest.fn(), + type: 'internal-custom', + factory: () => ({ verifyToken: jest.fn(), }), }, @@ -350,7 +355,58 @@ describe('ExternalTokenHandler', () => { }); expect(createHandler).toThrowErrorMatchingInlineSnapshot( - `"Unknown type 'internal-custom-invalid' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, + `"Unknown type(s) 'internal-custom-invalid' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, + ); + }); + it('should show all invalid config types', async () => { + const createHandler = () => + ExternalTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'internal-custom-invalid', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + { + type: 'internal-custom-invalid-2', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + externalTokenHandlers: [ + { + type: 'internal-custom', + factory: () => ({ + verifyToken: jest.fn(), + }), + }, + ], + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Unknown type(s) 'internal-custom-invalid, internal-custom-invalid-2' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts index a4c55810c3..cc93b7b182 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts @@ -16,20 +16,70 @@ import { BackstagePrincipalAccessRestrictions, + createServiceRef, LoggerService, RootConfigService, } from '@backstage/backend-plugin-api'; import { NotAllowedError } from '@backstage/errors'; -import { LegacyTokenHandler } from './legacy'; +import { LegacyConfigWrapper, LegacyTokenHandler } from './legacy'; import { StaticTokenHandler } from './static'; import { JWKSHandler } from './jwks'; import { TokenHandler } from './types'; import { Config } from '@backstage/config'; +import { groupBy } from 'lodash'; const NEW_CONFIG_KEY = 'backend.auth.externalAccess'; const OLD_CONFIG_KEY = 'backend.auth.keys'; let loggedDeprecationWarning = false; +/** + * @public + * This service is used to decorate the default plugin token handler with custom logic. + */ +export const externalTokenTypeHandlersRef = createServiceRef<{ + type: string; + factory: (config: Config[]) => TokenHandler; +}>({ + id: 'core.auth.externalTokenHandlers', + multiton: true, + // defaultFactory // :pepe-think: seems like is not possible to use defaultFactory with multiton +}); + +type TokenTypeHandler = { + type: string; + /** + * A factory function that takes all token configuration for a given type + * and returns a TokenHandler or an array of TokenHandlers. + */ + factory: (config: Config[]) => TokenHandler | TokenHandler[]; +}; +type LegacyTokenTypeHandler = { + type: 'legacy'; + /** + * A factory function that takes all token configuration for a given type + * and returns a TokenHandler or an array of TokenHandlers. + */ + factory: ( + config: (Config | LegacyConfigWrapper)[], + ) => TokenHandler | TokenHandler[]; +}; + +const defaultHandlers: (TokenTypeHandler | LegacyTokenTypeHandler)[] = [ + { + type: 'static', + factory: configs => new StaticTokenHandler(configs), + }, + { + type: 'legacy', + factory: (configs: (Config | { legacy: true; config: Config })[]) => + new LegacyTokenHandler(configs), + }, + { + type: 'jwks', + factory: configs => new JWKSHandler(configs), + }, +]; + /** * Handles all types of external caller token types (i.e. not Backstage user * tokens, nor Backstage backend plugin tokens). @@ -41,9 +91,7 @@ export class ExternalTokenHandler { ownPluginId: string; config: RootConfigService; logger: LoggerService; - externalTokenHandlers?: { - [key: string]: (config: Config) => TokenHandler; - }[]; + externalTokenHandlers?: TokenTypeHandler[]; }): ExternalTokenHandler { const { ownPluginId, @@ -52,36 +100,18 @@ export class ExternalTokenHandler { externalTokenHandlers: customHandlers, } = options; - const staticHandler = new StaticTokenHandler(); - const legacyHandler = new LegacyTokenHandler(); - const jwksHandler = new JWKSHandler(); - const handlers: Record TokenHandler> = { - static: (handlerConfig: Config) => staticHandler.add(handlerConfig), - legacy: (handlerConfig: Config) => legacyHandler.add(handlerConfig), - jwks: (handlerConfig: Config) => jwksHandler.add(handlerConfig), - ...Object.assign({}, ...(customHandlers ?? [])), - }; - const configuredHandlers = []; + const handlersTypes = [...defaultHandlers, ...(customHandlers ?? [])]; - // Load the new-style handlers const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? []; - for (const handlerConfig of handlerConfigs) { - const type = handlerConfig.getString('type'); - const handler = handlers[type]; - if (!handler) { - const valid = Object.keys(handlers) - .map(k => `'${k}'`) - .join(', '); - throw new Error( - `Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, - ); - } - configuredHandlers.push(handler(handlerConfig)); - // handler.add(handlerConfig); - } + const handlerConfigByType: Record & { + legacy?: (Config | LegacyConfigWrapper)[]; + } = groupBy(handlerConfigs, (handlerConfig: Config) => + handlerConfig.getString('type'), + ); // Load the old keys too const legacyConfigs = config.getOptionalConfigArray(OLD_CONFIG_KEY) ?? []; + if (legacyConfigs.length && !loggedDeprecationWarning) { loggedDeprecationWarning = true; logger.warn( @@ -89,10 +119,35 @@ export class ExternalTokenHandler { ); } for (const handlerConfig of legacyConfigs) { - legacyHandler.addOld(handlerConfig); + handlerConfigByType.legacy ??= []; + handlerConfigByType.legacy.push({ legacy: true, config: handlerConfig }); } - return new ExternalTokenHandler(ownPluginId, configuredHandlers); + const invalidTypes = Object.keys(handlerConfigByType).filter( + type => !handlersTypes.some(handler => handler.type === type), + ); + + if (invalidTypes.length > 0) { + const valid = handlersTypes + .map(handler => `'${handler.type}'`) + .join(', '); + throw new Error( + `Unknown type(s) '${invalidTypes.join( + ', ', + )}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, + ); + } + + const handlers = handlersTypes.flatMap(handler => { + const configs = handlerConfigByType[handler.type] ?? []; + const handlerInstances = handler.factory(configs); + if (Array.isArray(handlerInstances)) { + return handlerInstances; + } + return [handlerInstances]; + }); + + return new ExternalTokenHandler(ownPluginId, handlers); } constructor( diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts index 321e1823aa..eefbce5093 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts @@ -108,9 +108,7 @@ describe('JWKSHandler', () => { audience: 'backstage', }, }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(validEntry)); + const jwksHandler = new JWKSHandler([new ConfigReader(validEntry)]); const token = await factory.issueToken({ claims: { sub: mockSubject }, @@ -139,10 +137,10 @@ describe('JWKSHandler', () => { audience: ['multiple-audiences', 'backstage'], }, }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(invalidEntry)); - jwksHandler.add(new ConfigReader(validEntry)); + const jwksHandler = new JWKSHandler([ + new ConfigReader(invalidEntry), + new ConfigReader(validEntry), + ]); const token = await factory.issueToken({ claims: { sub: mockSubject }, @@ -169,10 +167,10 @@ describe('JWKSHandler', () => { audience: 'wrong', }, }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(invalidEntry1)); - jwksHandler.add(new ConfigReader(invalidEntry2)); + const jwksHandler = new JWKSHandler([ + new ConfigReader(invalidEntry1), + new ConfigReader(invalidEntry2), + ]); const token = await factory.issueToken({ claims: { sub: mockSubject }, @@ -184,30 +182,26 @@ describe('JWKSHandler', () => { }); it('rejects bad config', () => { - const jwksHandler = new JWKSHandler(); - expect(() => { - jwksHandler.add( + return new JWKSHandler([ new ConfigReader({ - options: { - url: 'https://exampl e.com/jwks', - }, + options: { url: 'https://exampl e.com/jwks' }, }), - ); + ]); }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); expect(() => { - jwksHandler.add( + return new JWKSHandler([ new ConfigReader({ options: { url: 'https://example.com/jwks\n', }, }), - ); + ]); }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); }); it('gracefully handles no added tokens', async () => { - const handler = new JWKSHandler(); + const handler = new JWKSHandler([]); await expect(handler.verifyToken('ghi')).resolves.toBeUndefined(); }); @@ -221,9 +215,7 @@ describe('JWKSHandler', () => { subjectPrefix: 'custom-prefix', }, }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(validEntry)); + const jwksHandler = new JWKSHandler([new ConfigReader(validEntry)]); const token = await factory.issueToken({ claims: { sub: mockSubject }, @@ -237,15 +229,14 @@ describe('JWKSHandler', () => { }); it('carries over access restrictions', async () => { - const jwksHandler = new JWKSHandler(); - jwksHandler.add( + const jwksHandler = new JWKSHandler([ new ConfigReader({ options: { url: `${mockBaseUrl}/.well-known/jwks.json`, }, accessRestrictions: [{ plugin: 'scaffolder', permission: 'do.it' }], }), - ); + ]); const token = await factory.issueToken({ claims: { sub: mockSubject } }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts index 49167bcd35..5f7fcb4127 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts @@ -38,7 +38,12 @@ export class JWKSHandler implements TokenHandler { allAccessRestrictions?: AccessRestrictionsMap; }> = []; - add(config: Config) { + constructor(configs: Config[]) { + for (const config of configs) { + this.add(config); + } + } + private add(config: Config) { if (!config.getString('options.url').match(/^\S+$/)) { throw new Error( 'Illegal JWKS URL, must be a set of non-space characters', diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts index c674e46ec9..4892d0a21a 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts @@ -21,7 +21,6 @@ import { DateTime } from 'luxon'; import { LegacyTokenHandler } from './legacy'; describe('LegacyTokenHandler', () => { - const tokenHandler = new LegacyTokenHandler(); const key1 = randomBytes(24); const key2 = randomBytes(24); const key3 = randomBytes(24); @@ -36,7 +35,7 @@ describe('LegacyTokenHandler', () => { }), ); - tokenHandler.add( + const configs = [ new ConfigReader({ options: { secret: key1.toString('base64'), @@ -44,8 +43,7 @@ describe('LegacyTokenHandler', () => { }, accessRestrictions: [{ plugin: 'scaffolder' }], }), - ); - tokenHandler.add( + new ConfigReader({ options: { secret: key2.toString('base64'), @@ -55,12 +53,14 @@ describe('LegacyTokenHandler', () => { { plugin: 'catalog', permission: 'catalog.entity.read' }, ], }), - ); - tokenHandler.addOld( - new ConfigReader({ - secret: key3.toString('base64'), - }), - ); + { + legacy: true, + config: new ConfigReader({ + secret: key3.toString('base64'), + }), + }, + ]; + const tokenHandler = new LegacyTokenHandler(configs); it('should verify valid tokens', async () => { const token1 = await new SignJWT({ @@ -163,94 +163,113 @@ describe('LegacyTokenHandler', () => { }); it('rejects bad config', () => { - const handler = new LegacyTokenHandler(); + const handler = new LegacyTokenHandler([]); // new style add, bad secrets - expect(() => - handler.add( - new ConfigReader({ options: { _missingsecret: true, subject: 'ok' } }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ + options: { _missingsecret: true, subject: 'ok' }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Missing required config value at 'options.secret' in 'mock-config'"`, ); - expect(() => - handler.add(new ConfigReader({ options: { secret: '', subject: 'ok' } })), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ options: { secret: '', subject: 'ok' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.secret' in 'mock-config', got empty-string, wanted string"`, ); - expect(() => - handler.add( - new ConfigReader({ options: { secret: 'has spaces', subject: 'ok' } }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ + options: { secret: 'has spaces', subject: 'ok' }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal secret, must be a valid base64 string"`, ); - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'hasnewline\n', subject: 'ok' }, - }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ + options: { secret: 'hasnewline\n', subject: 'ok' }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal secret, must be a valid base64 string"`, ); - expect(() => - handler.add(new ConfigReader({ options: { secret: 3, subject: 'ok' } })), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ options: { secret: 3, subject: 'ok' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.secret' in 'mock-config', got number, wanted string"`, ); // new style add, bad subjects - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', _missingsubject: true }, - }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ + options: { secret: 'b2s=', _missingsubject: true }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Missing required config value at 'options.subject' in 'mock-config'"`, ); - expect(() => - handler.add( - new ConfigReader({ options: { secret: 'b2s=', subject: '' } }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ options: { secret: 'b2s=', subject: '' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, ); - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', subject: 'has spaces' }, - }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ + options: { secret: 'b2s=', subject: 'has spaces' }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', subject: 'hasnewline\n' }, - }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ + options: { secret: 'b2s=', subject: 'hasnewline\n' }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); - expect(() => - handler.add( - new ConfigReader({ options: { secret: 'b2s=', subject: 3 } }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ options: { secret: 'b2s=', subject: 3 } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, ); // new style add, bad access restrictions - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', subject: 'subject' }, - accessRestrictions: [{ plugin: ['a'] }], - }), - ), + expect( + () => + new LegacyTokenHandler([ + new ConfigReader({ + options: { secret: 'b2s=', subject: 'subject' }, + accessRestrictions: [{ plugin: ['a'] }], + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'accessRestrictions[0].plugin' in 'mock-config', got array, wanted string"`, ); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts index 09d759a09e..8be0de19e4 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts @@ -19,6 +19,10 @@ import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; import { readAccessRestrictionsFromConfig } from './helpers'; import { AccessRestrictionsMap, TokenHandler } from './types'; +export type LegacyConfigWrapper = { + legacy: boolean; + config: Config; +}; /** * Handles `type: legacy` access. * @@ -33,6 +37,16 @@ export class LegacyTokenHandler implements TokenHandler { }; }>(); + constructor(configs: (Config | LegacyConfigWrapper)[]) { + for (const config of configs) { + if (isLegacy(config)) { + this.addOld(config.config); + continue; + } + this.add(config); + } + } + add(config: Config) { const allAccessRestrictions = readAccessRestrictionsFromConfig(config); this.#doAdd( @@ -119,3 +133,9 @@ export class LegacyTokenHandler implements TokenHandler { return undefined; } } + +function isLegacy( + config: Config | LegacyConfigWrapper, +): config is LegacyConfigWrapper { + return (config as LegacyConfigWrapper).legacy === true; +} diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts index a5945daba9..cf1c1a46ac 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts @@ -19,21 +19,19 @@ import { StaticTokenHandler } from './static'; describe('StaticTokenHandler', () => { it('accepts any of the added list of tokens', async () => { - const handler = new StaticTokenHandler(); - handler.add( + const configs = [ new ConfigReader({ options: { token: 'abcabcabc', subject: 'one' }, accessRestrictions: [{ plugin: 'scaffolder' }], }), - ); - handler.add( new ConfigReader({ options: { token: 'defdefdef', subject: 'two' }, accessRestrictions: [ { plugin: 'catalog', permission: 'catalog.entity.read' }, ], }), - ); + ]; + const handler = new StaticTokenHandler(configs); const accessRestrictionsOne = new Map(Object.entries({ scaffolder: {} })); const accessRestrictionsTwo = new Map( Object.entries({ @@ -55,95 +53,108 @@ describe('StaticTokenHandler', () => { }); it('gracefully handles no added tokens', async () => { - const handler = new StaticTokenHandler(); + const handler = new StaticTokenHandler([]); await expect(handler.verifyToken('ghi')).resolves.toBeUndefined(); }); it('rejects bad config', () => { - const handler = new StaticTokenHandler(); - - expect(() => - handler.add( - new ConfigReader({ options: { _missingtoken: true, subject: 'ok' } }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ options: { _missingtoken: true, subject: 'ok' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Missing required config value at 'options.token' in 'mock-config'"`, ); - expect(() => - handler.add(new ConfigReader({ options: { token: '', subject: 'ok' } })), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ options: { token: '', subject: 'ok' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.token' in 'mock-config', got empty-string, wanted string"`, ); - expect(() => - handler.add( - new ConfigReader({ options: { token: 'has spaces', subject: 'ok' } }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ options: { token: 'has spaces', subject: 'ok' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be a set of non-space characters"`, ); - expect(() => - handler.add( - new ConfigReader({ - options: { - token: 'hasnewlinebutislongenough\n', - subject: 'ok', - }, - }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ + options: { + token: 'hasnewlinebutislongenough\n', + subject: 'ok', + }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be a set of non-space characters"`, ); - expect(() => - handler.add( - new ConfigReader({ options: { token: 'short', subject: 'ok' } }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ options: { token: 'short', subject: 'ok' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be at least 8 characters length"`, ); - expect(() => - handler.add(new ConfigReader({ options: { token: 3, subject: 'ok' } })), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ options: { token: 3, subject: 'ok' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.token' in 'mock-config', got number, wanted string"`, ); - expect(() => - handler.add( - new ConfigReader({ - options: { token: 'validtoken', _missingsubject: true }, - }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ + options: { token: 'validtoken', _missingsubject: true }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Missing required config value at 'options.subject' in 'mock-config'"`, ); - expect(() => - handler.add( - new ConfigReader({ options: { token: 'validtoken', subject: '' } }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ options: { token: 'validtoken', subject: '' } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, ); - expect(() => - handler.add( - new ConfigReader({ - options: { token: 'validtoken', subject: 'has spaces' }, - }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ + options: { token: 'validtoken', subject: 'has spaces' }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); - expect(() => - handler.add( - new ConfigReader({ - options: { token: 'validtoken', subject: 'hasnewline\n' }, - }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ + options: { token: 'validtoken', subject: 'hasnewline\n' }, + }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); - expect(() => - handler.add( - new ConfigReader({ options: { token: 'validtoken', subject: 3 } }), - ), + expect( + () => + new StaticTokenHandler([ + new ConfigReader({ options: { token: 'validtoken', subject: 3 } }), + ]), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, ); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts index e003f0530b..7764569aca 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.ts @@ -34,7 +34,12 @@ export class StaticTokenHandler implements TokenHandler { } >(); - add(config: Config) { + constructor(configs: Config[]) { + for (const config of configs) { + this.add(config); + } + } + private add(config: Config) { const token = config.getString('options.token'); const subject = config.getString('options.subject'); const allAccessRestrictions = readAccessRestrictionsFromConfig(config); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index 6b882ef16b..c3d45fd0d7 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -15,7 +15,6 @@ */ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; /** * @public @@ -31,7 +30,6 @@ export type AccessRestrictionsMap = Map< * It is used by the auth service to verify tokens and extract the subject. */ export interface TokenHandler { - add?(options: Config): TokenHandler; verifyToken(token: string): Promise< | { subject: string; diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index 96db6c0535..1b1a29a247 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -17,9 +17,10 @@ export { authServiceFactory, pluginTokenHandlerDecoratorServiceRef, - externalTokenHandlersServiceRef, } from './authServiceFactory'; +export { externalTokenTypeHandlersRef } from './external/ExternalTokenHandler'; + export type { TokenHandler } from './external/types'; export type { AccessRestrictionsMap } from './external/types'; From 65d382e272f66896623a5d611caf77d9fe772b52 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Thu, 15 May 2025 22:58:35 +0200 Subject: [PATCH 025/193] fix: update docs with new approach and export types Signed-off-by: Juan Pablo Garcia Ripa --- docs/auth/service-to-service-auth.md | 35 ++++++++++++++---- packages/backend-defaults/report-auth.api.md | 12 ++++-- .../auth/external/ExternalTokenHandler.ts | 37 ++++++++----------- .../src/entrypoints/auth/external/types.ts | 15 ++++++++ .../src/entrypoints/auth/index.ts | 2 +- 5 files changed, 67 insertions(+), 34 deletions(-) diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index 8e8ef6a183..4d8829d91d 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -414,7 +414,7 @@ Each entry has one or more of the following fields: ## Adding custom or logic for validation and issuing of tokens -The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlersServiceRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation. +The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenTypeHandlersRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation. This is particularly useful when you want to add additional logic to the handler, such as logging or metrics or custom token validation. ### PluginTokenHandler decoration @@ -446,26 +446,47 @@ const decoratedPluginTokenHandler = createServiceFactory({ ### ExternalTokenHandler decoration -The `externalTokenHandlersServiceRef` can be used to add custom external token handlers to the default implementation. +The `externalTokenTypeHandlersRef` can be used to add custom external token handlers to the default implementation. -The returned object should be a map of token custom types and their handler factories. The object keys will be matched to the configured `externalAccess` type in the app-config, calling the factory function with the config object for that type. Custom token handlers should implement the `TokenHandler` interface, which provides methods for verifying tokens. +The returned object from the factory function must have a `type` property which is used to identify the handler. The `factory` method is called with an array of `Config` objects, for the given type. The factory method can return a single handler or an array of handlers. The handlers are then used to handle the token for the given type. -For example, to add a custom token handler for a type called 'custom': +For example, if we whant to add a custom external token handler for the `custom` type: + +our config would look like this: + +```yaml title="in e.g. app-config.production.yaml" +backend: + auth: + externalAccess: + - type: custom + options: + customOptions: additional-value + accessRestrictions: + - plugin: events + - type: custom + options: + customOptions: another-value + accessRestrictions: + - plugin: events +``` + +And we can implement the custom token handler like this: ```ts import { TokenHandler, - externalTokenHandlersServiceRef, + externalTokenTypeHandlersRef, } from '@backstage/backend-defaults/auth'; import { Config } from '@backstage/config'; import { createServiceFactory } from '@backstage/backend-plugin-api'; const customExternalTokenHandlers = createServiceFactory({ - service: externalTokenHandlersServiceRef, + service: externalTokenTypeHandlersRef, deps: {}, async factory() { return { - custom: (config: Config) => new CustomTokenHandler(config); + type: 'custom', + factory: (configs: Config[]) => new CustomTokenHandler(configs), // can return a simple handler or an array of handlers }; }, }); diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index bcc5c4a073..2a80a59d2d 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -24,10 +24,7 @@ export const authServiceFactory: ServiceFactory< // @public export const externalTokenTypeHandlersRef: ServiceRef< - { - type: string; - factory: (config: Config[]) => TokenHandler; - }, + TokenTypeHandler, 'plugin', 'multiton' >; @@ -74,5 +71,12 @@ export interface TokenHandler { >; } +// @public +export interface TokenTypeHandler { + factory: (configs: Config[]) => TokenHandler | TokenHandler[]; + // (undocumented) + type: string; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts index cc93b7b182..c3fab70026 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts @@ -27,32 +27,12 @@ import { JWKSHandler } from './jwks'; import { TokenHandler } from './types'; import { Config } from '@backstage/config'; import { groupBy } from 'lodash'; +import { TokenTypeHandler } from './types'; const NEW_CONFIG_KEY = 'backend.auth.externalAccess'; const OLD_CONFIG_KEY = 'backend.auth.keys'; let loggedDeprecationWarning = false; -/** - * @public - * This service is used to decorate the default plugin token handler with custom logic. - */ -export const externalTokenTypeHandlersRef = createServiceRef<{ - type: string; - factory: (config: Config[]) => TokenHandler; -}>({ - id: 'core.auth.externalTokenHandlers', - multiton: true, - // defaultFactory // :pepe-think: seems like is not possible to use defaultFactory with multiton -}); - -type TokenTypeHandler = { - type: string; - /** - * A factory function that takes all token configuration for a given type - * and returns a TokenHandler or an array of TokenHandlers. - */ - factory: (config: Config[]) => TokenHandler | TokenHandler[]; -}; type LegacyTokenTypeHandler = { type: 'legacy'; /** @@ -60,10 +40,23 @@ type LegacyTokenTypeHandler = { * and returns a TokenHandler or an array of TokenHandlers. */ factory: ( - config: (Config | LegacyConfigWrapper)[], + configs: ( + | import('@backstage/config').Config + | { legacy: true; config: import('@backstage/config').Config } + )[], ) => TokenHandler | TokenHandler[]; }; +/** + * @public + * This service is used to decorate the default plugin token handler with custom logic. + */ +export const externalTokenTypeHandlersRef = createServiceRef({ + id: 'core.auth.externalTokenHandlers', + multiton: true, + // defaultFactory // :pepe-think: seems like is not possible to use defaultFactory with multiton +}); + const defaultHandlers: (TokenTypeHandler | LegacyTokenTypeHandler)[] = [ { type: 'static', diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index c3d45fd0d7..5e26b66da5 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -38,3 +38,18 @@ export interface TokenHandler { | undefined >; } + +/** + * @public + * This interface is used to handle external tokens. + */ +export interface TokenTypeHandler { + type: string; + /** + * A factory function that takes all token configuration for a given type + * and returns a TokenHandler or an array of TokenHandlers. + */ + factory: ( + configs: import('@backstage/config').Config[], + ) => TokenHandler | TokenHandler[]; +} diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index 1b1a29a247..335fc2f62c 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -21,7 +21,7 @@ export { export { externalTokenTypeHandlersRef } from './external/ExternalTokenHandler'; -export type { TokenHandler } from './external/types'; +export type { TokenHandler, TokenTypeHandler } from './external/types'; export type { AccessRestrictionsMap } from './external/types'; export type { PluginTokenHandler } from './plugin/PluginTokenHandler'; From 79336700729da563bbdf6798a602a3c42e1609ad Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Wed, 9 Jul 2025 22:41:36 +0200 Subject: [PATCH 026/193] fix typos in docs Signed-off-by: Juan Pablo Garcia Ripa --- docs/auth/service-to-service-auth.md | 4 ++-- docs/backend-system/architecture/03-services.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index 4d8829d91d..322edbe6b6 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -444,13 +444,13 @@ const decoratedPluginTokenHandler = createServiceFactory({ }); ``` -### ExternalTokenHandler decoration +### Custom ExternalTokenHandler The `externalTokenTypeHandlersRef` can be used to add custom external token handlers to the default implementation. The returned object from the factory function must have a `type` property which is used to identify the handler. The `factory` method is called with an array of `Config` objects, for the given type. The factory method can return a single handler or an array of handlers. The handlers are then used to handle the token for the given type. -For example, if we whant to add a custom external token handler for the `custom` type: +For example, if we want to add a custom external token handler for the `custom` type: our config would look like this: diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 3b168b81e3..f6722496e5 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -225,7 +225,7 @@ This allows you to provide more advanced options for the service implementation ## Multiton -By default the service reference will point to a singleton instance of the service. This meand if a new service factory uses this reference will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service. +By default the service reference will point to a singleton instance of the service. This mean if a new service factory uses this reference will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service. For some services, is desirable to extend the functionality instead of overriding it. For example, some services could have many handler to address a specific event, and you may want to add a new handler instead of overriding the previous one. In this case, you can use the `multiton` option when creating the service reference: ```ts @@ -242,7 +242,7 @@ export const fooServiceRef = createServiceRef({ }); ``` -When adding this serviceRef ad dependency to a factory, the factory will receive an array of instances instead of a single instance: +When adding this `serviceRef` as a dependency to a factory, the factory will receive an array of instances instead of a single instance: ```ts deps: {fooServices: fooServiceRef}, From 445baebdedc78a8983853c13f55f56766e09df23 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Sat, 6 Sep 2025 19:02:05 +0200 Subject: [PATCH 027/193] refactor: rename ExternalTokenHandler to ExternalAuthTokenHandler Signed-off-by: Juan Pablo Garcia Ripa --- ...ernalTokenHandler.test.ts => ExternalAuthTokenHandler.test.ts} | 0 .../{ExternalTokenHandler.ts => ExternalAuthTokenHandler.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/backend-defaults/src/entrypoints/auth/external/{ExternalTokenHandler.test.ts => ExternalAuthTokenHandler.test.ts} (100%) rename packages/backend-defaults/src/entrypoints/auth/external/{ExternalTokenHandler.ts => ExternalAuthTokenHandler.ts} (100%) diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts similarity index 100% rename from packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts rename to packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts similarity index 100% rename from packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts rename to packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts From 95396a44f9b6ae7c12f2886751a4ec1049993e3b Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Sat, 6 Sep 2025 19:03:36 +0200 Subject: [PATCH 028/193] feat: simplify external token handler API - Remove accessRestrictions from individual handlers (now managed by framework) - Change API to evaluate configs individually for better performance Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/thirty-rules-press.md | 21 ++ docs/auth/service-to-service-auth.md | 100 +++++- packages/backend-defaults/report-auth.api.md | 2 +- .../entrypoints/auth/DefaultAuthService.ts | 4 +- .../auth/authServiceFactory.test.ts | 73 ++-- .../entrypoints/auth/authServiceFactory.ts | 6 +- .../external/ExternalAuthTokenHandler.test.ts | 174 ++++------ .../auth/external/ExternalAuthTokenHandler.ts | 133 +++----- .../src/entrypoints/auth/external/helpers.ts | 8 +- .../entrypoints/auth/external/jwks.test.ts | 131 ++------ .../src/entrypoints/auth/external/jwks.ts | 109 +++--- .../entrypoints/auth/external/legacy.test.ts | 314 ++++++++---------- .../src/entrypoints/auth/external/legacy.ts | 174 ++++------ .../entrypoints/auth/external/static.test.ts | 196 ++++++----- .../src/entrypoints/auth/external/static.ts | 103 ++++-- .../src/entrypoints/auth/external/types.ts | 42 ++- .../src/entrypoints/auth/index.ts | 6 +- 17 files changed, 728 insertions(+), 868 deletions(-) diff --git a/.changeset/thirty-rules-press.md b/.changeset/thirty-rules-press.md index d9968317cb..760780eb84 100644 --- a/.changeset/thirty-rules-press.md +++ b/.changeset/thirty-rules-press.md @@ -3,3 +3,24 @@ --- Add a new `externalTokenHandlersServiceRef` to allow custom external token validations + +BREAKING CHANGE: The `backend.auth.keys` config has been removed. Please migrate to the new `backend.auth.externalAccess` config as described in the documentation: https://backstage.io/docs/auth/service-to-service-auth + +**Migration Example:** + +```yaml +# ❌ Old format (no longer supported) +backend: + auth: + keys: + - secret: your-secret-key + +# ✅ New format +backend: + auth: + externalAccess: + - type: static + options: + token: your-secret-key + subject: external:backstage-plugin # this is the current default for old keys +``` diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index 322edbe6b6..4ba094068e 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -255,10 +255,16 @@ backend: subject: legacy-scaffolder ``` -The old style keys config is also supported as an alternative, but please -consider using the new style above instead: +:::warning +The old style `backend.auth.keys` config is **no longer supported** and has been removed. +If you are still using this configuration, you must migrate to the new `backend.auth.externalAccess` format above. -```yaml title="in e.g. app-config.production.yaml" +You'll see an error like `"Unknown key 'backend.auth.keys'"` during Backstage startup if you haven't migrated yet. +::: + +To migrate from the old config format: + +```yaml title="❌ Old format (no longer supported)" backend: auth: keys: @@ -266,6 +272,22 @@ backend: - secret: my-secret-key-scaffolder ``` +Convert to: + +```yaml title="✅ New format" +backend: + auth: + externalAccess: + - type: legacy + options: + secret: my-secret-key-catalog + subject: external:backstage-plugin + - type: legacy + options: + secret: my-secret-key-scaffolder + subject: external:backstage-plugin +``` + The secrets must be any base64-encoded random data, but for security reasons should be sufficiently long so as not to be easy to guess by brute force. You can for example generate them on the command line: @@ -444,11 +466,17 @@ const decoratedPluginTokenHandler = createServiceFactory({ }); ``` -### Custom ExternalTokenHandler +### Adding custom ExternalTokenHandler The `externalTokenTypeHandlersRef` can be used to add custom external token handlers to the default implementation. -The returned object from the factory function must have a `type` property which is used to identify the handler. The `factory` method is called with an array of `Config` objects, for the given type. The factory method can return a single handler or an array of handlers. The handlers are then used to handle the token for the given type. +Your service factory must return an object with a `type` property that matches the token type in your configuration (e.g., 'custom', 'api-key'). When Backstage encounters tokens of this type, it calls your `initialize` method with all the configuration entries that match this type. Your factory can return either a single token handler or an array of handlers to process and validate these tokens. + +:::note Note + +During token verification, all the token handlers are tested. Consider this when adding many token handlers, as it may impact performance. + +::: For example, if we want to add a custom external token handler for the `custom` type: @@ -474,20 +502,72 @@ And we can implement the custom token handler like this: ```ts import { - TokenHandler, + ExternalTokenHandler, externalTokenTypeHandlersRef, + createExternalTokenHandler, } from '@backstage/backend-defaults/auth'; -import { Config } from '@backstage/config'; import { createServiceFactory } from '@backstage/backend-plugin-api'; const customExternalTokenHandlers = createServiceFactory({ service: externalTokenTypeHandlersRef, deps: {}, async factory() { - return { + return createExternalTokenHandler({ type: 'custom', - factory: (configs: Config[]) => new CustomTokenHandler(configs), // can return a simple handler or an array of handlers - }; + initialize({ options }) { + // Initialize your handler context from config + const customOptions = options.getString('customOptions'); + return { customOptions }; + }, + async verifyToken(token, context) { + // Your custom token validation logic here + // Return undefined if token is invalid + // Return { subject: 'your-subject' } if token is valid + + if (token === 'valid-token') { + return { subject: `custom:${context.customOptions}` }; + } + return undefined; + }, + }); + }, +}); +``` + +The `createExternalTokenHandler` helper simplifies creating external token handlers with the new API: + +- **`type`**: A string identifier for your token handler type that matches the config +- **`initialize`**: Called once for each config entry of this type, receives the config options and returns a context object that will be passed to `verifyToken` +- **`verifyToken`**: Called for each token verification with the token and context, returns the subject if valid or `undefined` if not + +```ts +// Example of a more complex handler with external API call +const apiTokenHandler = createExternalTokenHandler({ + type: 'api-validation', + initialize({ options }) { + const apiBaseUrl = options.getString('apiBaseUrl'); + const apiKey = options.getString('apiKey'); + return { apiBaseUrl, apiKey }; + }, + async verifyToken(token, { apiBaseUrl, apiKey }) { + try { + const response = await fetch(`${apiBaseUrl}/validate-token`, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }), + }); + + if (response.ok) { + const { userId } = await response.json(); + return { subject: `api:${userId}` }; + } + } catch (error) { + // Log error but don't throw - return undefined for invalid tokens + } + return undefined; }, }); ``` diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 2a80a59d2d..176bdd16de 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -60,7 +60,7 @@ export const pluginTokenHandlerDecoratorServiceRef: ServiceRef< >; // @public -export interface TokenHandler { +export interface ExternalTokenHandler { // (undocumented) verifyToken(token: string): Promise< | { diff --git a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts index d8cb60e8b5..c2c4a064a3 100644 --- a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts +++ b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts @@ -25,7 +25,7 @@ import { import { AuthenticationError } from '@backstage/errors'; import { JsonObject } from '@backstage/types'; import { decodeJwt } from 'jose'; -import { ExternalTokenHandler } from './external/ExternalTokenHandler'; +import { ExternalAuthTokenManager } from './external/export class ExternalAuthTokenManager {'; import { createCredentialsWithNonePrincipal, createCredentialsWithServicePrincipal, @@ -41,7 +41,7 @@ export class DefaultAuthService implements AuthService { constructor( private readonly userTokenHandler: UserTokenHandler, private readonly pluginTokenHandler: PluginTokenHandler, - private readonly externalTokenHandler: ExternalTokenHandler, + private readonly externalTokenHandler: ExternalAuthTokenManager, private readonly pluginId: string, private readonly disableDefaultAuthPolicy: boolean, private readonly pluginKeySource: PluginKeySource, diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index b808973016..b6d5beefa2 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -30,9 +30,9 @@ import { setupServer } from 'msw/node'; import { toInternalBackstageCredentials } from './helpers'; import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; -import { AccessRestrictionsMap, TokenHandler } from './external/types'; -import { Config } from '@backstage/config'; -import { externalTokenTypeHandlersRef } from './external/ExternalTokenHandler'; + +import { externalTokenTypeHandlersRef } from './external/ExternalAuthTokenHandler'; +import { createExternalTokenHandler } from './external/helpers'; const server = setupServer(); @@ -44,7 +44,7 @@ const mockDeps = [ backend: { baseUrl: 'http://localhost', auth: { - keys: [{ secret: 'abc' }], + // keys: [{ secret: 'abc' }], externalAccess: [ { type: 'static', @@ -460,7 +460,6 @@ describe('authServiceFactory', () => { describe('add custom ExternalTokenHandler', () => { it('should allow custom logic to be injected into the plugin token handler', async () => { const customLogic = jest.fn(); - const customAddEntry = jest.fn(); const customConfig = jest.fn(); const deps = [ discoveryServiceFactory, @@ -469,7 +468,7 @@ describe('authServiceFactory', () => { backend: { baseUrl: 'http://localhost', auth: { - keys: [{ secret: 'abc' }], + // keys: [{ secret: 'abc' }w], externalAccess: [ { type: 'static', @@ -502,37 +501,30 @@ describe('authServiceFactory', () => { }), ]; - class CustomHandler implements TokenHandler { - constructor(options: Config[]) { - for (const option of options) { - customAddEntry(option); - } - } - async verifyToken(token: string): Promise< - | { - subject: string; - allAccessRestrictions?: AccessRestrictionsMap; - } - | undefined - > { + const customExternalTokenHandler = createExternalTokenHandler<{ + [`custom-config`]: string; + foo: string; + }>({ + type: 'custom', + initialize({ options }) { + const customConfigValue = options.getString('custom-config'); + const foo = options.getString('foo'); + customConfig(customConfigValue); + return { [`custom-config`]: customConfigValue, foo }; + }, + async verifyToken(token, ctx) { customLogic(token); return { - subject: 'foo', + subject: ctx.foo, }; - } - } + }, + }); const customPluginTokenHandler = createServiceFactory({ service: externalTokenTypeHandlersRef, deps: {}, async factory() { - return { - type: 'custom', - factory: (configs: Config[]) => { - customConfig(configs); - return new CustomHandler(configs); - }, - }; + return customExternalTokenHandler; }, }); const tester = ServiceFactoryTester.from(authServiceFactory, { @@ -541,29 +533,8 @@ describe('authServiceFactory', () => { const searchAuth = await tester.getSubject('search'); await searchAuth.authenticate('custom-token'); - expect(customAddEntry).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - options: expect.objectContaining({ - [`custom-config`]: 'custom-config', - foo: 'bar', - }), - }), - }), - ); expect(customLogic).toHaveBeenCalledWith('custom-token'); - expect(customConfig).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - options: expect.objectContaining({ - [`custom-config`]: 'custom-config', - foo: 'bar', - }), - }), - }), - ]), - ); + expect(customConfig).toHaveBeenCalledWith('custom-config'); }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index 43a3c9da50..34fc3b51ef 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -21,9 +21,9 @@ import { } from '@backstage/backend-plugin-api'; import { DefaultAuthService } from './DefaultAuthService'; import { - ExternalTokenHandler, + ExternalAuthTokenManager, externalTokenTypeHandlersRef, -} from './external/ExternalTokenHandler'; +} from './external/ExternalAuthTokenHandler'; import { DefaultPluginTokenHandler, PluginTokenHandler, @@ -107,7 +107,7 @@ export const authServiceFactory = createServiceFactory({ }), ); - const externalTokens = ExternalTokenHandler.create({ + const externalTokens = ExternalAuthTokenManager.create({ ownPluginId: plugin.getId(), config, logger, diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts index 3348d84e93..d97875d228 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts @@ -15,8 +15,9 @@ */ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; -import { ExternalTokenHandler } from './ExternalTokenHandler'; -import { TokenHandler } from './types'; +import { ExternalAuthTokenManager } from './ExternalAuthTokenHandler'; +import { createExternalTokenHandler } from './helpers'; +import { AccessRestrictionsMap, ExternalTokenHandler } from './types'; import { mockServices, registerMswTestHooks, @@ -84,25 +85,51 @@ describe('ExternalTokenHandler', () => { registerMswTestHooks(server); it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { - const handler1: TokenHandler = { + const handler1: ExternalTokenHandler = createExternalTokenHandler({ + type: 'type1', + initialize: jest.fn().mockResolvedValue(undefined), verifyToken: jest.fn().mockResolvedValue(undefined), - }; + }); - const handler2: TokenHandler = { - verifyToken: jest.fn().mockResolvedValue({ - subject: 'sub', - allAccessRestrictions: new Map( - Object.entries({ - plugin1: { - permissionNames: ['do.it'], - } satisfies BackstagePrincipalAccessRestrictions, - }), - ), + const handler2: ExternalTokenHandler = + createExternalTokenHandler({ + type: 'type2', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + }), + }); + + const accessRestrictions: AccessRestrictionsMap = new Map( + Object.entries({ + plugin1: { + permissionNames: ['do.it'], + } satisfies BackstagePrincipalAccessRestrictions, }), - }; + ); - const plugin1 = new ExternalTokenHandler('plugin1', [handler1, handler2]); - const plugin2 = new ExternalTokenHandler('plugin2', [handler1, handler2]); + const plugin1 = new ExternalAuthTokenManager('plugin1', [ + { + context: undefined, + handler: handler1, + }, + { + context: undefined, + handler: handler2, + allAccessRestrictions: accessRestrictions, + }, + ]); + const plugin2 = new ExternalAuthTokenManager('plugin2', [ + { + context: undefined, + handler: handler1, + }, + { + context: undefined, + handler: handler2, + allAccessRestrictions: accessRestrictions, + }, + ]); await expect(plugin1.verifyToken('token')).resolves.toEqual({ subject: 'sub', @@ -133,7 +160,7 @@ describe('ExternalTokenHandler', () => { ), ); - const handler = ExternalTokenHandler.create({ + const handler = ExternalAuthTokenManager.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ @@ -222,12 +249,10 @@ describe('ExternalTokenHandler', () => { ), ); - const customHandler: TokenHandler = { - verifyToken: jest.fn(), - }; - const customHandlerFactory = jest.fn(() => customHandler); + const verifyMock = jest.fn().mockResolvedValue({}); + const initializeMock = jest.fn().mockReturnValue({ context: 'a' }); - const handler = ExternalTokenHandler.create({ + const handler = ExternalAuthTokenManager.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ @@ -252,30 +277,23 @@ describe('ExternalTokenHandler', () => { }, }), externalTokenHandlers: [ - { + createExternalTokenHandler({ type: 'internal-custom', - factory: customHandlerFactory, - }, + initialize: initializeMock, + verifyToken: verifyMock, + }), ], }); - expect(customHandlerFactory).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - data: { - type: 'internal-custom', - options: { - issuer: 'my-company', - subject: 'internal-subject', - audience: 'backstage', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], - }, - }), - ]), - ); + expect(initializeMock).toHaveBeenCalledWith({ + options: expect.objectContaining({ + data: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + }), + }); const customToken = await factory.issueToken({ claims: { sub: 'internal-subject' }, @@ -283,11 +301,11 @@ describe('ExternalTokenHandler', () => { await handler.verifyToken(customToken); - expect(customHandler.verifyToken).toHaveBeenCalled(); + expect(verifyMock).toHaveBeenCalledWith(customToken, { context: 'a' }); }); it('should fail if config contains types not declared', async () => { const createHandler = () => - ExternalTokenHandler.create({ + ExternalAuthTokenManager.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ @@ -314,13 +332,13 @@ describe('ExternalTokenHandler', () => { }); expect(createHandler).toThrowErrorMatchingInlineSnapshot( - `"Unknown type(s) 'internal-custom' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks'"`, + `"Unknown type 'internal-custom' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks'"`, ); }); it('should show valid custom types in errors', async () => { const createHandler = () => - ExternalTokenHandler.create({ + ExternalAuthTokenManager.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ @@ -345,68 +363,18 @@ describe('ExternalTokenHandler', () => { }, }), externalTokenHandlers: [ - { + createExternalTokenHandler({ type: 'internal-custom', - factory: () => ({ - verifyToken: jest.fn(), + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', }), - }, + }), ], }); expect(createHandler).toThrowErrorMatchingInlineSnapshot( - `"Unknown type(s) 'internal-custom-invalid' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, - ); - }); - it('should show all invalid config types', async () => { - const createHandler = () => - ExternalTokenHandler.create({ - ownPluginId: 'catalog', - logger: mockServices.logger.mock(), - config: mockServices.rootConfig({ - data: { - backend: { - auth: { - externalAccess: [ - { - type: 'internal-custom-invalid', - options: { - issuer: 'my-company', - subject: 'internal-subject', - audience: 'backstage', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], - }, - { - type: 'internal-custom-invalid-2', - options: { - issuer: 'my-company', - subject: 'internal-subject', - audience: 'backstage', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], - }, - ], - }, - }, - }, - }), - externalTokenHandlers: [ - { - type: 'internal-custom', - factory: () => ({ - verifyToken: jest.fn(), - }), - }, - ], - }); - - expect(createHandler).toThrowErrorMatchingInlineSnapshot( - `"Unknown type(s) 'internal-custom-invalid, internal-custom-invalid-2' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, + `"Unknown type 'internal-custom-invalid' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts index c3fab70026..bf57ad42f4 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts @@ -21,85 +21,82 @@ import { RootConfigService, } from '@backstage/backend-plugin-api'; import { NotAllowedError } from '@backstage/errors'; -import { LegacyConfigWrapper, LegacyTokenHandler } from './legacy'; -import { StaticTokenHandler } from './static'; -import { JWKSHandler } from './jwks'; -import { TokenHandler } from './types'; -import { Config } from '@backstage/config'; -import { groupBy } from 'lodash'; -import { TokenTypeHandler } from './types'; +import { legacyTokenHandler } from './legacy'; +import { staticTokenHandler } from './static'; +import { jwksTokenHandler } from './jwks'; +import { AccessRestrictionsMap, ExternalTokenHandler } from './types'; +import { readAccessRestrictionsFromConfig } from './helpers'; const NEW_CONFIG_KEY = 'backend.auth.externalAccess'; const OLD_CONFIG_KEY = 'backend.auth.keys'; let loggedDeprecationWarning = false; -type LegacyTokenTypeHandler = { - type: 'legacy'; - /** - * A factory function that takes all token configuration for a given type - * and returns a TokenHandler or an array of TokenHandlers. - */ - factory: ( - configs: ( - | import('@backstage/config').Config - | { legacy: true; config: import('@backstage/config').Config } - )[], - ) => TokenHandler | TokenHandler[]; -}; - /** * @public * This service is used to decorate the default plugin token handler with custom logic. */ -export const externalTokenTypeHandlersRef = createServiceRef({ +export const externalTokenTypeHandlersRef = createServiceRef< + ExternalTokenHandler +>({ id: 'core.auth.externalTokenHandlers', multiton: true, // defaultFactory // :pepe-think: seems like is not possible to use defaultFactory with multiton }); -const defaultHandlers: (TokenTypeHandler | LegacyTokenTypeHandler)[] = [ - { - type: 'static', - factory: configs => new StaticTokenHandler(configs), - }, - { - type: 'legacy', - factory: (configs: (Config | { legacy: true; config: Config })[]) => - new LegacyTokenHandler(configs), - }, - { - type: 'jwks', - factory: configs => new JWKSHandler(configs), - }, +const defaultHandlers: ExternalTokenHandler[] = [ + staticTokenHandler, + legacyTokenHandler, + jwksTokenHandler, ]; +type ContextMapEntry = { + context: T; + handler: ExternalTokenHandler; + allAccessRestrictions?: AccessRestrictionsMap; +}; /** * Handles all types of external caller token types (i.e. not Backstage user * tokens, nor Backstage backend plugin tokens). * * @internal */ -export class ExternalTokenHandler { +export class ExternalAuthTokenManager { static create(options: { ownPluginId: string; config: RootConfigService; logger: LoggerService; - externalTokenHandlers?: TokenTypeHandler[]; - }): ExternalTokenHandler { + externalTokenHandlers?: ExternalTokenHandler[]; + }): ExternalAuthTokenManager { const { ownPluginId, config, - logger, externalTokenHandlers: customHandlers, } = options; const handlersTypes = [...defaultHandlers, ...(customHandlers ?? [])]; const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? []; - const handlerConfigByType: Record & { - legacy?: (Config | LegacyConfigWrapper)[]; - } = groupBy(handlerConfigs, (handlerConfig: Config) => - handlerConfig.getString('type'), + const contexts: ContextMapEntry[] = handlerConfigs.map( + handlerConfig => { + const type = handlerConfig.getString('type'); + + const handler = handlersTypes.find(h => h.type === type); + if (!handler) { + const valid = handlersTypes.map(h => `'${h.type}'`).join(', '); + throw new Error( + `Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, + ); + } + return { + context: handler.initialize({ + options: handlerConfig.getConfig('options'), + }), + handler, + allAccessRestrictions: handlerConfig + ? readAccessRestrictionsFromConfig(handlerConfig) + : undefined, + }; + }, ); // Load the old keys too @@ -107,45 +104,22 @@ export class ExternalTokenHandler { if (legacyConfigs.length && !loggedDeprecationWarning) { loggedDeprecationWarning = true; - logger.warn( - `DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`, - ); - } - for (const handlerConfig of legacyConfigs) { - handlerConfigByType.legacy ??= []; - handlerConfigByType.legacy.push({ legacy: true, config: handlerConfig }); - } - - const invalidTypes = Object.keys(handlerConfigByType).filter( - type => !handlersTypes.some(handler => handler.type === type), - ); - - if (invalidTypes.length > 0) { - const valid = handlersTypes - .map(handler => `'${handler.type}'`) - .join(', '); + // :pepe-think: this message was here for more than a year, and replacing this simplifies things a lot throw new Error( - `Unknown type(s) '${invalidTypes.join( - ', ', - )}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, + `The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`, ); } - const handlers = handlersTypes.flatMap(handler => { - const configs = handlerConfigByType[handler.type] ?? []; - const handlerInstances = handler.factory(configs); - if (Array.isArray(handlerInstances)) { - return handlerInstances; - } - return [handlerInstances]; - }); - - return new ExternalTokenHandler(ownPluginId, handlers); + return new ExternalAuthTokenManager(ownPluginId, contexts); } constructor( private readonly ownPluginId: string, - private readonly handlers: TokenHandler[], + private readonly contexts: { + context: unknown; + handler: ExternalTokenHandler; + allAccessRestrictions?: AccessRestrictionsMap; + }[], ) {} async verifyToken(token: string): Promise< @@ -155,10 +129,9 @@ export class ExternalTokenHandler { } | undefined > { - for (const handler of this.handlers) { - const result = await handler.verifyToken(token); + for (const { handler, allAccessRestrictions, context } of this.contexts) { + const result = await handler.verifyToken(token, context); if (result) { - const { allAccessRestrictions, ...rest } = result; if (allAccessRestrictions) { const accessRestrictions = allAccessRestrictions.get( this.ownPluginId, @@ -173,12 +146,12 @@ export class ExternalTokenHandler { } return { - ...rest, + ...result, accessRestrictions, }; } - return rest; + return result; } } diff --git a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts index 4a01117e28..296ec2e72b 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { AccessRestrictionsMap } from './types'; +import { AccessRestrictionsMap, ExternalTokenHandler } from './types'; /** * Parses and returns the `accessRestrictions` configuration from an @@ -147,3 +147,9 @@ function readPermissionAttributes(externalAccessEntryConfig: Config) { return Object.keys(result).length ? result : undefined; } + +export function createExternalTokenHandler( + handler: ExternalTokenHandler, +): ExternalTokenHandler { + return handler; +} diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts index eefbce5093..778e5090bf 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts @@ -20,7 +20,7 @@ import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { v4 as uuid } from 'uuid'; -import { JWKSHandler } from './jwks'; +import { jwksTokenHandler } from './jwks'; // Simplified copy of TokenFactory in @backstage/plugin-auth-backend interface AnyJWK extends Record { @@ -101,108 +101,45 @@ describe('JWKSHandler', () => { it('verifies token with valid entry', async () => { const validEntry = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: mockBaseUrl, - audience: 'backstage', - }, + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: 'RS256', + issuer: mockBaseUrl, + audience: 'backstage', }; - const jwksHandler = new JWKSHandler([new ConfigReader(validEntry)]); + const context = jwksTokenHandler.initialize({ + options: new ConfigReader(validEntry), + }); const token = await factory.issueToken({ claims: { sub: mockSubject }, }); - const result = await jwksHandler.verifyToken(token); + const result = await jwksTokenHandler.verifyToken(token, context); expect(result).toEqual({ subject: `external:${mockSubject}` }); }); - it('skips invalid entry and continues verification', async () => { - const invalidEntry = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: ['fakeIssuer'], - audience: ['fakeAud'], - }, - }; - - const validEntry = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: ['multiple-issuers', mockBaseUrl], - audience: ['multiple-audiences', 'backstage'], - }, - }; - const jwksHandler = new JWKSHandler([ - new ConfigReader(invalidEntry), - new ConfigReader(validEntry), - ]); - - const token = await factory.issueToken({ - claims: { sub: mockSubject }, - }); - - const result = await jwksHandler.verifyToken(token); - - expect(result).toEqual({ subject: `external:${mockSubject}` }); - }); - - it('returns undefined if no valid entry found', async () => { - const invalidEntry1 = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: 'wrong', - }, - }; - - const invalidEntry2 = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: ['HS256'], - audience: 'wrong', - }, - }; - const jwksHandler = new JWKSHandler([ - new ConfigReader(invalidEntry1), - new ConfigReader(invalidEntry2), - ]); - - const token = await factory.issueToken({ - claims: { sub: mockSubject }, - }); - - const result = await jwksHandler.verifyToken(token); - - expect(result).toBeUndefined(); - }); - it('rejects bad config', () => { expect(() => { - return new JWKSHandler([ - new ConfigReader({ - options: { url: 'https://exampl e.com/jwks' }, + return jwksTokenHandler.initialize({ + options: new ConfigReader({ + url: 'https://exampl e.com/jwks', }), - ]); + }); }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); expect(() => { - return new JWKSHandler([ - new ConfigReader({ - options: { - url: 'https://example.com/jwks\n', - }, + return jwksTokenHandler.initialize({ + options: new ConfigReader({ + url: 'https://example.com/jwks\n', }), - ]); + }); }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); }); it('gracefully handles no added tokens', async () => { - const handler = new JWKSHandler([]); - await expect(handler.verifyToken('ghi')).resolves.toBeUndefined(); + await expect( + jwksTokenHandler.verifyToken('ghi', {} as any), + ).resolves.toBeUndefined(); }); it('uses custom subject prefix if provided', async () => { @@ -215,38 +152,18 @@ describe('JWKSHandler', () => { subjectPrefix: 'custom-prefix', }, }; - const jwksHandler = new JWKSHandler([new ConfigReader(validEntry)]); + const context = jwksTokenHandler.initialize({ + options: new ConfigReader(validEntry.options), + }); const token = await factory.issueToken({ claims: { sub: mockSubject }, }); - const result = await jwksHandler.verifyToken(token); + const result = await jwksTokenHandler.verifyToken(token, context); expect(result).toEqual({ subject: `external:${validEntry.options.subjectPrefix}:${mockSubject}`, }); }); - - it('carries over access restrictions', async () => { - const jwksHandler = new JWKSHandler([ - new ConfigReader({ - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - }, - accessRestrictions: [{ plugin: 'scaffolder', permission: 'do.it' }], - }), - ]); - - const token = await factory.issueToken({ claims: { sub: mockSubject } }); - - await expect(jwksHandler.verifyToken(token)).resolves.toEqual({ - subject: `external:${mockSubject}`, - allAccessRestrictions: new Map( - Object.entries({ - scaffolder: { permissionNames: ['do.it'] }, - }), - ), - }); - }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts index 5f7fcb4127..892fcf2181 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts @@ -15,56 +15,40 @@ */ import { jwtVerify, createRemoteJWKSet, JWTVerifyGetKey } from 'jose'; -import { Config } from '@backstage/config'; import { + createExternalTokenHandler, readAccessRestrictionsFromConfig, readStringOrStringArrayFromConfig, } from './helpers'; -import { AccessRestrictionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap } from './types'; -/** - * Handles `type: jwks` access. - * - * @internal - */ -export class JWKSHandler implements TokenHandler { - #entries: Array<{ - algorithms?: string[]; - audiences?: string[]; - issuers?: string[]; - subjectPrefix?: string; - url: URL; - jwks: JWTVerifyGetKey; - allAccessRestrictions?: AccessRestrictionsMap; - }> = []; +type JWKSTokenContext = { + algorithms?: string[]; + audiences?: string[]; + issuers?: string[]; + subjectPrefix?: string; + url: URL; + jwks: JWTVerifyGetKey; + allAccessRestrictions?: AccessRestrictionsMap; +}; - constructor(configs: Config[]) { - for (const config of configs) { - this.add(config); - } - } - private add(config: Config) { - if (!config.getString('options.url').match(/^\S+$/)) { +export const jwksTokenHandler = createExternalTokenHandler({ + type: 'jwks', + initialize({ options }): JWKSTokenContext { + if (!options.getString('url').match(/^\S+$/)) { throw new Error( 'Illegal JWKS URL, must be a set of non-space characters', ); } - const algorithms = readStringOrStringArrayFromConfig( - config, - 'options.algorithm', - ); - const issuers = readStringOrStringArrayFromConfig(config, 'options.issuer'); - const audiences = readStringOrStringArrayFromConfig( - config, - 'options.audience', - ); - const subjectPrefix = config.getOptionalString('options.subjectPrefix'); - const url = new URL(config.getString('options.url')); + const algorithms = readStringOrStringArrayFromConfig(options, 'algorithm'); + const issuers = readStringOrStringArrayFromConfig(options, 'issuer'); + const audiences = readStringOrStringArrayFromConfig(options, 'audience'); + const subjectPrefix = options.getOptionalString('subjectPrefix'); + const url = new URL(options.getString('url')); const jwks = createRemoteJWKSet(url); - const allAccessRestrictions = readAccessRestrictionsFromConfig(config); - - this.#entries.push({ + const allAccessRestrictions = readAccessRestrictionsFromConfig(options); + return { algorithms, audiences, issuers, @@ -72,34 +56,31 @@ export class JWKSHandler implements TokenHandler { subjectPrefix, url, allAccessRestrictions, - }); - return this; - } + }; + }, - async verifyToken(token: string) { - for (const entry of this.#entries) { - try { - const { - payload: { sub }, - } = await jwtVerify(token, entry.jwks, { - algorithms: entry.algorithms, - issuer: entry.issuers, - audience: entry.audiences, - }); + async verifyToken(token: string, context: JWKSTokenContext) { + try { + const { + payload: { sub }, + } = await jwtVerify(token, context.jwks, { + algorithms: context.algorithms, + issuer: context.issuers, + audience: context.audiences, + }); - if (sub) { - const prefix = entry.subjectPrefix - ? `external:${entry.subjectPrefix}:` - : 'external:'; - return { - subject: `${prefix}${sub}`, - allAccessRestrictions: entry.allAccessRestrictions, - }; - } - } catch { - continue; + if (sub) { + const prefix = context.subjectPrefix + ? `external:${context.subjectPrefix}:` + : 'external:'; + return { + subject: `${prefix}${sub}`, + allAccessRestrictions: context.allAccessRestrictions, + }; } + } catch { + return undefined; } return undefined; - } -} + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts index 4892d0a21a..43160b46da 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts @@ -18,49 +18,27 @@ import { ConfigReader } from '@backstage/config'; import { randomBytes } from 'crypto'; import { SignJWT, importJWK } from 'jose'; import { DateTime } from 'luxon'; -import { LegacyTokenHandler } from './legacy'; +import { legacyTokenHandler } from './legacy'; describe('LegacyTokenHandler', () => { const key1 = randomBytes(24); const key2 = randomBytes(24); - const key3 = randomBytes(24); - const accessRestrictions1 = new Map( - Object.entries({ - scaffolder: {}, - }), - ); - const accessRestrictions2 = new Map( - Object.entries({ - catalog: { permissionNames: ['catalog.entity.read'] }, - }), - ); - const configs = [ - new ConfigReader({ - options: { - secret: key1.toString('base64'), - subject: 'key1', - }, - accessRestrictions: [{ plugin: 'scaffolder' }], - }), + const tokenHandler = legacyTokenHandler; - new ConfigReader({ - options: { - secret: key2.toString('base64'), - subject: 'key2', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], + const context1 = tokenHandler.initialize({ + options: new ConfigReader({ + secret: key1.toString('base64'), + subject: 'key1', }), - { - legacy: true, - config: new ConfigReader({ - secret: key3.toString('base64'), - }), - }, - ]; - const tokenHandler = new LegacyTokenHandler(configs); + }); + + const context2 = tokenHandler.initialize({ + options: new ConfigReader({ + secret: key2.toString('base64'), + subject: 'key2', + }), + }); it('should verify valid tokens', async () => { const token1 = await new SignJWT({ @@ -70,9 +48,8 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key1); - await expect(tokenHandler.verifyToken(token1)).resolves.toEqual({ + await expect(tokenHandler.verifyToken(token1, context1)).resolves.toEqual({ subject: 'key1', - allAccessRestrictions: accessRestrictions1, }); const token2 = await new SignJWT({ @@ -82,20 +59,8 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key2); - await expect(tokenHandler.verifyToken(token2)).resolves.toEqual({ + await expect(tokenHandler.verifyToken(token2, context2)).resolves.toEqual({ subject: 'key2', - allAccessRestrictions: accessRestrictions2, - }); - - const token3 = await new SignJWT({ - sub: 'backstage-server', - exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(), - }) - .setProtectedHeader({ alg: 'HS256' }) - .sign(key3); - - await expect(tokenHandler.verifyToken(token3)).resolves.toEqual({ - subject: 'external:backstage-plugin', }); }); @@ -107,10 +72,18 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key1); - await expect(tokenHandler.verifyToken(validToken)).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(validToken, context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(validToken, context2), + ).resolves.toBeUndefined(); await expect( - tokenHandler.verifyToken('statickeyblaaa'), + tokenHandler.verifyToken('statickeyblaaa', context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken('statickeyblaaa', context2), ).resolves.toBeUndefined(); const randomToken = await new SignJWT({ @@ -120,7 +93,10 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(randomBytes(24)); await expect( - tokenHandler.verifyToken(randomToken), + tokenHandler.verifyToken(randomToken, context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(randomToken, context2), ).resolves.toBeUndefined(); const mockPublicKey = { @@ -144,7 +120,10 @@ describe('LegacyTokenHandler', () => { .sign(await importJWK(mockPrivateKey)); await expect( - tokenHandler.verifyToken(keyWithWrongAlg), + tokenHandler.verifyToken(keyWithWrongAlg, context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(keyWithWrongAlg, context2), ).resolves.toBeUndefined(); }); @@ -157,151 +136,134 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key1); - await expect(tokenHandler.verifyToken(keyWithWrongExp)).rejects.toThrow( - /\"exp\" claim must be a number/, - ); + await expect( + tokenHandler.verifyToken(keyWithWrongExp, context1), + ).rejects.toThrow(/\"exp\" claim must be a number/); }); it('rejects bad config', () => { - const handler = new LegacyTokenHandler([]); + const handler = legacyTokenHandler; // new style add, bad secrets - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ - options: { _missingsecret: true, subject: 'ok' }, - }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.secret' in 'mock-config'"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ options: { secret: '', subject: 'ok' } }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.secret' in 'mock-config', got empty-string, wanted string"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ - options: { secret: 'has spaces', subject: 'ok' }, - }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal secret, must be a valid base64 string"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ - options: { secret: 'hasnewline\n', subject: 'ok' }, - }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal secret, must be a valid base64 string"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ options: { secret: 3, subject: 'ok' } }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.secret' in 'mock-config', got number, wanted string"`, - ); - - // new style add, bad subjects - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ - options: { secret: 'b2s=', _missingsubject: true }, - }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.subject' in 'mock-config'"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ options: { secret: 'b2s=', subject: '' } }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ - options: { secret: 'b2s=', subject: 'has spaces' }, - }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal subject, must be a set of non-space characters"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ - options: { secret: 'b2s=', subject: 'hasnewline\n' }, - }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal subject, must be a set of non-space characters"`, - ); - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ options: { secret: 'b2s=', subject: 3 } }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, - ); - - // new style add, bad access restrictions - expect( - () => - new LegacyTokenHandler([ - new ConfigReader({ - options: { secret: 'b2s=', subject: 'subject' }, - accessRestrictions: [{ plugin: ['a'] }], - }), - ]), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'accessRestrictions[0].plugin' in 'mock-config', got array, wanted string"`, - ); - - // old style add expect(() => - handler.addOld(new ConfigReader({ secret: 'b2s=' })), - ).not.toThrow(); - expect(() => - handler.addOld(new ConfigReader({ _missingsecret: true })), + handler.initialize({ + options: new ConfigReader({ + _missingsecret: true, + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Missing required config value at 'secret' in 'mock-config'"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: '' })), + handler.initialize({ + options: new ConfigReader({ secret: '', subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'secret' in 'mock-config', got empty-string, wanted string"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: 'has spaces' })), + handler.initialize({ + options: new ConfigReader({ + secret: 'has spaces', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal secret, must be a valid base64 string"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: 'hasnewline\n' })), + handler.initialize({ + options: new ConfigReader({ + secret: 'hasnewline\n', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal secret, must be a valid base64 string"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: 3 })), + handler.initialize({ + options: new ConfigReader({ secret: 3, subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'secret' in 'mock-config', got number, wanted string"`, ); + + // new style add, bad subjects + expect(() => + handler.initialize({ + options: new ConfigReader({ + secret: 'b2s=', + _missingsubject: true, + }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required config value at 'subject' in 'mock-config'"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'b2s=', subject: '' }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'subject' in 'mock-config', got empty-string, wanted string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ + secret: 'b2s=', + subject: 'has spaces', + }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal subject, must be a set of non-space characters"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ + secret: 'b2s=', + subject: 'hasnewline\n', + }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal subject, must be a set of non-space characters"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'b2s=', subject: 3 }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`, + ); + + // // old style add + // expect(() => + // handler.addOld(new ConfigReader({ secret: 'b2s=' })), + // ).not.toThrow(); + // expect(() => + // handler.addOld(new ConfigReader({ _missingsecret: true })), + // ).toThrowErrorMatchingInlineSnapshot( + // `"Missing required config value at 'secret' in 'mock-config'"`, + // ); + // expect(() => + // handler.addOld(new ConfigReader({ secret: '' })), + // ).toThrowErrorMatchingInlineSnapshot( + // `"Invalid type in config for key 'secret' in 'mock-config', got empty-string, wanted string"`, + // ); + // expect(() => + // handler.addOld(new ConfigReader({ secret: 'has spaces' })), + // ).toThrowErrorMatchingInlineSnapshot( + // `"Illegal secret, must be a valid base64 string"`, + // ); + // expect(() => + // handler.addOld(new ConfigReader({ secret: 'hasnewline\n' })), + // ).toThrowErrorMatchingInlineSnapshot( + // `"Illegal secret, must be a valid base64 string"`, + // ); + // expect(() => + // handler.addOld(new ConfigReader({ secret: 3 })), + // ).toThrowErrorMatchingInlineSnapshot( + // `"Invalid type in config for key 'secret' in 'mock-config', got number, wanted string"`, + // ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts index 8be0de19e4..f9932a8735 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts @@ -16,126 +16,78 @@ import { Config } from '@backstage/config'; import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; -import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestrictionsMap, TokenHandler } from './types'; + +import { createExternalTokenHandler } from './helpers'; export type LegacyConfigWrapper = { legacy: boolean; config: Config; }; -/** - * Handles `type: legacy` access. - * - * @internal - */ -export class LegacyTokenHandler implements TokenHandler { - #entries = new Array<{ - key: Uint8Array; - result: { - subject: string; - allAccessRestrictions?: AccessRestrictionsMap; - }; - }>(); - constructor(configs: (Config | LegacyConfigWrapper)[]) { - for (const config of configs) { - if (isLegacy(config)) { - this.addOld(config.config); - continue; +type LegacyTokenHandlerContext = { + key: Uint8Array; + + subject: string; +}; + +export const legacyTokenHandler = + createExternalTokenHandler({ + type: 'legacy', + initialize(ctx: { options: Config }): LegacyTokenHandlerContext { + const secret = ctx.options.getString('secret'); + const subject = ctx.options.getString('subject'); + + if (!secret.match(/^\S+$/)) { + throw new Error('Illegal secret, must be a valid base64 string'); + } else if (!subject.match(/^\S+$/)) { + throw new Error( + 'Illegal subject, must be a set of non-space characters', + ); } - this.add(config); - } - } - add(config: Config) { - const allAccessRestrictions = readAccessRestrictionsFromConfig(config); - this.#doAdd( - config.getString('options.secret'), - config.getString('options.subject'), - allAccessRestrictions, - ); - return this; - } - - // used only for the old backend.auth.keys array - addOld(config: Config) { - // This choice of subject is for compatibility reasons - this.#doAdd(config.getString('secret'), 'external:backstage-plugin'); - } - - #doAdd( - secret: string, - subject: string, - allAccessRestrictions?: AccessRestrictionsMap, - ) { - if (!secret.match(/^\S+$/)) { - throw new Error('Illegal secret, must be a valid base64 string'); - } else if (!subject.match(/^\S+$/)) { - throw new Error('Illegal subject, must be a set of non-space characters'); - } - - let key: Uint8Array; - try { - key = base64url.decode(secret); - } catch { - throw new Error('Illegal secret, must be a valid base64 string'); - } - - if (this.#entries.some(e => e.key === key)) { - throw new Error( - 'Legacy externalAccess token was declared more than once', - ); - } - - this.#entries.push({ - key, - result: { - subject, - allAccessRestrictions, - }, - }); - } - - async verifyToken(token: string) { - // First do a duck typing check to see if it remotely looks like a legacy token - try { - // We do a fair amount of checking upfront here. Since we aren't certain - // that it's even the right type of key that we're looking at, we can't - // defer eg the alg check to jwtVerify, because it won't be possible to - // discern different reasons for key verification failures from each other - // easily - const { alg } = decodeProtectedHeader(token); - if (alg !== 'HS256') { - return undefined; - } - const { sub, aud } = decodeJwt(token); - if (sub !== 'backstage-server' || aud) { - return undefined; - } - } catch (e) { - // Doesn't look like a jwt at all - return undefined; - } - - for (const { key, result } of this.#entries) { try { - await jwtVerify(token, key); - return result; - } catch (e) { - if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { - throw e; - } - // Otherwise continue to try the next key + return { + key: base64url.decode(secret), + subject, + }; + } catch { + throw new Error('Illegal secret, must be a valid base64 string'); } - } + }, - // None of the signing keys matched - return undefined; - } -} + async verifyToken(token: string, context: LegacyTokenHandlerContext) { + // First do a duck typing check to see if it remotely looks like a legacy token + try { + // We do a fair amount of checking upfront here. Since we aren't certain + // that it's even the right type of key that we're looking at, we can't + // defer eg the alg check to jwtVerify, because it won't be possible to + // discern different reasons for key verification failures from each other + // easily + const { alg } = decodeProtectedHeader(token); + if (alg !== 'HS256') { + return undefined; + } + const { sub, aud } = decodeJwt(token); + if (sub !== 'backstage-server' || aud) { + return undefined; + } + } catch (e) { + // Doesn't look like a jwt at all + return undefined; + } -function isLegacy( - config: Config | LegacyConfigWrapper, -): config is LegacyConfigWrapper { - return (config as LegacyConfigWrapper).legacy === true; -} + try { + await jwtVerify(token, context.key); + return { + subject: context.subject, + }; + } catch (error) { + if (error.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { + throw error; + } + } + + // None of the signing keys matched + return undefined; + }, + }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts index cf1c1a46ac..10e78d2dbf 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts @@ -15,148 +15,146 @@ */ import { ConfigReader } from '@backstage/config'; -import { StaticTokenHandler } from './static'; +import { staticTokenHandler } from './static'; describe('StaticTokenHandler', () => { it('accepts any of the added list of tokens', async () => { - const configs = [ - new ConfigReader({ - options: { token: 'abcabcabc', subject: 'one' }, - accessRestrictions: [{ plugin: 'scaffolder' }], + const context1 = staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'abcabcabc', + subject: 'one', }), - new ConfigReader({ - options: { token: 'defdefdef', subject: 'two' }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], + }); + const context2 = staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'defdefdef', + subject: 'two', }), - ]; - const handler = new StaticTokenHandler(configs); - const accessRestrictionsOne = new Map(Object.entries({ scaffolder: {} })); - const accessRestrictionsTwo = new Map( - Object.entries({ - catalog: { - permissionNames: ['catalog.entity.read'], - }, - }), - ); + }); - await expect(handler.verifyToken('abcabcabc')).resolves.toEqual({ + await expect( + staticTokenHandler.verifyToken('abcabcabc', context1), + ).resolves.toEqual({ subject: 'one', - allAccessRestrictions: accessRestrictionsOne, }); - await expect(handler.verifyToken('defdefdef')).resolves.toEqual({ + await expect( + staticTokenHandler.verifyToken('defdefdef', context2), + ).resolves.toEqual({ subject: 'two', - allAccessRestrictions: accessRestrictionsTwo, }); - await expect(handler.verifyToken('ghighighi')).resolves.toBeUndefined(); + await expect( + staticTokenHandler.verifyToken('ghighighi', context1), + ).resolves.toBeUndefined(); }); it('gracefully handles no added tokens', async () => { - const handler = new StaticTokenHandler([]); - await expect(handler.verifyToken('ghi')).resolves.toBeUndefined(); + await expect( + staticTokenHandler.verifyToken('ghi', {} as any), + ).resolves.toBeUndefined(); }); it('rejects bad config', () => { - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ options: { _missingtoken: true, subject: 'ok' } }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ _missingtoken: true, subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.token' in 'mock-config'"`, + `"Missing required config value at 'token' in 'mock-config'"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ options: { token: '', subject: 'ok' } }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ token: '', subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.token' in 'mock-config', got empty-string, wanted string"`, + `"Invalid type in config for key 'token' in 'mock-config', got empty-string, wanted string"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ options: { token: 'has spaces', subject: 'ok' } }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'has spaces', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be a set of non-space characters"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ - options: { - token: 'hasnewlinebutislongenough\n', - subject: 'ok', - }, - }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'hasnewlinebutislongenough\n', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be a set of non-space characters"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ options: { token: 'short', subject: 'ok' } }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'short', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be at least 8 characters length"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ options: { token: 3, subject: 'ok' } }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ token: 3, subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.token' in 'mock-config', got number, wanted string"`, + `"Invalid type in config for key 'token' in 'mock-config', got number, wanted string"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ - options: { token: 'validtoken', _missingsubject: true }, - }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + _missingsubject: true, + }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.subject' in 'mock-config'"`, + `"Missing required config value at 'subject' in 'mock-config'"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ options: { token: 'validtoken', subject: '' } }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: '', + }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, + `"Invalid type in config for key 'subject' in 'mock-config', got empty-string, wanted string"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ - options: { token: 'validtoken', subject: 'has spaces' }, - }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: 'has spaces', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ - options: { token: 'validtoken', subject: 'hasnewline\n' }, - }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: 'hasnewline\n', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); - expect( - () => - new StaticTokenHandler([ - new ConfigReader({ options: { token: 'validtoken', subject: 3 } }), - ]), + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: 3, + }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, + `"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`, ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts index 7764569aca..fe7b9995a4 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.ts @@ -15,34 +15,18 @@ */ import { Config } from '@backstage/config'; -import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestrictionsMap, TokenHandler } from './types'; +import { createExternalTokenHandler } from './helpers'; const MIN_TOKEN_LENGTH = 8; -/** - * Handles `type: static` access. - * - * @internal - */ -export class StaticTokenHandler implements TokenHandler { - #entries = new Map< - string, - { - subject: string; - allAccessRestrictions?: AccessRestrictionsMap; - } - >(); - - constructor(configs: Config[]) { - for (const config of configs) { - this.add(config); - } - } - private add(config: Config) { - const token = config.getString('options.token'); - const subject = config.getString('options.subject'); - const allAccessRestrictions = readAccessRestrictionsFromConfig(config); +export const staticTokenHandler = createExternalTokenHandler<{ + token: string; + subject: string; +}>({ + type: 'static', + initialize(ctx: { options: Config }): { token: string; subject: string } { + const token = ctx.options.getString('token'); + const subject = ctx.options.getString('subject'); if (!token.match(/^\S+$/)) { throw new Error('Illegal token, must be a set of non-space characters'); @@ -52,17 +36,64 @@ export class StaticTokenHandler implements TokenHandler { ); } else if (!subject.match(/^\S+$/)) { throw new Error('Illegal subject, must be a set of non-space characters'); - } else if (this.#entries.has(token)) { - throw new Error( - 'Static externalAccess token was declared more than once', - ); } - this.#entries.set(token, { subject, allAccessRestrictions }); - return this; - } + return { token, subject }; + }, + async verifyToken( + token: string, + context: { token: string; subject: string }, + ) { + if (token === context.token) { + return { subject: context.subject }; + } + return undefined; + }, +}); - async verifyToken(token: string) { - return this.#entries.get(token); - } -} +/** + * Handles `type: static` access. + * + * @internal + */ +// export class StaticTokenHandler implements TokenHandler { +// #entries = new Map< +// string, +// { +// subject: string; +// allAccessRestrictions?: AccessRestrictionsMap; +// } +// >(); + +// constructor(configs: Config[]) { +// for (const config of configs) { +// this.add(config); +// } +// } +// private add(config: Config) { +// const token = config.getString('options.token'); +// const subject = config.getString('options.subject'); +// const allAccessRestrictions = readAccessRestrictionsFromConfig(config); + +// if (!token.match(/^\S+$/)) { +// throw new Error('Illegal token, must be a set of non-space characters'); +// } else if (token.length < MIN_TOKEN_LENGTH) { +// throw new Error( +// `Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`, +// ); +// } else if (!subject.match(/^\S+$/)) { +// throw new Error('Illegal subject, must be a set of non-space characters'); +// } else if (this.#entries.has(token)) { +// throw new Error( +// 'Static externalAccess token was declared more than once', +// ); +// } + +// this.#entries.set(token, { subject, allAccessRestrictions }); +// return this; +// } + +// async verifyToken(token: string) { +// return this.#entries.get(token); +// } +// } diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index 5e26b66da5..6ce1ca13b6 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -15,6 +15,7 @@ */ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; /** * @public @@ -29,27 +30,24 @@ export type AccessRestrictionsMap = Map< * This interface is used to handle external tokens. * It is used by the auth service to verify tokens and extract the subject. */ -export interface TokenHandler { - verifyToken(token: string): Promise< - | { - subject: string; - allAccessRestrictions?: AccessRestrictionsMap; - } - | undefined - >; +export interface ExternalTokenHandler { + type: string; + initialize(ctx: { options: Config }): TContext; + verifyToken( + token: string, + ctx: TContext, + ): Promise<{ subject: string } | undefined>; } -/** - * @public - * This interface is used to handle external tokens. - */ -export interface TokenTypeHandler { - type: string; - /** - * A factory function that takes all token configuration for a given type - * and returns a TokenHandler or an array of TokenHandlers. - */ - factory: ( - configs: import('@backstage/config').Config[], - ) => TokenHandler | TokenHandler[]; -} +// /** +// * @public +// * This interface is used to handle external tokens. +// */ +// export interface TokenTypeHandler { +// type: string; +// /** +// * A factory function that takes all token configuration for a given type +// * and returns a TokenHandler or an array of TokenHandlers. +// */ +// factory: (configs: Config[]) => TokenHandler | TokenHandler[]; +// } diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index 335fc2f62c..e205c8b566 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -19,9 +19,11 @@ export { pluginTokenHandlerDecoratorServiceRef, } from './authServiceFactory'; -export { externalTokenTypeHandlersRef } from './external/ExternalTokenHandler'; +export { createExternalTokenHandler } from './external/helpers'; -export type { TokenHandler, TokenTypeHandler } from './external/types'; +export { externalTokenTypeHandlersRef } from './external/ExternalAuthTokenHandler'; + +export type { ExternalTokenHandler } from './external/types'; export type { AccessRestrictionsMap } from './external/types'; export type { PluginTokenHandler } from './plugin/PluginTokenHandler'; From 6361c0c00fe8716c5b9c2635dbd366aa49f8fb86 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Sat, 6 Sep 2025 19:11:23 +0200 Subject: [PATCH 029/193] fix: rename the Internal token handler Signed-off-by: Juan Pablo Garcia Ripa --- .../src/entrypoints/auth/DefaultAuthService.ts | 4 ++-- .../src/entrypoints/auth/authServiceFactory.ts | 4 ++-- .../auth/external/ExternalAuthTokenHandler.test.ts | 14 +++++++------- .../auth/external/ExternalAuthTokenHandler.ts | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts index c2c4a064a3..dba4f70dec 100644 --- a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts +++ b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts @@ -25,7 +25,7 @@ import { import { AuthenticationError } from '@backstage/errors'; import { JsonObject } from '@backstage/types'; import { decodeJwt } from 'jose'; -import { ExternalAuthTokenManager } from './external/export class ExternalAuthTokenManager {'; +import { ExternalAuthTokenHandler } from './external/ExternalAuthTokenHandler'; import { createCredentialsWithNonePrincipal, createCredentialsWithServicePrincipal, @@ -41,7 +41,7 @@ export class DefaultAuthService implements AuthService { constructor( private readonly userTokenHandler: UserTokenHandler, private readonly pluginTokenHandler: PluginTokenHandler, - private readonly externalTokenHandler: ExternalAuthTokenManager, + private readonly externalTokenHandler: ExternalAuthTokenHandler, private readonly pluginId: string, private readonly disableDefaultAuthPolicy: boolean, private readonly pluginKeySource: PluginKeySource, diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index 34fc3b51ef..0f8c5ff75d 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; import { DefaultAuthService } from './DefaultAuthService'; import { - ExternalAuthTokenManager, + ExternalAuthTokenHandler, externalTokenTypeHandlersRef, } from './external/ExternalAuthTokenHandler'; import { @@ -107,7 +107,7 @@ export const authServiceFactory = createServiceFactory({ }), ); - const externalTokens = ExternalAuthTokenManager.create({ + const externalTokens = ExternalAuthTokenHandler.create({ ownPluginId: plugin.getId(), config, logger, diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts index d97875d228..491de30ac4 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts @@ -15,7 +15,7 @@ */ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; -import { ExternalAuthTokenManager } from './ExternalAuthTokenHandler'; +import { ExternalAuthTokenHandler } from './ExternalAuthTokenHandler'; import { createExternalTokenHandler } from './helpers'; import { AccessRestrictionsMap, ExternalTokenHandler } from './types'; import { @@ -108,7 +108,7 @@ describe('ExternalTokenHandler', () => { }), ); - const plugin1 = new ExternalAuthTokenManager('plugin1', [ + const plugin1 = new ExternalAuthTokenHandler('plugin1', [ { context: undefined, handler: handler1, @@ -119,7 +119,7 @@ describe('ExternalTokenHandler', () => { allAccessRestrictions: accessRestrictions, }, ]); - const plugin2 = new ExternalAuthTokenManager('plugin2', [ + const plugin2 = new ExternalAuthTokenHandler('plugin2', [ { context: undefined, handler: handler1, @@ -160,7 +160,7 @@ describe('ExternalTokenHandler', () => { ), ); - const handler = ExternalAuthTokenManager.create({ + const handler = ExternalAuthTokenHandler.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ @@ -252,7 +252,7 @@ describe('ExternalTokenHandler', () => { const verifyMock = jest.fn().mockResolvedValue({}); const initializeMock = jest.fn().mockReturnValue({ context: 'a' }); - const handler = ExternalAuthTokenManager.create({ + const handler = ExternalAuthTokenHandler.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ @@ -305,7 +305,7 @@ describe('ExternalTokenHandler', () => { }); it('should fail if config contains types not declared', async () => { const createHandler = () => - ExternalAuthTokenManager.create({ + ExternalAuthTokenHandler.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ @@ -338,7 +338,7 @@ describe('ExternalTokenHandler', () => { it('should show valid custom types in errors', async () => { const createHandler = () => - ExternalAuthTokenManager.create({ + ExternalAuthTokenHandler.create({ ownPluginId: 'catalog', logger: mockServices.logger.mock(), config: mockServices.rootConfig({ diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts index bf57ad42f4..17541ff384 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts @@ -60,13 +60,13 @@ type ContextMapEntry = { * * @internal */ -export class ExternalAuthTokenManager { +export class ExternalAuthTokenHandler { static create(options: { ownPluginId: string; config: RootConfigService; logger: LoggerService; externalTokenHandlers?: ExternalTokenHandler[]; - }): ExternalAuthTokenManager { + }): ExternalAuthTokenHandler { const { ownPluginId, config, @@ -110,7 +110,7 @@ export class ExternalAuthTokenManager { ); } - return new ExternalAuthTokenManager(ownPluginId, contexts); + return new ExternalAuthTokenHandler(ownPluginId, contexts); } constructor( From 968b2f606ed3062f4bb08fd37b51c07e83019aea Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Sat, 6 Sep 2025 19:48:23 +0200 Subject: [PATCH 030/193] fix: api-reports Signed-off-by: Juan Pablo Garcia Ripa --- packages/backend-defaults/report-auth.api.md | 46 ++++++++++--------- .../src/entrypoints/auth/external/helpers.ts | 28 +++++++++++ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 176bdd16de..0164fa13c1 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -5,7 +5,7 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; +import type { Config } from '@backstage/config'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -22,9 +22,32 @@ export const authServiceFactory: ServiceFactory< 'singleton' >; +// @public +export function createExternalTokenHandler( + handler: ExternalTokenHandler, +): ExternalTokenHandler; + +// @public +export interface ExternalTokenHandler { + // (undocumented) + initialize(ctx: { options: Config }): TContext; + // (undocumented) + type: string; + // (undocumented) + verifyToken( + token: string, + ctx: TContext, + ): Promise< + | { + subject: string; + } + | undefined + >; +} + // @public export const externalTokenTypeHandlersRef: ServiceRef< - TokenTypeHandler, + ExternalTokenHandler, 'plugin', 'multiton' >; @@ -59,24 +82,5 @@ export const pluginTokenHandlerDecoratorServiceRef: ServiceRef< 'singleton' >; -// @public -export interface ExternalTokenHandler { - // (undocumented) - verifyToken(token: string): Promise< - | { - subject: string; - allAccessRestrictions?: AccessRestrictionsMap; - } - | undefined - >; -} - -// @public -export interface TokenTypeHandler { - factory: (configs: Config[]) => TokenHandler | TokenHandler[]; - // (undocumented) - type: string; -} - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts index 296ec2e72b..94d289bd7c 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts @@ -148,6 +148,34 @@ function readPermissionAttributes(externalAccessEntryConfig: Config) { return Object.keys(result).length ? result : undefined; } +/** + * Creates an external token handler with the provided implementation. + * + * This helper function simplifies the creation of external token handlers by + * providing type safety and a consistent API. External token handlers are used + * to validate tokens from external systems that need to authenticate with Backstage. + * + * See {@link https://backstage.io/docs/auth/service-to-service-auth#adding-custom-externaltokenhandler | the service-to-service auth docs} + * for more information about implementing custom external token handlers. + * + * @public + * @param handler - The external token handler implementation with type, initialize, and verifyToken methods + * @returns The same handler instance, typed as ExternalTokenHandler + * + * @example + * ```ts + * const customHandler = createExternalTokenHandler({ + * type: 'custom', + * initialize({ options }) { + * return { apiKey: options.getString('apiKey') }; + * }, + * async verifyToken(token, context) { + * // Custom validation logic here + * return { subject: 'custom:user' }; + * }, + * }); + * ``` + */ export function createExternalTokenHandler( handler: ExternalTokenHandler, ): ExternalTokenHandler { From 6ff5452d3c37b21a9390c06bb38a6f03707e5b41 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 8 Sep 2025 09:52:12 +0200 Subject: [PATCH 031/193] fix: correct grammar Co-authored-by: Peter Macdonald Signed-off-by: Juan Pablo Garcia Ripa --- docs/backend-system/architecture/03-services.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index f6722496e5..022aab202b 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -225,8 +225,8 @@ This allows you to provide more advanced options for the service implementation ## Multiton -By default the service reference will point to a singleton instance of the service. This mean if a new service factory uses this reference will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service. -For some services, is desirable to extend the functionality instead of overriding it. For example, some services could have many handler to address a specific event, and you may want to add a new handler instead of overriding the previous one. In this case, you can use the `multiton` option when creating the service reference: +By default the service reference will point to a singleton instance of the service. This mean if a new service factory uses this reference it will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service. +For some services, it is desirable to extend the functionality instead of overriding it. For example, some services could have many handlers to address specific events, and you may want to add a new handler instead of overriding the previous one. In this case, you can use the `multiton` option when creating the service reference: ```ts // example-service-ref.ts From 823ed49a43b7b6f22344f28ce729f8b16c373809 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Thu, 11 Sep 2025 16:33:45 +0200 Subject: [PATCH 032/193] clean up Signed-off-by: Juan Pablo Garcia Ripa --- .../entrypoints/auth/external/legacy.test.ts | 30 ------------ .../src/entrypoints/auth/external/static.ts | 47 ------------------- 2 files changed, 77 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts index 43160b46da..a278e5766c 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts @@ -235,35 +235,5 @@ describe('LegacyTokenHandler', () => { ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`, ); - - // // old style add - // expect(() => - // handler.addOld(new ConfigReader({ secret: 'b2s=' })), - // ).not.toThrow(); - // expect(() => - // handler.addOld(new ConfigReader({ _missingsecret: true })), - // ).toThrowErrorMatchingInlineSnapshot( - // `"Missing required config value at 'secret' in 'mock-config'"`, - // ); - // expect(() => - // handler.addOld(new ConfigReader({ secret: '' })), - // ).toThrowErrorMatchingInlineSnapshot( - // `"Invalid type in config for key 'secret' in 'mock-config', got empty-string, wanted string"`, - // ); - // expect(() => - // handler.addOld(new ConfigReader({ secret: 'has spaces' })), - // ).toThrowErrorMatchingInlineSnapshot( - // `"Illegal secret, must be a valid base64 string"`, - // ); - // expect(() => - // handler.addOld(new ConfigReader({ secret: 'hasnewline\n' })), - // ).toThrowErrorMatchingInlineSnapshot( - // `"Illegal secret, must be a valid base64 string"`, - // ); - // expect(() => - // handler.addOld(new ConfigReader({ secret: 3 })), - // ).toThrowErrorMatchingInlineSnapshot( - // `"Invalid type in config for key 'secret' in 'mock-config', got number, wanted string"`, - // ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts index fe7b9995a4..502150d5a8 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.ts @@ -50,50 +50,3 @@ export const staticTokenHandler = createExternalTokenHandler<{ return undefined; }, }); - -/** - * Handles `type: static` access. - * - * @internal - */ -// export class StaticTokenHandler implements TokenHandler { -// #entries = new Map< -// string, -// { -// subject: string; -// allAccessRestrictions?: AccessRestrictionsMap; -// } -// >(); - -// constructor(configs: Config[]) { -// for (const config of configs) { -// this.add(config); -// } -// } -// private add(config: Config) { -// const token = config.getString('options.token'); -// const subject = config.getString('options.subject'); -// const allAccessRestrictions = readAccessRestrictionsFromConfig(config); - -// if (!token.match(/^\S+$/)) { -// throw new Error('Illegal token, must be a set of non-space characters'); -// } else if (token.length < MIN_TOKEN_LENGTH) { -// throw new Error( -// `Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`, -// ); -// } else if (!subject.match(/^\S+$/)) { -// throw new Error('Illegal subject, must be a set of non-space characters'); -// } else if (this.#entries.has(token)) { -// throw new Error( -// 'Static externalAccess token was declared more than once', -// ); -// } - -// this.#entries.set(token, { subject, allAccessRestrictions }); -// return this; -// } - -// async verifyToken(token: string) { -// return this.#entries.get(token); -// } -// } From 818c835a10e54e1889edcb450c9a11311367139d Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 15 Sep 2025 22:40:23 +0200 Subject: [PATCH 033/193] bring the legacy config back Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/thirty-rules-press.md | 21 --- docs/auth/service-to-service-auth.md | 36 +----- packages/backend-defaults/report-auth.api.md | 9 +- .../auth/authServiceFactory.test.ts | 8 +- .../entrypoints/auth/authServiceFactory.ts | 4 +- .../external/ExternalAuthTokenHandler.test.ts | 51 ++++++++ .../auth/external/ExternalAuthTokenHandler.ts | 24 +++- .../entrypoints/auth/external/legacy.test.ts | 70 ++++++++++ .../src/entrypoints/auth/external/legacy.ts | 120 ++++++++++-------- .../src/entrypoints/auth/external/types.ts | 16 --- .../src/entrypoints/auth/index.ts | 3 +- 11 files changed, 219 insertions(+), 143 deletions(-) diff --git a/.changeset/thirty-rules-press.md b/.changeset/thirty-rules-press.md index 760780eb84..d9968317cb 100644 --- a/.changeset/thirty-rules-press.md +++ b/.changeset/thirty-rules-press.md @@ -3,24 +3,3 @@ --- Add a new `externalTokenHandlersServiceRef` to allow custom external token validations - -BREAKING CHANGE: The `backend.auth.keys` config has been removed. Please migrate to the new `backend.auth.externalAccess` config as described in the documentation: https://backstage.io/docs/auth/service-to-service-auth - -**Migration Example:** - -```yaml -# ❌ Old format (no longer supported) -backend: - auth: - keys: - - secret: your-secret-key - -# ✅ New format -backend: - auth: - externalAccess: - - type: static - options: - token: your-secret-key - subject: external:backstage-plugin # this is the current default for old keys -``` diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index 4ba094068e..1423fa92f7 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -255,16 +255,10 @@ backend: subject: legacy-scaffolder ``` -:::warning -The old style `backend.auth.keys` config is **no longer supported** and has been removed. -If you are still using this configuration, you must migrate to the new `backend.auth.externalAccess` format above. +The old style keys config is also supported as an alternative, but please +consider using the new style above instead: -You'll see an error like `"Unknown key 'backend.auth.keys'"` during Backstage startup if you haven't migrated yet. -::: - -To migrate from the old config format: - -```yaml title="❌ Old format (no longer supported)" +```yaml title="in e.g. app-config.production.yaml" backend: auth: keys: @@ -272,22 +266,6 @@ backend: - secret: my-secret-key-scaffolder ``` -Convert to: - -```yaml title="✅ New format" -backend: - auth: - externalAccess: - - type: legacy - options: - secret: my-secret-key-catalog - subject: external:backstage-plugin - - type: legacy - options: - secret: my-secret-key-scaffolder - subject: external:backstage-plugin -``` - The secrets must be any base64-encoded random data, but for security reasons should be sufficiently long so as not to be easy to guess by brute force. You can for example generate them on the command line: @@ -436,7 +414,7 @@ Each entry has one or more of the following fields: ## Adding custom or logic for validation and issuing of tokens -The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenTypeHandlersRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation. +The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlersServiceRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation. This is particularly useful when you want to add additional logic to the handler, such as logging or metrics or custom token validation. ### PluginTokenHandler decoration @@ -468,7 +446,7 @@ const decoratedPluginTokenHandler = createServiceFactory({ ### Adding custom ExternalTokenHandler -The `externalTokenTypeHandlersRef` can be used to add custom external token handlers to the default implementation. +The `externalTokenHandlersServiceRef` can be used to add custom external token handlers to the default implementation. Your service factory must return an object with a `type` property that matches the token type in your configuration (e.g., 'custom', 'api-key'). When Backstage encounters tokens of this type, it calls your `initialize` method with all the configuration entries that match this type. Your factory can return either a single token handler or an array of handlers to process and validate these tokens. @@ -503,13 +481,13 @@ And we can implement the custom token handler like this: ```ts import { ExternalTokenHandler, - externalTokenTypeHandlersRef, + externalTokenHandlersServiceRef, createExternalTokenHandler, } from '@backstage/backend-defaults/auth'; import { createServiceFactory } from '@backstage/backend-plugin-api'; const customExternalTokenHandlers = createServiceFactory({ - service: externalTokenTypeHandlersRef, + service: externalTokenHandlersServiceRef, deps: {}, async factory() { return createExternalTokenHandler({ diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 0164fa13c1..2fc0f16452 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -4,17 +4,10 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; -import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import type { Config } from '@backstage/config'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; -// @public (undocumented) -export type AccessRestrictionsMap = Map< - string, // plugin ID - BackstagePrincipalAccessRestrictions ->; - // @public export const authServiceFactory: ServiceFactory< AuthService, @@ -46,7 +39,7 @@ export interface ExternalTokenHandler { } // @public -export const externalTokenTypeHandlersRef: ServiceRef< +export const externalTokenHandlersServiceRef: ServiceRef< ExternalTokenHandler, 'plugin', 'multiton' diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index b6d5beefa2..b70aedfb9a 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -31,7 +31,7 @@ import { toInternalBackstageCredentials } from './helpers'; import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; -import { externalTokenTypeHandlersRef } from './external/ExternalAuthTokenHandler'; +import { externalTokenHandlersServiceRef } from './external/ExternalAuthTokenHandler'; import { createExternalTokenHandler } from './external/helpers'; const server = setupServer(); @@ -44,7 +44,7 @@ const mockDeps = [ backend: { baseUrl: 'http://localhost', auth: { - // keys: [{ secret: 'abc' }], + keys: [{ secret: 'abc' }], externalAccess: [ { type: 'static', @@ -468,7 +468,7 @@ describe('authServiceFactory', () => { backend: { baseUrl: 'http://localhost', auth: { - // keys: [{ secret: 'abc' }w], + keys: [{ secret: 'abc' }], externalAccess: [ { type: 'static', @@ -521,7 +521,7 @@ describe('authServiceFactory', () => { }); const customPluginTokenHandler = createServiceFactory({ - service: externalTokenTypeHandlersRef, + service: externalTokenHandlersServiceRef, deps: {}, async factory() { return customExternalTokenHandler; diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index 0f8c5ff75d..ee171aef25 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -22,7 +22,7 @@ import { import { DefaultAuthService } from './DefaultAuthService'; import { ExternalAuthTokenHandler, - externalTokenTypeHandlersRef, + externalTokenHandlersServiceRef, } from './external/ExternalAuthTokenHandler'; import { DefaultPluginTokenHandler, @@ -67,7 +67,7 @@ export const authServiceFactory = createServiceFactory({ plugin: coreServices.pluginMetadata, database: coreServices.database, pluginTokenHandlerDecorator: pluginTokenHandlerDecoratorServiceRef, - externalTokenHandlers: externalTokenTypeHandlersRef, + externalTokenHandlers: externalTokenHandlersServiceRef, }, async factory({ config, diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts index 491de30ac4..f315b43314 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts @@ -233,6 +233,57 @@ describe('ExternalTokenHandler', () => { accessRestrictions: { permissionNames: ['catalog.entity.read'] }, }); }); + it('successfully uses legacy configs', async () => { + const legacyKey = randomBytes(24); + const factory = new FakeTokenFactory({ + issuer: 'my-company', + keyDurationSeconds: 100, + }); + + server.use( + rest.get( + 'https://example.com/.well-known/jwks.json', + async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + return res(ctx.json(keys)); + }, + ), + ); + + const logger = mockServices.logger.mock(); + const handler = ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger, + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + keys: [ + { + secret: legacyKey.toString('base64'), + }, + ], + }, + }, + }, + }), + }); + + expect(logger.warn).toHaveBeenCalledWith( + `DEPRECATION WARNING: The backend.auth.keys config has been replaced by backend.auth.externalAccess, see https://backstage.io/docs/auth/service-to-service-auth`, + ); + + const legacyToken = await new SignJWT({ + sub: 'backstage-server', + exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(), + }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(legacyKey); + + await expect(handler.verifyToken(legacyToken)).resolves.toEqual({ + subject: 'external:backstage-plugin', + }); + }); it('successfully uses custom token handlers', async () => { const factory = new FakeTokenFactory({ issuer: 'my-company', diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts index 17541ff384..4b2cbb3e5a 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts @@ -33,14 +33,13 @@ let loggedDeprecationWarning = false; /** * @public - * This service is used to decorate the default plugin token handler with custom logic. + * This service is used to add custom handlers for external token. */ -export const externalTokenTypeHandlersRef = createServiceRef< +export const externalTokenHandlersServiceRef = createServiceRef< ExternalTokenHandler >({ id: 'core.auth.externalTokenHandlers', multiton: true, - // defaultFactory // :pepe-think: seems like is not possible to use defaultFactory with multiton }); const defaultHandlers: ExternalTokenHandler[] = [ @@ -54,6 +53,7 @@ type ContextMapEntry = { handler: ExternalTokenHandler; allAccessRestrictions?: AccessRestrictionsMap; }; + /** * Handles all types of external caller token types (i.e. not Backstage user * tokens, nor Backstage backend plugin tokens). @@ -71,6 +71,7 @@ export class ExternalAuthTokenHandler { ownPluginId, config, externalTokenHandlers: customHandlers, + logger, } = options; const handlersTypes = [...defaultHandlers, ...(customHandlers ?? [])]; @@ -104,12 +105,23 @@ export class ExternalAuthTokenHandler { if (legacyConfigs.length && !loggedDeprecationWarning) { loggedDeprecationWarning = true; - // :pepe-think: this message was here for more than a year, and replacing this simplifies things a lot - throw new Error( - `The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`, + + logger.warn( + `DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`, ); } + for (const legacyConfig of legacyConfigs) { + contexts.push({ + context: legacyTokenHandler.initialize({ + legacy: true, + options: legacyConfig, + }), + handler: legacyTokenHandler, + allAccessRestrictions: readAccessRestrictionsFromConfig(legacyConfig), + }); + } + return new ExternalAuthTokenHandler(ownPluginId, contexts); } diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts index a278e5766c..48dbcc2418 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts @@ -23,6 +23,7 @@ import { legacyTokenHandler } from './legacy'; describe('LegacyTokenHandler', () => { const key1 = randomBytes(24); const key2 = randomBytes(24); + const key3 = randomBytes(24); const tokenHandler = legacyTokenHandler; @@ -40,6 +41,13 @@ describe('LegacyTokenHandler', () => { }), }); + const contextWithLegacy = tokenHandler.initialize({ + options: new ConfigReader({ + secret: key3.toString('base64'), + }), + legacy: true, + }); + it('should verify valid tokens', async () => { const token1 = await new SignJWT({ sub: 'backstage-server', @@ -62,6 +70,19 @@ describe('LegacyTokenHandler', () => { await expect(tokenHandler.verifyToken(token2, context2)).resolves.toEqual({ subject: 'key2', }); + + const token3 = await new SignJWT({ + sub: 'backstage-server', + exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(), + }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(key3); + + await expect( + tokenHandler.verifyToken(token3, contextWithLegacy), + ).resolves.toEqual({ + subject: 'external:backstage-plugin', + }); }); it('should return undefined if the token is not a valid legacy token', async () => { @@ -235,5 +256,54 @@ describe('LegacyTokenHandler', () => { ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`, ); + + // old style add + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'b2s=' }), + legacy: true, + }), + ).not.toThrow(); + + expect(() => + handler.initialize({ + options: new ConfigReader({ _missingsecret: true }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required config value at 'secret' in 'mock-config'"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: '' }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'secret' in 'mock-config', got empty-string, wanted string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'has spaces' }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'hasnewline\n' }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 3 }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'secret' in 'mock-config', got number, wanted string"`, + ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts index f9932a8735..2ab8c3a5ea 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; -import { createExternalTokenHandler } from './helpers'; +import { ExternalTokenHandler } from './types'; export type LegacyConfigWrapper = { legacy: boolean; @@ -30,64 +30,74 @@ type LegacyTokenHandlerContext = { subject: string; }; -export const legacyTokenHandler = - createExternalTokenHandler({ - type: 'legacy', - initialize(ctx: { options: Config }): LegacyTokenHandlerContext { - const secret = ctx.options.getString('secret'); - const subject = ctx.options.getString('subject'); +type LegacyTokenHandlerOverloaded = + ExternalTokenHandler & { + initialize(ctx: { + options: Config; + legacy: true; + }): LegacyTokenHandlerContext; + }; - if (!secret.match(/^\S+$/)) { - throw new Error('Illegal secret, must be a valid base64 string'); - } else if (!subject.match(/^\S+$/)) { - throw new Error( - 'Illegal subject, must be a set of non-space characters', - ); - } +export const legacyTokenHandler: LegacyTokenHandlerOverloaded = { + type: 'legacy', + initialize(ctx: { + options: Config; + legacy?: true; + }): LegacyTokenHandlerContext { + const secret = ctx.options.getString('secret'); + const subject = ctx.legacy + ? 'external:backstage-plugin' + : ctx.options.getString('subject'); - try { - return { - key: base64url.decode(secret), - subject, - }; - } catch { - throw new Error('Illegal secret, must be a valid base64 string'); - } - }, + if (!secret.match(/^\S+$/)) { + throw new Error('Illegal secret, must be a valid base64 string'); + } else if (!subject.match(/^\S+$/)) { + throw new Error('Illegal subject, must be a set of non-space characters'); + } - async verifyToken(token: string, context: LegacyTokenHandlerContext) { - // First do a duck typing check to see if it remotely looks like a legacy token - try { - // We do a fair amount of checking upfront here. Since we aren't certain - // that it's even the right type of key that we're looking at, we can't - // defer eg the alg check to jwtVerify, because it won't be possible to - // discern different reasons for key verification failures from each other - // easily - const { alg } = decodeProtectedHeader(token); - if (alg !== 'HS256') { - return undefined; - } - const { sub, aud } = decodeJwt(token); - if (sub !== 'backstage-server' || aud) { - return undefined; - } - } catch (e) { - // Doesn't look like a jwt at all + try { + return { + key: base64url.decode(secret), + subject, + }; + } catch { + throw new Error('Illegal secret, must be a valid base64 string'); + } + }, + + async verifyToken(token: string, context: LegacyTokenHandlerContext) { + // First do a duck typing check to see if it remotely looks like a legacy token + try { + // We do a fair amount of checking upfront here. Since we aren't certain + // that it's even the right type of key that we're looking at, we can't + // defer eg the alg check to jwtVerify, because it won't be possible to + // discern different reasons for key verification failures from each other + // easily + const { alg } = decodeProtectedHeader(token); + if (alg !== 'HS256') { return undefined; } - - try { - await jwtVerify(token, context.key); - return { - subject: context.subject, - }; - } catch (error) { - if (error.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { - throw error; - } + const { sub, aud } = decodeJwt(token); + if (sub !== 'backstage-server' || aud) { + return undefined; } - - // None of the signing keys matched + } catch (e) { + // Doesn't look like a jwt at all return undefined; - }, - }); + } + + try { + await jwtVerify(token, context.key); + return { + subject: context.subject, + }; + } catch (error) { + if (error.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { + throw error; + } + } + + // None of the signing keys matched + return undefined; + }, +}; diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index 6ce1ca13b6..01d5a26149 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -17,9 +17,6 @@ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import type { Config } from '@backstage/config'; -/** - * @public - */ export type AccessRestrictionsMap = Map< string, // plugin ID BackstagePrincipalAccessRestrictions @@ -38,16 +35,3 @@ export interface ExternalTokenHandler { ctx: TContext, ): Promise<{ subject: string } | undefined>; } - -// /** -// * @public -// * This interface is used to handle external tokens. -// */ -// export interface TokenTypeHandler { -// type: string; -// /** -// * A factory function that takes all token configuration for a given type -// * and returns a TokenHandler or an array of TokenHandlers. -// */ -// factory: (configs: Config[]) => TokenHandler | TokenHandler[]; -// } diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index e205c8b566..69d41b0310 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -21,9 +21,8 @@ export { export { createExternalTokenHandler } from './external/helpers'; -export { externalTokenTypeHandlersRef } from './external/ExternalAuthTokenHandler'; +export { externalTokenHandlersServiceRef } from './external/ExternalAuthTokenHandler'; export type { ExternalTokenHandler } from './external/types'; -export type { AccessRestrictionsMap } from './external/types'; export type { PluginTokenHandler } from './plugin/PluginTokenHandler'; From fe17ef8115b79c300ac78b253cd78b553836fc8e Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 15 Sep 2025 23:08:47 +0200 Subject: [PATCH 034/193] failing api report Signed-off-by: Juan Pablo Garcia Ripa --- plugins/catalog-react/report-alpha.api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 30cdd91704..cf28d80c11 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -92,12 +92,12 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'entityTableColumnTitle.title': 'Title'; readonly 'entityTableColumnTitle.description': 'Description'; readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.tags': 'Tags'; readonly 'entityTableColumnTitle.namespace': 'Namespace'; readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; readonly 'entityTableColumnTitle.owner': 'Owner'; - readonly 'entityTableColumnTitle.system': 'System'; readonly 'entityTableColumnTitle.targets': 'Targets'; - readonly 'entityTableColumnTitle.tags': 'Tags'; } >; @@ -533,8 +533,8 @@ export const EntityTableColumnTitle: ({ translationKey, }: EntityTableColumnTitleProps) => | 'Title' - | 'Domain' | 'System' + | 'Domain' | 'Lifecycle' | 'Namespace' | 'Owner' From 8ed53eb466c328bb67f426e52f3a1452dd58d63d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Sep 2025 00:05:27 +0200 Subject: [PATCH 035/193] frontend-plugin-api: add coreExtensionData.title Signed-off-by: Patrik Oldsberg --- .changeset/nasty-moose-rescue.md | 5 +++++ .../building-plugins/04-built-in-data-refs.md | 8 ++++++++ packages/frontend-plugin-api/report.api.md | 1 + .../frontend-plugin-api/src/wiring/coreExtensionData.ts | 1 + plugins/catalog-react/report-alpha.api.md | 6 +++--- 5 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 .changeset/nasty-moose-rescue.md diff --git a/.changeset/nasty-moose-rescue.md b/.changeset/nasty-moose-rescue.md new file mode 100644 index 0000000000..ff8cf1b02c --- /dev/null +++ b/.changeset/nasty-moose-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but availble for use for other cases too. diff --git a/docs/frontend-system/building-plugins/04-built-in-data-refs.md b/docs/frontend-system/building-plugins/04-built-in-data-refs.md index b69e4cb3f4..c01520d7bd 100644 --- a/docs/frontend-system/building-plugins/04-built-in-data-refs.md +++ b/docs/frontend-system/building-plugins/04-built-in-data-refs.md @@ -34,6 +34,14 @@ const examplePage = createExtension({ }); ``` +### `title` + +| id | type | +| :----------: | :------: | +| `core.title` | `string` | + +The `title` data reference can be used for defining the extension input/output of string titles. + ### `routePath` | id | type | diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index f9e2336408..52a50659b6 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -377,6 +377,7 @@ export interface ConfigurableExtensionDataRef< // @public (undocumented) export const coreExtensionData: { + title: ConfigurableExtensionDataRef; reactElement: ConfigurableExtensionDataRef< JSX_3.Element, 'core.reactElement', diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index 18466d8196..b778539101 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -20,6 +20,7 @@ import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ export const coreExtensionData = { + title: createExtensionDataRef().with({ id: 'core.title' }), reactElement: createExtensionDataRef().with({ id: 'core.reactElement', }), diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 30cdd91704..cf28d80c11 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -92,12 +92,12 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'entityTableColumnTitle.title': 'Title'; readonly 'entityTableColumnTitle.description': 'Description'; readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.tags': 'Tags'; readonly 'entityTableColumnTitle.namespace': 'Namespace'; readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; readonly 'entityTableColumnTitle.owner': 'Owner'; - readonly 'entityTableColumnTitle.system': 'System'; readonly 'entityTableColumnTitle.targets': 'Targets'; - readonly 'entityTableColumnTitle.tags': 'Tags'; } >; @@ -533,8 +533,8 @@ export const EntityTableColumnTitle: ({ translationKey, }: EntityTableColumnTitleProps) => | 'Title' - | 'Domain' | 'System' + | 'Domain' | 'Lifecycle' | 'Namespace' | 'Owner' From 024645e1ab98608413a4f10d683094fe48b6edd2 Mon Sep 17 00:00:00 2001 From: Hitoshi Kamezaki Date: Mon, 7 Jul 2025 10:26:42 +0900 Subject: [PATCH 036/193] Remove unused @octokit moudles from cli package - @octokit/graphql - @octokit/graphql-schema - @octokit/oauth-app Signed-off-by: Hitoshi Kamezaki --- .changeset/silent-mice-play.md | 9 ++++++ packages/cli/package.json | 3 -- yarn.lock | 51 ++-------------------------------- 3 files changed, 11 insertions(+), 52 deletions(-) create mode 100644 .changeset/silent-mice-play.md diff --git a/.changeset/silent-mice-play.md b/.changeset/silent-mice-play.md new file mode 100644 index 0000000000..aa14cf69e3 --- /dev/null +++ b/.changeset/silent-mice-play.md @@ -0,0 +1,9 @@ +--- +'@backstage/cli': patch +--- + +Remove unused @octokit modules from cli package + +- @octokit/graphql +- @octokit/graphql-schema +- @octokit/oauth-app diff --git a/packages/cli/package.json b/packages/cli/package.json index f50a5601eb..aad6eb410d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,9 +59,6 @@ "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", "@module-federation/enhanced": "^0.9.0", - "@octokit/graphql": "^5.0.0", - "@octokit/graphql-schema": "^13.7.0", - "@octokit/oauth-app": "^4.2.0", "@octokit/request": "^8.0.0", "@rollup/plugin-commonjs": "^26.0.0", "@rollup/plugin-json": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index e606b8246d..dac807ce19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2854,9 +2854,6 @@ __metadata: "@backstage/types": "workspace:^" "@manypkg/get-packages": "npm:^1.1.3" "@module-federation/enhanced": "npm:^0.9.0" - "@octokit/graphql": "npm:^5.0.0" - "@octokit/graphql-schema": "npm:^13.7.0" - "@octokit/oauth-app": "npm:^4.2.0" "@octokit/request": "npm:^8.0.0" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.7" "@rollup/plugin-commonjs": "npm:^26.0.0" @@ -11867,16 +11864,6 @@ __metadata: languageName: node linkType: hard -"@octokit/auth-unauthenticated@npm:^3.0.0": - version: 3.0.0 - resolution: "@octokit/auth-unauthenticated@npm:3.0.0" - dependencies: - "@octokit/request-error": "npm:^2.1.0" - "@octokit/types": "npm:^6.0.3" - checksum: 10/3100d1f4a201ea4ca160c1f432121829ed77a3225aeb77c9d600e677dfdd1fa79bd14e6d24f51f6b10312ba40de31780148eb80bb096b7b33fc4b9c26c593526 - languageName: node - linkType: hard - "@octokit/auth-unauthenticated@npm:^5.0.0": version: 5.0.1 resolution: "@octokit/auth-unauthenticated@npm:5.0.1" @@ -11887,7 +11874,7 @@ __metadata: languageName: node linkType: hard -"@octokit/core@npm:^4.0.0, @octokit/core@npm:^4.2.1": +"@octokit/core@npm:^4.2.1": version: 4.2.4 resolution: "@octokit/core@npm:4.2.4" dependencies: @@ -11949,16 +11936,6 @@ __metadata: languageName: node linkType: hard -"@octokit/graphql-schema@npm:^13.7.0": - version: 13.10.0 - resolution: "@octokit/graphql-schema@npm:13.10.0" - dependencies: - graphql: "npm:^16.0.0" - graphql-tag: "npm:^2.10.3" - checksum: 10/c8714a9dc8f36d01e3db125e570a95030faeaf6c815811be2190ce288691bfcc5e939d161b33ef91b4c9fbca21663bace53d3eea2115a443de27a51b5dbfe2a4 - languageName: node - linkType: hard - "@octokit/graphql@npm:^5.0.0": version: 5.0.6 resolution: "@octokit/graphql@npm:5.0.6" @@ -11981,23 +11958,6 @@ __metadata: languageName: node linkType: hard -"@octokit/oauth-app@npm:^4.2.0": - version: 4.2.4 - resolution: "@octokit/oauth-app@npm:4.2.4" - dependencies: - "@octokit/auth-oauth-app": "npm:^5.0.0" - "@octokit/auth-oauth-user": "npm:^2.0.0" - "@octokit/auth-unauthenticated": "npm:^3.0.0" - "@octokit/core": "npm:^4.0.0" - "@octokit/oauth-authorization-url": "npm:^5.0.0" - "@octokit/oauth-methods": "npm:^2.0.0" - "@types/aws-lambda": "npm:^8.10.83" - fromentries: "npm:^1.3.1" - universal-user-agent: "npm:^6.0.0" - checksum: 10/1c9e48b56fb4cf3428b8967335b46cedf7740d27932ea394530d07deffa8c3bff5ceef8d14bf145b3bfc64ad1088f4ddf46ceca2a4c052f267c3faa12188ef14 - languageName: node - linkType: hard - "@octokit/oauth-app@npm:^6.0.0": version: 6.0.0 resolution: "@octokit/oauth-app@npm:6.0.0" @@ -30904,13 +30864,6 @@ __metadata: languageName: node linkType: hard -"fromentries@npm:^1.3.1": - version: 1.3.2 - resolution: "fromentries@npm:1.3.2" - checksum: 10/10d6e07d289db102c0c1eaf5c3e3fa55ddd6b50033d7de16d99a7cd89f1e1a302dfadb26457031f9bb5d2ed95a179aaf0396092dde5abcae06e8a2f0476826be - languageName: node - linkType: hard - "fs-constants@npm:^1.0.0": version: 1.0.0 resolution: "fs-constants@npm:1.0.0" @@ -31884,7 +31837,7 @@ __metadata: languageName: node linkType: hard -"graphql-tag@npm:^2.10.3, graphql-tag@npm:^2.12.6": +"graphql-tag@npm:^2.12.6": version: 2.12.6 resolution: "graphql-tag@npm:2.12.6" dependencies: From 3d09bb218aa4c565cfa4ab6a1b3f1500a8556d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Edeg=C3=A5rd?= Date: Wed, 17 Sep 2025 11:30:28 +0000 Subject: [PATCH 037/193] Adds username as optional config in order to send with a different username than the Slack App have. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Henrik Edegård --- .changeset/heavy-cooks-divide.md | 5 + docs/notifications/processors.md | 1 + .../lib/SlackNotificationProcessor.test.ts | 291 ++++++++++++++++++ .../src/lib/SlackNotificationProcessor.ts | 15 +- .../src/lib/util.ts | 4 +- 5 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 .changeset/heavy-cooks-divide.md diff --git a/.changeset/heavy-cooks-divide.md b/.changeset/heavy-cooks-divide.md new file mode 100644 index 0000000000..181cd01075 --- /dev/null +++ b/.changeset/heavy-cooks-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-slack': minor +--- + +Adds username as optional config in order to send Slack notifications with a specific username in the case when using one Slack App for more than just Backstage. diff --git a/docs/notifications/processors.md b/docs/notifications/processors.md index ad3e427364..c25b669ed6 100644 --- a/docs/notifications/processors.md +++ b/docs/notifications/processors.md @@ -148,6 +148,7 @@ notifications: - token: xoxb-XXXXXXXXX broadcastChannels: # Optional, if you wish to support broadcast notifications. - C12345678 + username: 'Backstage Bot' # Optional, defaults to the name of the Slack App. ``` Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index 45a74cee70..c970aec7ba 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -542,6 +542,297 @@ describe('SlackNotificationProcessor', () => { }); }); + describe('when username is configured', () => { + it('should include username in group messages', async () => { + const slack = new WebClient(); + const usernameConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + username: 'BackstageBot', + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(usernameConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ + recipients: { type: 'entity', entityRef: 'group:default/mock' }, + payload: { title: 'notification' }, + }); + + expect(slack.chat.postMessage).toHaveBeenCalledWith({ + channel: 'C12345678', + text: 'notification', + username: 'BackstageBot', + attachments: [ + { + color: '#00A699', + blocks: [ + { + type: 'section', + text: { + text: 'No description provided', + type: 'mrkdwn', + }, + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'View More', + }, + action_id: 'button-action', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: 'Severity: normal', + emoji: true, + }, + { + type: 'plain_text', + text: 'Topic: N/A', + emoji: true, + }, + ], + }, + ], + fallback: 'notification', + }, + ], + }); + }); + + it('should include username in direct user messages', async () => { + const slack = new WebClient(); + const usernameConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + username: 'BackstageBot', + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(usernameConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: 'user:default/mock', + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, + }, + { + recipients: { type: 'entity', entityRef: 'user:default/mock' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledWith({ + channel: 'U12345678', + text: 'notification', + username: 'BackstageBot', + attachments: [ + { + color: '#00A699', + blocks: [ + { + type: 'section', + text: { + text: 'No description provided', + type: 'mrkdwn', + }, + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'View More', + }, + action_id: 'button-action', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: 'Severity: normal', + emoji: true, + }, + { + type: 'plain_text', + text: 'Topic: N/A', + emoji: true, + }, + ], + }, + ], + fallback: 'notification', + }, + ], + }); + }); + + it('should include username in broadcast messages', async () => { + const slack = new WebClient(); + const usernameAndBroadcastConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + username: 'BackstageBot', + broadcastChannels: ['C12345678'], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig( + usernameAndBroadcastConfig, + { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + }, + )[0]; + + await processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + link: '/catalog/user/default/jane.doe', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledWith({ + channel: 'C12345678', + text: 'notification', + username: 'BackstageBot', + attachments: [ + { + color: '#00A699', + blocks: [ + { + type: 'section', + text: { + text: 'No description provided', + type: 'mrkdwn', + }, + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'View More', + }, + action_id: 'button-action', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: 'Severity: normal', + emoji: true, + }, + { + type: 'plain_text', + text: 'Topic: N/A', + emoji: true, + }, + ], + }, + ], + fallback: 'notification', + }, + ], + }); + }); + }); + + describe('when username is not configured', () => { + it('should not include username in messages', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ + recipients: { type: 'entity', entityRef: 'group:default/mock' }, + payload: { title: 'notification' }, + }); + + const calls = (slack.chat.postMessage as jest.Mock).mock.calls; + expect(calls).toHaveLength(1); + expect(calls[0][0]).not.toHaveProperty('username'); + }); + }); + describe('when replacing user entity refs with Slack IDs', () => { const createBaseMessage = (text: string) => ({ channel: 'U12345678', diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index f7dbc6a93e..f050b3b677 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -49,6 +49,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly messagesFailed: Counter; private readonly broadcastChannels?: string[]; private readonly entityLoader: DataLoader; + private readonly username?: string; static fromConfig( config: Config, @@ -66,9 +67,11 @@ export class SlackNotificationProcessor implements NotificationProcessor { const token = c.getString('token'); const slack = options.slack ?? new WebClient(token); const broadcastChannels = c.getOptionalStringArray('broadcastChannels'); + const username = c.getOptionalString('username'); return new SlackNotificationProcessor({ slack, broadcastChannels, + username, ...options, }); }); @@ -80,13 +83,16 @@ export class SlackNotificationProcessor implements NotificationProcessor { logger: LoggerService; catalog: CatalogService; broadcastChannels?: string[]; + username?: string; }) { - const { auth, catalog, logger, slack, broadcastChannels } = options; + const { auth, catalog, logger, slack, broadcastChannels, username } = + options; this.logger = logger; this.catalog = catalog; this.auth = auth; this.slack = slack; this.broadcastChannels = broadcastChannels; + this.username = username; this.entityLoader = new DataLoader( async entityRefs => { @@ -206,6 +212,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { const payload = toChatPostMessageArgs({ channel, payload: options.payload, + ...(this.username && { username: this.username }), }); this.logger.debug( @@ -261,7 +268,11 @@ export class SlackNotificationProcessor implements NotificationProcessor { options.payload, ); const outbound = destinations.map(channel => - toChatPostMessageArgs({ channel, payload: formattedPayload }), + toChatPostMessageArgs({ + channel, + payload: formattedPayload, + ...(this.username && { username: this.username }), + }), ); // Log debug info diff --git a/plugins/notifications-backend-module-slack/src/lib/util.ts b/plugins/notifications-backend-module-slack/src/lib/util.ts index 24f9f6a964..1116218b09 100644 --- a/plugins/notifications-backend-module-slack/src/lib/util.ts +++ b/plugins/notifications-backend-module-slack/src/lib/util.ts @@ -23,12 +23,14 @@ import { ChatPostMessageArguments, KnownBlock } from '@slack/web-api'; export function toChatPostMessageArgs(options: { channel: string; payload: NotificationPayload; + username?: string; }): ChatPostMessageArguments { - const { channel, payload } = options; + const { channel, payload, username } = options; const args: ChatPostMessageArguments = { channel, text: payload.title, + ...(username && { username }), attachments: [ { color: getColor(payload.severity), From 99fcf986fee0d121ae1b786219fac5e0a2642c79 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Mon, 15 Sep 2025 16:19:57 -0400 Subject: [PATCH 038/193] Remove all unused luxon dependencies Signed-off-by: Hope Hadfield --- .changeset/warm-items-look.md | 17 +++++++++++++++++ .../catalog-backend-module-aws/knip-report.md | 12 ------------ plugins/catalog-backend-module-aws/package.json | 5 +---- .../catalog-backend-module-azure/knip-report.md | 5 ----- .../catalog-backend-module-azure/package.json | 1 - .../knip-report.md | 11 ----------- .../package.json | 2 -- .../knip-report.md | 11 ----------- .../package.json | 2 -- .../knip-report.md | 5 ----- .../catalog-backend-module-gerrit/package.json | 1 - .../knip-report.md | 5 ----- .../package.json | 3 +-- .../knip-report.md | 12 ------------ .../catalog-backend-module-github/package.json | 3 --- .../knip-report.md | 5 ----- .../package.json | 3 +-- .../knip-report.md | 5 ----- .../catalog-backend-module-gitlab/package.json | 1 - .../knip-report.md | 5 ----- .../catalog-backend-module-msgraph/package.json | 1 - .../knip-report.md | 5 ----- .../package.json | 1 - plugins/kubernetes-cluster/knip-report.md | 15 --------------- plugins/kubernetes-cluster/package.json | 6 ------ plugins/kubernetes/knip-report.md | 15 --------------- plugins/kubernetes/package.json | 13 +------------ 27 files changed, 21 insertions(+), 149 deletions(-) create mode 100644 .changeset/warm-items-look.md diff --git a/.changeset/warm-items-look.md b/.changeset/warm-items-look.md new file mode 100644 index 0000000000..b8a2abd61d --- /dev/null +++ b/.changeset/warm-items-look.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-github-org': patch +'@backstage/plugin-catalog-backend-module-gitlab-org': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes': patch +--- + +Removed unused dependencies diff --git a/plugins/catalog-backend-module-aws/knip-report.md b/plugins/catalog-backend-module-aws/knip-report.md index ed7557334b..97d5b385fd 100644 --- a/plugins/catalog-backend-module-aws/knip-report.md +++ b/plugins/catalog-backend-module-aws/knip-report.md @@ -1,15 +1,3 @@ # Knip report -## Unused dependencies (2) - -| Name | Location | Severity | -| :---------------------------- | :----------- | :------- | -| @aws-sdk/credential-providers | plugins/catalog-backend-module-aws/package.json | error | -| winston | plugins/catalog-backend-module-aws/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-aws/package.json | error | diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 3f21de8eb1..0b29e2bda8 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -54,7 +54,6 @@ "@aws-sdk/client-eks": "^3.350.0", "@aws-sdk/client-organizations": "^3.350.0", "@aws-sdk/client-s3": "^3.350.0", - "@aws-sdk/credential-providers": "^3.350.0", "@aws-sdk/middleware-endpoint": "^3.347.0", "@aws-sdk/types": "^3.347.0", "@backstage/backend-defaults": "workspace:^", @@ -68,8 +67,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "p-limit": "^3.0.2", - "uuid": "^11.0.0", - "winston": "^3.2.1" + "uuid": "^11.0.0" }, "devDependencies": { "@aws-sdk/util-stream-node": "^3.350.0", @@ -77,7 +75,6 @@ "@backstage/cli": "workspace:^", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0", - "luxon": "^3.0.0", "yaml": "^2.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-azure/knip-report.md b/plugins/catalog-backend-module-azure/knip-report.md index 622077aa40..97d5b385fd 100644 --- a/plugins/catalog-backend-module-azure/knip-report.md +++ b/plugins/catalog-backend-module-azure/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-azure/package.json | error | diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 17c66699a5..0aadfcecfc 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -63,7 +63,6 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md b/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md index a6400f9cd2..97d5b385fd 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md @@ -1,14 +1,3 @@ # Knip report -## Unused dependencies (1) - -| Name | Location | Severity | -| :------------------------ | :----------- | :------- | -| @backstage/catalog-client | plugins/catalog-backend-module-bitbucket-cloud/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-bitbucket-cloud/package.json | error | diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 68dfbaa619..f5afab87d2 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -52,7 +52,6 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/integration": "workspace:^", @@ -66,7 +65,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", - "luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-bitbucket-server/knip-report.md b/plugins/catalog-backend-module-bitbucket-server/knip-report.md index 067155bd06..97d5b385fd 100644 --- a/plugins/catalog-backend-module-bitbucket-server/knip-report.md +++ b/plugins/catalog-backend-module-bitbucket-server/knip-report.md @@ -1,14 +1,3 @@ # Knip report -## Unused dependencies (1) - -| Name | Location | Severity | -| :------------------------ | :----------- | :------- | -| @backstage/catalog-client | plugins/catalog-backend-module-bitbucket-server/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-bitbucket-server/package.json | error | diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index ce3ec49ae8..95b310eec1 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -47,7 +47,6 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", @@ -61,7 +60,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", - "luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-gerrit/knip-report.md b/plugins/catalog-backend-module-gerrit/knip-report.md index 7a02c83e07..97d5b385fd 100644 --- a/plugins/catalog-backend-module-gerrit/knip-report.md +++ b/plugins/catalog-backend-module-gerrit/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-gerrit/package.json | error | diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 1c4d7e8aa7..ba6c981a41 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -61,7 +61,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0", - "luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-github-org/knip-report.md b/plugins/catalog-backend-module-github-org/knip-report.md index d71302f8ae..97d5b385fd 100644 --- a/plugins/catalog-backend-module-github-org/knip-report.md +++ b/plugins/catalog-backend-module-github-org/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-github-org/package.json | error | diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index f9c1f516fb..a864ffe6e2 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -41,7 +41,6 @@ }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", - "@backstage/cli": "workspace:^", - "luxon": "^3.0.0" + "@backstage/cli": "workspace:^" } } diff --git a/plugins/catalog-backend-module-github/knip-report.md b/plugins/catalog-backend-module-github/knip-report.md index 35b39536c9..c9e8569b52 100644 --- a/plugins/catalog-backend-module-github/knip-report.md +++ b/plugins/catalog-backend-module-github/knip-report.md @@ -1,17 +1,5 @@ # Knip report -## Unused dependencies (2) - -| Name | Location | Severity | -| :-------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend | plugins/catalog-backend-module-github/package.json | error | -| @backstage/catalog-client | plugins/catalog-backend-module-github/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-github/package.json | error | ## Unlisted dependencies (4) diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 5187103233..0a5075c3c9 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -52,11 +52,9 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/integration": "workspace:^", - "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", @@ -73,7 +71,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "luxon": "^3.0.0", "msw": "^2.0.0", "type-fest": "^4.41.0" }, diff --git a/plugins/catalog-backend-module-gitlab-org/knip-report.md b/plugins/catalog-backend-module-gitlab-org/knip-report.md index 43328f5e48..97d5b385fd 100644 --- a/plugins/catalog-backend-module-gitlab-org/knip-report.md +++ b/plugins/catalog-backend-module-gitlab-org/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-gitlab-org/package.json | error | diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 09f6ee7811..2ebcdf5472 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -41,7 +41,6 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/plugin-events-backend-test-utils": "workspace:^", - "luxon": "^3.0.0" + "@backstage/plugin-events-backend-test-utils": "workspace:^" } } diff --git a/plugins/catalog-backend-module-gitlab/knip-report.md b/plugins/catalog-backend-module-gitlab/knip-report.md index 8c7b1cc0a0..97d5b385fd 100644 --- a/plugins/catalog-backend-module-gitlab/knip-report.md +++ b/plugins/catalog-backend-module-gitlab/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-gitlab/package.json | error | diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index c18be1028b..b14f6bb930 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -69,7 +69,6 @@ "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", "@types/lodash": "^4.14.151", - "luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-msgraph/knip-report.md b/plugins/catalog-backend-module-msgraph/knip-report.md index 1b4a6feaab..97d5b385fd 100644 --- a/plugins/catalog-backend-module-msgraph/knip-report.md +++ b/plugins/catalog-backend-module-msgraph/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-msgraph/package.json | error | diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index f2f8a5d442..cb69f932f0 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -67,7 +67,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-puppetdb/knip-report.md b/plugins/catalog-backend-module-puppetdb/knip-report.md index ac2ab908f0..97d5b385fd 100644 --- a/plugins/catalog-backend-module-puppetdb/knip-report.md +++ b/plugins/catalog-backend-module-puppetdb/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused dependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-puppetdb/package.json | error | diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index ced5df4773..68b5127056 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -56,7 +56,6 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "lodash": "^4.17.21", - "luxon": "^3.0.0", "uuid": "^11.0.0" }, "devDependencies": { diff --git a/plugins/kubernetes-cluster/knip-report.md b/plugins/kubernetes-cluster/knip-report.md index 55eba7a829..97d5b385fd 100644 --- a/plugins/kubernetes-cluster/knip-report.md +++ b/plugins/kubernetes-cluster/knip-report.md @@ -1,18 +1,3 @@ # Knip report -## Unused dependencies (5) - -| Name | Location | Severity | -| :---------------------- | :----------- | :------- | -| @kubernetes-models/base | plugins/kubernetes-cluster/package.json | error | -| cronstrue | plugins/kubernetes-cluster/package.json | error | -| js-yaml | plugins/kubernetes-cluster/package.json | error | -| lodash | plugins/kubernetes-cluster/package.json | error | -| luxon | plugins/kubernetes-cluster/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :------------------- | :----------- | :------- | -| @testing-library/dom | plugins/kubernetes-cluster/package.json | error | diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index e789c3edaf..49ec22c9c3 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -62,20 +62,14 @@ "@backstage/plugin-kubernetes-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@kubernetes-models/apimachinery": "^2.0.0", - "@kubernetes-models/base": "^5.0.0", "@material-ui/core": "^4.12.2", "@material-ui/lab": "4.0.0-alpha.61", - "cronstrue": "^2.2.0", - "js-yaml": "^4.0.0", "kubernetes-models": "^4.1.0", - "lodash": "^4.17.21", - "luxon": "^3.0.0", "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", - "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", "@types/node": "^20.16.0", diff --git a/plugins/kubernetes/knip-report.md b/plugins/kubernetes/knip-report.md index 11b66acd1b..97d5b385fd 100644 --- a/plugins/kubernetes/knip-report.md +++ b/plugins/kubernetes/knip-report.md @@ -1,18 +1,3 @@ # Knip report -## Unused dependencies (11) - -| Name | Location | Severity | -| :------------------------------ | :----------- | :------- | -| @kubernetes-models/apimachinery | plugins/kubernetes/package.json | error | -| @kubernetes-models/base | plugins/kubernetes/package.json | error | -| @kubernetes/client-node | plugins/kubernetes/package.json | error | -| @xterm/addon-attach | plugins/kubernetes/package.json | error | -| kubernetes-models | plugins/kubernetes/package.json | error | -| @xterm/addon-fit | plugins/kubernetes/package.json | error | -| @xterm/xterm | plugins/kubernetes/package.json | error | -| cronstrue | plugins/kubernetes/package.json | error | -| js-yaml | plugins/kubernetes/package.json | error | -| lodash | plugins/kubernetes/package.json | error | -| luxon | plugins/kubernetes/package.json | error | diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index e9da7b47ec..d5bc9c0b88 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -67,18 +67,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", - "@kubernetes-models/apimachinery": "^2.0.0", - "@kubernetes-models/base": "^5.0.0", - "@kubernetes/client-node": "1.1.2", - "@material-ui/core": "^4.12.2", - "@xterm/addon-attach": "^0.11.0", - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "cronstrue": "^2.2.0", - "js-yaml": "^4.0.0", - "kubernetes-models": "^4.1.0", - "lodash": "^4.17.21", - "luxon": "^3.0.0" + "@material-ui/core": "^4.12.2" }, "devDependencies": { "@backstage/cli": "workspace:^", From fa589daef639b24884fc306e7c504fac8f9ed5dd Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Wed, 17 Sep 2025 16:29:54 -0400 Subject: [PATCH 039/193] add yarn.lock Signed-off-by: Hope Hadfield --- yarn.lock | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/yarn.lock b/yarn.lock index e606b8246d..ebc7061499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4332,7 +4332,6 @@ __metadata: "@aws-sdk/client-eks": "npm:^3.350.0" "@aws-sdk/client-organizations": "npm:^3.350.0" "@aws-sdk/client-s3": "npm:^3.350.0" - "@aws-sdk/credential-providers": "npm:^3.350.0" "@aws-sdk/middleware-endpoint": "npm:^3.347.0" "@aws-sdk/types": "npm:^3.347.0" "@aws-sdk/util-stream-node": "npm:^3.350.0" @@ -4350,10 +4349,8 @@ __metadata: "@backstage/plugin-kubernetes-common": "workspace:^" aws-sdk-client-mock: "npm:^4.0.0" aws-sdk-client-mock-jest: "npm:^4.0.0" - luxon: "npm:^3.0.0" p-limit: "npm:^3.0.2" uuid: "npm:^11.0.0" - winston: "npm:^3.2.1" yaml: "npm:^2.0.0" languageName: unknown linkType: soft @@ -4371,7 +4368,6 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" - luxon: "npm:^3.0.0" msw: "npm:^1.0.0" uuid: "npm:^11.0.0" languageName: unknown @@ -4403,7 +4399,6 @@ __metadata: dependencies: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" - "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -4413,7 +4408,6 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - luxon: "npm:^3.0.0" msw: "npm:^1.0.0" uuid: "npm:^11.0.0" languageName: unknown @@ -4425,7 +4419,6 @@ __metadata: dependencies: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" - "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -4435,7 +4428,6 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - luxon: "npm:^3.0.0" msw: "npm:^1.0.0" uuid: "npm:^11.0.0" languageName: unknown @@ -4470,7 +4462,6 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@types/fs-extra": "npm:^11.0.0" fs-extra: "npm:^11.2.0" - luxon: "npm:^3.0.0" msw: "npm:^1.0.0" p-limit: "npm:^3.1.0" uuid: "npm:^11.0.0" @@ -4505,7 +4496,6 @@ __metadata: "@backstage/plugin-catalog-backend-module-github": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - luxon: "npm:^3.0.0" languageName: unknown linkType: soft @@ -4515,12 +4505,10 @@ __metadata: dependencies: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" - "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/integration": "workspace:^" - "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" @@ -4531,7 +4519,6 @@ __metadata: "@types/lodash": "npm:^4.14.151" git-url-parse: "npm:^15.0.0" lodash: "npm:^4.17.21" - luxon: "npm:^3.0.0" minimatch: "npm:^9.0.0" msw: "npm:^2.0.0" type-fest: "npm:^4.41.0" @@ -4550,7 +4537,6 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - luxon: "npm:^3.0.0" languageName: unknown linkType: soft @@ -4572,7 +4558,6 @@ __metadata: "@gitbeaker/rest": "npm:^40.0.3" "@types/lodash": "npm:^4.14.151" lodash: "npm:^4.17.21" - luxon: "npm:^3.0.0" msw: "npm:^1.0.0" node-fetch: "npm:^2.7.0" uuid: "npm:^11.0.0" @@ -4654,7 +4639,6 @@ __metadata: "@microsoft/microsoft-graph-types": "npm:^2.6.0" "@types/lodash": "npm:^4.14.151" lodash: "npm:^4.17.21" - luxon: "npm:^3.0.0" msw: "npm:^1.0.0" p-limit: "npm:^3.0.2" qs: "npm:^6.9.4" @@ -4694,7 +4678,6 @@ __metadata: "@backstage/types": "workspace:^" "@types/lodash": "npm:^4.14.151" lodash: "npm:^4.17.21" - luxon: "npm:^3.0.0" msw: "npm:^1.0.0" uuid: "npm:^11.0.0" languageName: unknown @@ -5533,19 +5516,13 @@ __metadata: "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@kubernetes-models/apimachinery": "npm:^2.0.0" - "@kubernetes-models/base": "npm:^5.0.0" "@material-ui/core": "npm:^4.12.2" "@material-ui/lab": "npm:4.0.0-alpha.61" - "@testing-library/dom": "npm:^10.0.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@types/node": "npm:^20.16.0" "@types/react": "npm:^18.0.0" - cronstrue: "npm:^2.2.0" - js-yaml: "npm:^4.0.0" kubernetes-models: "npm:^4.1.0" - lodash: "npm:^4.17.21" - luxon: "npm:^3.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.3.0" @@ -5660,22 +5637,11 @@ __metadata: "@backstage/plugin-kubernetes-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" - "@kubernetes-models/apimachinery": "npm:^2.0.0" - "@kubernetes-models/base": "npm:^5.0.0" - "@kubernetes/client-node": "npm:1.1.2" "@material-ui/core": "npm:^4.12.2" "@testing-library/dom": "npm:^10.0.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@types/react": "npm:^18.0.0" - "@xterm/addon-attach": "npm:^0.11.0" - "@xterm/addon-fit": "npm:^0.10.0" - "@xterm/xterm": "npm:^5.5.0" - cronstrue: "npm:^2.2.0" - js-yaml: "npm:^4.0.0" - kubernetes-models: "npm:^4.1.0" - lodash: "npm:^4.17.21" - luxon: "npm:^3.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.3.0" @@ -26680,7 +26646,7 @@ __metadata: languageName: node linkType: hard -"cronstrue@npm:^2.2.0, cronstrue@npm:^2.32.0": +"cronstrue@npm:^2.32.0": version: 2.52.0 resolution: "cronstrue@npm:2.52.0" bin: From 02c67c58cfafd662968674b67f0f5f446bc05f54 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Thu, 18 Sep 2025 08:51:01 +0200 Subject: [PATCH 040/193] Feedback Signed-off-by: TA31OU --- plugins/catalog-backend-module-gitea/package.json | 2 +- .../EntityRelationsGraph/DefaultRenderEdge.tsx | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json index 8389ee70ef..c30c6a7dd2 100644 --- a/plugins/catalog-backend-module-gitea/package.json +++ b/plugins/catalog-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitea", - "version": "0.1.4-next.1", + "version": "0.1.4-next.0", "description": "The gitea backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx deleted file mode 100644 index 379ce77f52..0000000000 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderEdge.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ From f5695351141ce9414c53185c7bd4d6b50c2dc1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EC=A4=80?= Date: Sat, 20 Sep 2025 13:55:54 +0900 Subject: [PATCH 041/193] feat: add kubernetes configmap component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 김병준 --- .../src/components/Cluster/Cluster.tsx | 6 + .../ConfigmapsAccordions.tsx | 118 ++++++++++++++++++ .../ConfigmapsAccordions/ConfigmapsDrawer.tsx | 64 ++++++++++ .../components/ConfigmapsAccordions/index.ts | 16 +++ 4 files changed, 204 insertions(+) create mode 100644 plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx create mode 100644 plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.tsx create mode 100644 plugins/kubernetes-react/src/components/ConfigmapsAccordions/index.ts diff --git a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx index 642ed642a4..4e8c4a1947 100644 --- a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx @@ -30,6 +30,7 @@ import { DeploymentsAccordions } from '../DeploymentsAccordions'; import { StatefulSetsAccordions } from '../StatefulSetsAccordions'; import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; +import { ConfigmapsAccordions } from '../ConfigmapsAccordions'; import { CronJobsAccordions } from '../CronJobsAccordions'; import { CustomResources } from '../CustomResources'; import { DaemonSetsAccordions } from '../DaemonSetsAccordions'; @@ -170,6 +171,11 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { ) : undefined} + {groupedResponses.configMaps.length > 0 ? ( + + + + ) : undefined} {groupedResponses.cronJobs.length > 0 ? ( diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx new file mode 100644 index 0000000000..81ab874737 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx @@ -0,0 +1,118 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useContext } from 'react'; +import Accordion from '@material-ui/core/Accordion'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import type { V1ConfigMap } from '@kubernetes/client-node'; +import { ConfigmapsDrawer } from './ConfigmapsDrawer.tsx'; +import { GroupedResponsesContext } from '../../hooks'; +import { StructuredMetadataTable } from '@backstage/core-components'; + +type ConfigmapSummaryProps = { + configmap: V1ConfigMap; +}; + +const ConfigmapSummary = ({ configmap }: ConfigmapSummaryProps) => { + return ( + + + + + + + + Data Count: {configmap.data ? Object.keys(configmap.data).length : 0} + + + + ); +}; + +type ConfigmapsCardProps = { + configmap: V1ConfigMap; +}; + +const ConfigmapCard = ({ configmap }: ConfigmapsCardProps) => { + const metadata: any = {}; + + metadata.data = configmap.data; + + return ( + + ); +}; + +/** + * + * + * @public + */ +export type ConfigmapsAccordionsProps = {}; + +type ConfigmapsAccordionProps = { + configmap: V1ConfigMap; +}; + +const ConfigmapsAccordion = ({ configmap }: ConfigmapsAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +/** + * + * + * @public + */ +export const ConfigmapsAccordions = ({}: ConfigmapsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + return ( + + {groupedResponses.configMaps.map((configmap, i) => ( + + + + ))} + + ); +}; diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.tsx new file mode 100644 index 0000000000..97e8aab723 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2020 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 { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import Chip from '@material-ui/core/Chip'; +import type { V1ConfigMap } from '@kubernetes/client-node'; + +export const ConfigmapsDrawer = ({ + configmap, + expanded, +}: { + configmap: V1ConfigMap; + expanded?: boolean; +}) => { + const namespace = configmap.metadata?.namespace; + return ( + { + return configmapObject || {}; + }} + > + + + + {configmap.metadata?.name ?? 'unknown object'} + + + + + ConfigMap + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/index.ts b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/index.ts new file mode 100644 index 0000000000..25a1578a79 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './ConfigmapsAccordions.tsx'; From 5787cda0b9efa66bd94b9ca3f872cc6b9d68ccba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EC=A4=80?= Date: Sat, 20 Sep 2025 14:07:50 +0900 Subject: [PATCH 042/193] feat: add test for configmap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 김병준 --- .../src/__fixtures__/1-configmaps.json | 26 +++++++++ .../src/__fixtures__/2-configmaps.json | 47 ++++++++++++++++ .../ConfigmapsAccordions.test.tsx | 54 +++++++++++++++++++ .../ConfigmapsDrawer.test.tsx | 37 +++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 plugins/kubernetes-react/src/__fixtures__/1-configmaps.json create mode 100644 plugins/kubernetes-react/src/__fixtures__/2-configmaps.json create mode 100644 plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.test.tsx create mode 100644 plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.test.tsx diff --git a/plugins/kubernetes-react/src/__fixtures__/1-configmaps.json b/plugins/kubernetes-react/src/__fixtures__/1-configmaps.json new file mode 100644 index 0000000000..ab0518e87d --- /dev/null +++ b/plugins/kubernetes-react/src/__fixtures__/1-configmaps.json @@ -0,0 +1,26 @@ +{ + "configMaps": [ + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "app-config", + "namespace": "default", + "uid": "1ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "app": "dice-roller" + }, + "annotations": {} + }, + "data": { + "database.host": "localhost", + "database.port": "5432", + "app.name": "dice-roller", + "app.version": "1.0.0" + } + } + ] +} diff --git a/plugins/kubernetes-react/src/__fixtures__/2-configmaps.json b/plugins/kubernetes-react/src/__fixtures__/2-configmaps.json new file mode 100644 index 0000000000..2c0c7f8121 --- /dev/null +++ b/plugins/kubernetes-react/src/__fixtures__/2-configmaps.json @@ -0,0 +1,47 @@ +{ + "configMaps": [ + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "app-config", + "namespace": "default", + "uid": "1ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "app": "dice-roller" + }, + "annotations": {} + }, + "data": { + "database.host": "localhost", + "database.port": "5432", + "app.name": "dice-roller", + "app.version": "1.0.0" + } + }, + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "redis-config", + "namespace": "default", + "uid": "2ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732553", + "creationTimestamp": "2021-07-16T22:40:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "app": "redis" + }, + "annotations": {} + }, + "data": { + "redis.conf": "# Redis configuration\nmaxmemory 256mb\nmaxmemory-policy allkeys-lru", + "redis.host": "redis-service", + "redis.port": "6379" + } + } + ] +} diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.test.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.test.tsx new file mode 100644 index 0000000000..d819a01cd9 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { screen } from '@testing-library/react'; +import { ConfigmapsAccordions } from './ConfigmapsAccordions'; +import * as oneConfigmapsFixture from '../../__fixtures__/1-configmaps.json'; +import * as twoConfigmapsFixture from '../../__fixtures__/2-configmaps.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('ConfigmapsAccordions', () => { + it('should render 1 configmap', async () => { + const wrapper = kubernetesProviders( + oneConfigmapsFixture, + new Set(), + ); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('app-config')).toBeInTheDocument(); + expect(screen.getByText('ConfigMap')).toBeInTheDocument(); + expect(screen.getByText('namespace: default')).toBeInTheDocument(); + expect(screen.getByText('Data Count: 4')).toBeInTheDocument(); + }); + + it('should render 2 configmaps', async () => { + const wrapper = kubernetesProviders( + twoConfigmapsFixture, + new Set(), + ); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('app-config')).toBeInTheDocument(); + expect(screen.getByText('redis-config')).toBeInTheDocument(); + expect(screen.getAllByText('ConfigMap')).toHaveLength(2); + expect(screen.getAllByText('namespace: default')).toHaveLength(2); + expect(screen.getByText('Data Count: 4')).toBeInTheDocument(); + expect(screen.getByText('Data Count: 3')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.test.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.test.tsx new file mode 100644 index 0000000000..cde7265cab --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as oneConfigmapsFixture from '../../__fixtures__/1-configmaps.json'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { ConfigmapsDrawer } from './ConfigmapsDrawer'; +import { kubernetesClusterLinkFormatterApiRef } from '../../api'; + +describe('ConfigmapsDrawer', () => { + it('should render configmap drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + + + , + ); + + expect(getAllByText('app-config')).toHaveLength(3); + expect(getAllByText('ConfigMap')).toHaveLength(3); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + }); +}); From ac405f2ed726015e024cb6c8d021679b86b6bbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EC=A4=80?= Date: Sat, 20 Sep 2025 22:56:04 +0900 Subject: [PATCH 043/193] chore: add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 김병준 --- .changeset/famous-loops-tickle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/famous-loops-tickle.md diff --git a/.changeset/famous-loops-tickle.md b/.changeset/famous-loops-tickle.md new file mode 100644 index 0000000000..ae8c9d80ee --- /dev/null +++ b/.changeset/famous-loops-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +The configmaps added to be rendered From 37f540b71db528990ab0fab590e4e9b6a1f6dcbf Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 22 Sep 2025 10:37:52 +0100 Subject: [PATCH 044/193] update test handlers to simulate checking for both project.id and project.path_with_namespace Signed-off-by: Claire Peng --- .../src/__testUtils__/handlers.ts | 44 ++++++++++++------- .../src/lib/client.test.ts | 7 ++- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 9af00d2564..241bc86df6 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -266,24 +266,34 @@ const httpProjectFindByIdDynamic = all_projects_response.map(project => { return res(ctx.json(all_projects_response.find(p => p.id === project.id))); }); }); -const httpProjectCatalogDynamic = all_projects_response.map(project => { - const path: string = project.path_with_namespace - ? project.path_with_namespace!.replace(/\//g, '%2F') - : `${project.path_with_namespace}%2F${project.name}`; - return rest.head( - `${apiBaseUrl}/projects/${path}/repository/files/catalog-info.yaml`, - (req, res, ctx) => { - const branch = req.url.searchParams.get('ref'); - if ( - branch === project.default_branch || - branch === 'main' || - branch === 'develop' - ) { - return res(ctx.status(200)); - } - return res(ctx.status(404, 'Not Found')); - }, +/** + * Checks for both project id and namespaced path, as these can both be used for the :id segment in Gitlab API: + * https://docs.gitlab.com/api/repository_files/#get-file-from-repository + */ +const httpProjectCatalogDynamic = all_projects_response.flatMap(project => { + const possibleIdSegments: string[] = [ + project.id.toString(), + project.path_with_namespace ?? '', + ]; + + return possibleIdSegments.map(seg => + rest.head( + `${apiBaseUrl}/projects/${encodeURIComponent( + seg, + )}/repository/files/catalog-info.yaml`, + (req, res, ctx) => { + const branch = req.url.searchParams.get('ref'); + if ( + branch === project.default_branch || + branch === 'main' || + branch === 'develop' + ) { + return res(ctx.status(200)); + } + return res(ctx.status(404, 'Not Found')); + }, + ), ); }); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index 9b3bac6399..5a60d18493 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -580,7 +580,12 @@ describe('hasFile', () => { }); }); - it('should find catalog file', async () => { + it('should find catalog file by id', async () => { + const hasFile = await client.hasFile('1', 'main', 'catalog-info.yaml'); + expect(hasFile).toBe(true); + }); + + it('should find catalog file by namespace path', async () => { const hasFile = await client.hasFile( 'group1/test-repo1', 'main', From 047681317a79bd32d0a7b7be46acb5ee120f65ae Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 22 Sep 2025 10:49:28 +0100 Subject: [PATCH 045/193] update jsdoc for hasFile for the projectPath param Signed-off-by: Claire Peng --- plugins/catalog-backend-module-gitlab/src/lib/client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 3178e2c0d1..fcdf135199 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -342,8 +342,9 @@ export class GitLabClient { /** * General existence check. + * @see {@link https://docs.gitlab.com/api/repository_files/#get-file-from-repository | GitLab Repository Files API} * - * @param projectPath - The path to the project + * @param projectPath - The path to the project, either the numeric ID or the namespaced path. * @param branch - The branch used to search * @param filePath - The path to the file */ From 53aedd6a177f9db4622eb3a1ae285dd558bc3ab0 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 22 Sep 2025 11:09:29 +0100 Subject: [PATCH 046/193] rename projectPath param to project Signed-off-by: Claire Peng --- plugins/catalog-backend-module-gitlab/src/lib/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index fcdf135199..7860c66d1a 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -344,17 +344,17 @@ export class GitLabClient { * General existence check. * @see {@link https://docs.gitlab.com/api/repository_files/#get-file-from-repository | GitLab Repository Files API} * - * @param projectPath - The path to the project, either the numeric ID or the namespaced path. + * @param project - The path to the project, either the numeric ID or the namespaced path. * @param branch - The branch used to search * @param filePath - The path to the file */ async hasFile( - projectPath: string, + project: string, branch: string, filePath: string, ): Promise { const endpoint: string = `/projects/${encodeURIComponent( - projectPath, + project, )}/repository/files/${encodeURIComponent(filePath)}`; const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); request.searchParams.append('ref', branch); From d84bf179485dd6ebf8d804171d2a48ab88943a1c Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 22 Sep 2025 12:49:07 +0100 Subject: [PATCH 047/193] update client.hasFile to prefer project.id, then namespacedPath, then empty string fallback Signed-off-by: Claire Peng --- plugins/catalog-backend-module-gitlab/src/lib/client.ts | 6 +++--- .../src/providers/GitlabDiscoveryEntityProvider.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 7860c66d1a..46ff54846c 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -344,17 +344,17 @@ export class GitLabClient { * General existence check. * @see {@link https://docs.gitlab.com/api/repository_files/#get-file-from-repository | GitLab Repository Files API} * - * @param project - The path to the project, either the numeric ID or the namespaced path. + * @param projectIdentifier - The identifier of the project, either the numeric ID or the namespaced path. * @param branch - The branch used to search * @param filePath - The path to the file */ async hasFile( - project: string, + projectIdentifier: string, branch: string, filePath: string, ): Promise { const endpoint: string = `/projects/${encodeURIComponent( - project, + projectIdentifier, )}/repository/files/${encodeURIComponent(filePath)}`; const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); request.searchParams.append('ref', branch); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 11b6a0ce04..24bc5faccb 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -590,7 +590,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { this.config.fallbackBranch; const hasFile = await client.hasFile( - project.path_with_namespace ?? '', + project.id.toString() ?? project.path_with_namespace ?? '', project_branch, this.config.catalogFile, ); From bcac475df24d338433e3df8e86bcd58826d8995e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 22 Sep 2025 13:42:16 +0100 Subject: [PATCH 048/193] update shouldProcessProject to first check by namespacedPath (as previous), then check by id if failed Signed-off-by: Claire Peng --- .../src/providers/GitlabDiscoveryEntityProvider.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 24bc5faccb..869374de07 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -589,12 +589,22 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { project.default_branch ?? this.config.fallbackBranch; - const hasFile = await client.hasFile( - project.id.toString() ?? project.path_with_namespace ?? '', + // Find file with namespaced path (most use-cases) + let hasFile = await client.hasFile( + project.path_with_namespace ?? '', project_branch, this.config.catalogFile, ); + // Find file with project id if namespace failed + if (!hasFile) { + hasFile = await client.hasFile( + project.id.toString(), + project_branch, + this.config.catalogFile, + ); + } + return hasFile; } } From f5e09631a43afd659c30b318e1bef9a03bc18bd8 Mon Sep 17 00:00:00 2001 From: Hope Hadfield Date: Thu, 18 Sep 2025 11:15:16 -0400 Subject: [PATCH 049/193] Remove unused dependencies in notifications and scaffolder Signed-off-by: Hope Hadfield --- .changeset/shiny-candles-hide.md | 9 +++++++++ .../knip-report.md | 5 ----- .../package.json | 1 - plugins/notifications-backend/knip-report.md | 14 -------------- plugins/notifications-backend/package.json | 7 +------ plugins/notifications/knip-report.md | 13 ------------- plugins/notifications/package.json | 4 ---- .../knip-report.md | 5 ----- .../package.json | 3 +-- .../knip-report.md | 5 ----- .../scaffolder-backend-module-gitlab/package.json | 1 - yarn.lock | 12 ------------ 12 files changed, 11 insertions(+), 68 deletions(-) create mode 100644 .changeset/shiny-candles-hide.md diff --git a/.changeset/shiny-candles-hide.md b/.changeset/shiny-candles-hide.md new file mode 100644 index 0000000000..7ec4a8062e --- /dev/null +++ b/.changeset/shiny-candles-hide.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-notifications-backend-module-email': patch +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications': patch +--- + +Removed unused dependencies diff --git a/plugins/notifications-backend-module-email/knip-report.md b/plugins/notifications-backend-module-email/knip-report.md index a36d75cae4..97d5b385fd 100644 --- a/plugins/notifications-backend-module-email/knip-report.md +++ b/plugins/notifications-backend-module-email/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused dependencies (1) - -| Name | Location | Severity | -| :------------- | :----------- | :------- | -| @aws-sdk/types | plugins/notifications-backend-module-email/package.json | error | diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index ac8682fe2d..37b0f4d64d 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -35,7 +35,6 @@ }, "dependencies": { "@aws-sdk/client-ses": "^3.550.0", - "@aws-sdk/types": "^3.347.0", "@azure/communication-email": "^1.0.0", "@azure/identity": "^4.0.0", "@backstage/backend-plugin-api": "workspace:^", diff --git a/plugins/notifications-backend/knip-report.md b/plugins/notifications-backend/knip-report.md index 3514fea30a..97d5b385fd 100644 --- a/plugins/notifications-backend/knip-report.md +++ b/plugins/notifications-backend/knip-report.md @@ -1,17 +1,3 @@ # Knip report -## Unused dependencies (4) - -| Name | Location | Severity | -| :---------------------------- | :----------- | :------- | -| @backstage/plugin-events-node | plugins/notifications-backend/package.json | error | -| @backstage/plugin-auth-node | plugins/notifications-backend/package.json | error | -| winston | plugins/notifications-backend/package.json | error | -| yn | plugins/notifications-backend/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :-- | :----------- | :------- | -| msw | plugins/notifications-backend/package.json | error | diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 1a2c746409..975e2fec0a 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -43,9 +43,7 @@ "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", - "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-notifications-node": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", @@ -54,9 +52,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "p-throttle": "^4.1.1", - "uuid": "^11.0.0", - "winston": "^3.2.1", - "yn": "^4.0.0" + "uuid": "^11.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", @@ -68,7 +64,6 @@ "@backstage/plugin-signals-backend": "workspace:^", "@types/express": "^4.17.6", "@types/supertest": "^2.0.8", - "msw": "^1.0.0", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/notifications/knip-report.md b/plugins/notifications/knip-report.md index 6cd21cf3b8..97d5b385fd 100644 --- a/plugins/notifications/knip-report.md +++ b/plugins/notifications/knip-report.md @@ -1,16 +1,3 @@ # Knip report -## Unused dependencies (2) - -| Name | Location | Severity | -| :--------------- | :----------- | :------- | -| @backstage/types | plugins/notifications/package.json | error | -| @material-ui/lab | plugins/notifications/package.json | error | - -## Unused devDependencies (2) - -| Name | Location | Severity | -| :-------------------------- | :----------- | :------- | -| @testing-library/user-event | plugins/notifications/package.json | error | -| @backstage/core-app-api | plugins/notifications/package.json | error | diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index e6d66a5ec7..cbc06d242e 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -59,10 +59,8 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-signals-react": "workspace:^", "@backstage/theme": "workspace:^", - "@backstage/types": "workspace:^", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "^4.0.0-alpha.61", "lodash": "^4.17.21", "material-ui-confirm": "^3.0.12", "notistack": "^3.0.1", @@ -71,13 +69,11 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", "@backstage/plugin-signals": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", - "@testing-library/user-event": "^14.0.0", "@types/react": "^18.0.0", "msw": "^1.0.0", "react": "^18.0.2", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md b/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md index 0dd672d03d..97d5b385fd 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused dependencies (1) - -| Name | Location | Severity | -| :-- | :----------- | :------- | -| zod | plugins/scaffolder-backend-module-bitbucket-server/package.json | error | diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 452653afb7..c980146970 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -48,8 +48,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "yaml": "^2.0.0", - "zod": "^3.22.4" + "yaml": "^2.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-gitlab/knip-report.md b/plugins/scaffolder-backend-module-gitlab/knip-report.md index 3e1fc873d4..97d5b385fd 100644 --- a/plugins/scaffolder-backend-module-gitlab/knip-report.md +++ b/plugins/scaffolder-backend-module-gitlab/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused dependencies (1) - -| Name | Location | Severity | -| :------ | :----------- | :------- | -| winston | plugins/scaffolder-backend-module-gitlab/package.json | error | diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index f8194a434c..c1fb8024e9 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -53,7 +53,6 @@ "@gitbeaker/requester-utils": "^41.2.0", "@gitbeaker/rest": "^41.2.0", "luxon": "^3.0.0", - "winston": "^3.2.1", "yaml": "^2.0.0", "zod": "^3.22.4" }, diff --git a/yarn.lock b/yarn.lock index 483bd8e5b8..d0948f8474 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5715,7 +5715,6 @@ __metadata: resolution: "@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email" dependencies: "@aws-sdk/client-ses": "npm:^3.550.0" - "@aws-sdk/types": "npm:^3.347.0" "@azure/communication-email": "npm:^1.0.0" "@azure/identity": "npm:^4.0.0" "@backstage/backend-plugin-api": "workspace:^" @@ -5775,10 +5774,8 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" - "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-backend": "workspace:^" - "@backstage/plugin-events-node": "workspace:^" "@backstage/plugin-notifications-common": "workspace:^" "@backstage/plugin-notifications-node": "workspace:^" "@backstage/plugin-signals-backend": "workspace:^" @@ -5789,12 +5786,9 @@ __metadata: express: "npm:^4.17.1" express-promise-router: "npm:^4.1.0" knex: "npm:^3.0.0" - msw: "npm:^1.0.0" p-throttle: "npm:^4.1.1" supertest: "npm:^7.0.0" uuid: "npm:^11.0.0" - winston: "npm:^3.2.1" - yn: "npm:^4.0.0" languageName: unknown linkType: soft @@ -5832,7 +5826,6 @@ __metadata: resolution: "@backstage/plugin-notifications@workspace:plugins/notifications" dependencies: "@backstage/cli": "workspace:^" - "@backstage/core-app-api": "workspace:^" "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" @@ -5844,13 +5837,10 @@ __metadata: "@backstage/plugin-signals-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@backstage/types": "workspace:^" "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" - "@material-ui/lab": "npm:^4.0.0-alpha.61" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" - "@testing-library/user-event": "npm:^14.0.0" "@types/react": "npm:^18.0.0" lodash: "npm:^4.17.21" material-ui-confirm: "npm:^3.0.12" @@ -6147,7 +6137,6 @@ __metadata: fs-extra: "npm:^11.2.0" msw: "npm:^1.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.22.4" languageName: unknown linkType: soft @@ -6307,7 +6296,6 @@ __metadata: "@gitbeaker/requester-utils": "npm:^41.2.0" "@gitbeaker/rest": "npm:^41.2.0" luxon: "npm:^3.0.0" - winston: "npm:^3.2.1" yaml: "npm:^2.0.0" zod: "npm:^3.22.4" languageName: unknown From 226ef498e224ceb58c2c90b98683c0607785d644 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 23 Sep 2025 08:51:35 +0100 Subject: [PATCH 050/193] simplify client.hasFile to always use project.id in shouldProcessProject Signed-off-by: Claire Peng --- .../src/providers/GitlabDiscoveryEntityProvider.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 869374de07..e52c231d46 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -589,22 +589,12 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { project.default_branch ?? this.config.fallbackBranch; - // Find file with namespaced path (most use-cases) - let hasFile = await client.hasFile( - project.path_with_namespace ?? '', + const hasFile = await client.hasFile( + project.id.toString(), project_branch, this.config.catalogFile, ); - // Find file with project id if namespace failed - if (!hasFile) { - hasFile = await client.hasFile( - project.id.toString(), - project_branch, - this.config.catalogFile, - ); - } - return hasFile; } } From 0443119e7a3a1dd1b086d715a0bf0ff10aa29cf5 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 23 Sep 2025 09:16:57 +0100 Subject: [PATCH 051/193] add changeset Signed-off-by: Claire Peng --- .changeset/twelve-oranges-grin.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .changeset/twelve-oranges-grin.md diff --git a/.changeset/twelve-oranges-grin.md b/.changeset/twelve-oranges-grin.md new file mode 100644 index 0000000000..b508a59b0b --- /dev/null +++ b/.changeset/twelve-oranges-grin.md @@ -0,0 +1,20 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Update GitlabDiscoveryEntityProvider to use `project.id` rather than `project.path_with_namespace` for `hasFile` check in `shouldProcessProject` + +Solves [#30147](https://github.com/backstage/backstage/issues/30147): + +> Use of project.id avoids edgecases caused by path encoding or project structure changes +> [...] +> It also simplifies reasoning about the GitLab API usage, since IDs are immutable, while paths are not. + +```diff +const hasFile = await client.hasFile( +- project.path_with_namespace, ++ project.id.toString(), + project_branch, + this.config.catalogFile, +); +``` From a9b88beaaae397c4146bb11999ed98d10f0ccbba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 23 Sep 2025 12:08:01 +0200 Subject: [PATCH 052/193] fix(bui): Enable tooltips on disabled buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .changeset/moody-singers-deny.md | 5 +++++ .../ui/src/components/Button/Button.stories.tsx | 10 ++++++++++ packages/ui/src/components/Button/Button.tsx | 16 ++++++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .changeset/moody-singers-deny.md diff --git a/.changeset/moody-singers-deny.md b/.changeset/moody-singers-deny.md new file mode 100644 index 0000000000..28072baf4b --- /dev/null +++ b/.changeset/moody-singers-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Enable tooltips on disabled buttons with automatic wrapper diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index db8e999f98..1604c04eb4 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -19,6 +19,7 @@ import { Button } from './Button'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { Icon } from '../Icon'; +import { Tooltip, TooltipTrigger } from '../Tooltip'; const meta = { title: 'Backstage UI/Button', @@ -216,3 +217,12 @@ export const Playground: Story = { ), }; + +export const DisabledWithTooltips: Story = { + render: () => ( + + + Why this is disabled + + ), +}; diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index 40879e51ed..9f658fd00a 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -16,7 +16,7 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; -import { Button as RAButton } from 'react-aria-components'; +import { Button as RAButton, Focusable } from 'react-aria-components'; import type { ButtonProps } from './types'; import { useStyles } from '../../hooks/useStyles'; @@ -30,6 +30,7 @@ export const Button = forwardRef( iconEnd, children, className, + isDisabled, ...rest } = props; @@ -38,18 +39,29 @@ export const Button = forwardRef( variant, }); - return ( + const btn = ( {iconStart} {children} {iconEnd} ); + + return isDisabled ? ( + + + {btn} + + + ) : ( + btn + ); }, ); From 4adbb03a272089649fbfbda3d834644fb0b9e971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 23 Sep 2025 14:26:18 +0200 Subject: [PATCH 053/193] fix(bui): Fix SearchField onChange prop and add onChange story MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .changeset/tired-mice-cheer.md | 5 ++++ .../SearchField/SearchField.stories.tsx | 24 ++++++++++++++++++- .../components/SearchField/SearchField.tsx | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .changeset/tired-mice-cheer.md diff --git a/.changeset/tired-mice-cheer.md b/.changeset/tired-mice-cheer.md new file mode 100644 index 0000000000..929aae0f59 --- /dev/null +++ b/.changeset/tired-mice-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Avoid overriding onChange when spreading props diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx index a811d2a451..7cb89dfbfb 100644 --- a/packages/ui/src/components/SearchField/SearchField.stories.tsx +++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx @@ -51,6 +51,7 @@ export const Default: Story = { style: { maxWidth: '300px', }, + 'aria-label': 'Search', }, }; @@ -192,7 +193,7 @@ export const InHeader: Story = { size="small" variant="secondary" /> - + } @@ -272,3 +273,24 @@ export const StartCollapsedWithButtons: Story = { ), }; + +export const StartCollapsedWithOnChange: Story = { + args: { + ...StartCollapsed.args, + }, + render: args => { + const handleChange = (value: string) => { + console.log('Search value:', value); + }; + + return ( + + + + ); + }, +}; diff --git a/packages/ui/src/components/SearchField/SearchField.tsx b/packages/ui/src/components/SearchField/SearchField.tsx index e497cecd49..1c4bb7fa18 100644 --- a/packages/ui/src/components/SearchField/SearchField.tsx +++ b/packages/ui/src/components/SearchField/SearchField.tsx @@ -39,6 +39,7 @@ export const SearchField = forwardRef( secondaryLabel, description, isRequired, + onChange, placeholder = 'Search', startCollapsed = false, 'aria-label': ariaLabel, From 23829e022c5bd9eb7ead7efd1ef5056744db2e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 23 Sep 2025 15:13:32 +0200 Subject: [PATCH 054/193] fix(bui): Add missing Cell component props to table documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- docs-ui/src/content/components/table.props.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs-ui/src/content/components/table.props.ts b/docs-ui/src/content/components/table.props.ts index 1b40055849..d46c6b8ea7 100644 --- a/docs-ui/src/content/components/table.props.ts +++ b/docs-ui/src/content/components/table.props.ts @@ -212,6 +212,11 @@ export const cellPropDefs: Record = { description: "A string representation of the cell's contents, used for features like typeahead.", }, + leadingIcon: { + type: 'enum', + values: ['ReactNode'], + description: 'Optional icon to display before the cell content.', + }, ...classNamePropDefs, ...stylePropDefs, }; From 9acc1d6c9474b224664118888c643b1e358abe3d Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Tue, 23 Sep 2025 16:36:42 +0100 Subject: [PATCH 055/193] Adding show/hide visibility and clear action icons to BUI TextField Signed-off-by: Hermione Bird --- .changeset/eighty-cows-care.md | 5 ++ packages/ui/css/styles.css | 25 ++++++++ .../components/TextField/TextField.styles.css | 23 +++++++ .../ui/src/components/TextField/TextField.tsx | 64 ++++++++++++++++++- packages/ui/src/components/TextField/types.ts | 10 +++ packages/ui/src/utils/componentDefinitions.ts | 1 + 6 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 .changeset/eighty-cows-care.md diff --git a/.changeset/eighty-cows-care.md b/.changeset/eighty-cows-care.md new file mode 100644 index 0000000000..fc0a5c48bc --- /dev/null +++ b/.changeset/eighty-cows-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Added show/hide visibility and clear action icons to TextField diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index 8aa99175b3..e9bc5a289f 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -10451,6 +10451,31 @@ } } +.bui-InputAction { + position: absolute; + display: flex; + flex-direction: row; + right: var(--bui-space-3); + top: 50%; + transform: translateY(-50%); + color: var(--bui-fg-primary); + flex-shrink: 0; + /* To animate the icon when the input is collapsed */ + transition: right 0.2s ease-in-out; + + &[data-size='small'], + &[data-size='small'] svg { + width: 1rem; + height: 1rem; + } + + &[data-size='medium'], + &[data-size='medium'] svg { + width: 1.25rem; + height: 1.25rem; + } +} + .bui-InputIcon { left: var(--bui-space-3); margin-right: var(--bui-space-1); diff --git a/packages/ui/src/components/TextField/TextField.styles.css b/packages/ui/src/components/TextField/TextField.styles.css index 792d5d85c9..77b39396e1 100644 --- a/packages/ui/src/components/TextField/TextField.styles.css +++ b/packages/ui/src/components/TextField/TextField.styles.css @@ -116,3 +116,26 @@ border: 1px solid var(--bui-border-disabled); } } + +.bui-InputAction { + position: absolute; + right: var(--bui-space-3); + top: 50%; + transform: translateY(-50%); + color: var(--bui-fg-primary); + flex-shrink: 0; + /* To animate the icon when the input is collapsed */ + transition: right 0.2s ease-in-out; + + &[data-size='small'], + &[data-size='small'] svg { + width: 1rem; + height: 1rem; + } + + &[data-size='medium'], + &[data-size='medium'] svg { + width: 1.25rem; + height: 1.25rem; + } +} diff --git a/packages/ui/src/components/TextField/TextField.tsx b/packages/ui/src/components/TextField/TextField.tsx index 2756c3c85f..748dd4655a 100644 --- a/packages/ui/src/components/TextField/TextField.tsx +++ b/packages/ui/src/components/TextField/TextField.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { forwardRef, useEffect } from 'react'; +import { forwardRef, useEffect, useState } from 'react'; import { Input, TextField as AriaTextField } from 'react-aria-components'; import clsx from 'clsx'; import { FieldLabel } from '../FieldLabel'; @@ -22,6 +22,8 @@ import { FieldError } from '../FieldError'; import type { TextFieldProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { Icon } from '../Icon'; +import { ButtonIcon } from '../ButtonIcon'; /** @public */ export const TextField = forwardRef( @@ -34,6 +36,8 @@ export const TextField = forwardRef( secondaryLabel, description, isRequired, + enableVisibility, + isClearable, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, placeholder, @@ -56,6 +60,30 @@ export const TextField = forwardRef( const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null); + // Manage value for clearable behavior, supporting both controlled and uncontrolled usage + const { + value: controlledValue, + defaultValue, + onChange, + isDisabled, + } = rest as any; + const [uncontrolledValue, setUncontrolledValue] = useState( + defaultValue ?? '', + ); + const effectiveValue = + controlledValue !== undefined ? controlledValue : uncontrolledValue; + const handleChange = (value: string) => { + if (controlledValue === undefined) setUncontrolledValue(value); + onChange?.(value); + }; + + // Manage secret visibility toggle + const [isVisible, setIsVisible] = useState(false); + const showClear = Boolean( + isClearable && effectiveValue && effectiveValue.length > 0, + ); + const showSecretToggle = Boolean(enableVisibility); + return ( ( aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} {...rest} + value={effectiveValue} + onChange={handleChange} ref={ref} > ( {icon} )} + {(showClear || showSecretToggle) && ( +
+ {showSecretToggle && ( + setIsVisible(v => !v)} + icon={} + /> + )} + {showClear && ( + { + if (controlledValue === undefined) { + setUncontrolledValue(''); + } + onChange?.(''); + }} + icon={} + /> + )} +
+ )} diff --git a/packages/ui/src/components/TextField/types.ts b/packages/ui/src/components/TextField/types.ts index 75a5c68aaa..d64be41050 100644 --- a/packages/ui/src/components/TextField/types.ts +++ b/packages/ui/src/components/TextField/types.ts @@ -38,4 +38,14 @@ export interface TextFieldProps * Text to display in the input when it has no value */ placeholder?: string; + + /** + * Displays icon to toggle between showing and hiding the input value + */ + enableVisibility?: boolean; + + /** + * Displays a clear button when there is content in the input + */ + isClearable?: boolean; } diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index 5a0e8bdc1a..bf01a0dc63 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -286,6 +286,7 @@ export const componentDefinitions = { inputWrapper: 'bui-InputWrapper', input: 'bui-Input', inputIcon: 'bui-InputIcon', + inputAction: 'bui-InputAction', }, dataAttributes: { invalid: [true, false] as const, From d144d916c8b43f5954b3aefe426289325de75c83 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Tue, 23 Sep 2025 17:39:22 +0100 Subject: [PATCH 056/193] reduce right spacing Signed-off-by: Hermione Bird --- packages/ui/css/styles.css | 2 +- packages/ui/src/components/TextField/TextField.styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index e9bc5a289f..1ea7d73bf7 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -10455,7 +10455,7 @@ position: absolute; display: flex; flex-direction: row; - right: var(--bui-space-3); + right: var(--bui-space-1); top: 50%; transform: translateY(-50%); color: var(--bui-fg-primary); diff --git a/packages/ui/src/components/TextField/TextField.styles.css b/packages/ui/src/components/TextField/TextField.styles.css index 77b39396e1..c5765bf8ac 100644 --- a/packages/ui/src/components/TextField/TextField.styles.css +++ b/packages/ui/src/components/TextField/TextField.styles.css @@ -119,7 +119,7 @@ .bui-InputAction { position: absolute; - right: var(--bui-space-3); + right: var(--bui-space-1); top: 50%; transform: translateY(-50%); color: var(--bui-fg-primary); From 8192e82be1184d08e5b3811de8f6296c4c691111 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Tue, 23 Sep 2025 18:12:41 +0100 Subject: [PATCH 057/193] remove clear action Signed-off-by: Hermione Bird --- .../ui/src/components/TextField/TextField.tsx | 57 +++---------------- 1 file changed, 8 insertions(+), 49 deletions(-) diff --git a/packages/ui/src/components/TextField/TextField.tsx b/packages/ui/src/components/TextField/TextField.tsx index 748dd4655a..c4fc81f029 100644 --- a/packages/ui/src/components/TextField/TextField.tsx +++ b/packages/ui/src/components/TextField/TextField.tsx @@ -37,7 +37,6 @@ export const TextField = forwardRef( description, isRequired, enableVisibility, - isClearable, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, placeholder, @@ -60,28 +59,8 @@ export const TextField = forwardRef( const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null); - // Manage value for clearable behavior, supporting both controlled and uncontrolled usage - const { - value: controlledValue, - defaultValue, - onChange, - isDisabled, - } = rest as any; - const [uncontrolledValue, setUncontrolledValue] = useState( - defaultValue ?? '', - ); - const effectiveValue = - controlledValue !== undefined ? controlledValue : uncontrolledValue; - const handleChange = (value: string) => { - if (controlledValue === undefined) setUncontrolledValue(value); - onChange?.(value); - }; - // Manage secret visibility toggle const [isVisible, setIsVisible] = useState(false); - const showClear = Boolean( - isClearable && effectiveValue && effectiveValue.length > 0, - ); const showSecretToggle = Boolean(enableVisibility); return ( @@ -91,8 +70,6 @@ export const TextField = forwardRef( aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} {...rest} - value={effectiveValue} - onChange={handleChange} ref={ref} > ( {icon} )} - {(showClear || showSecretToggle) && ( + {showSecretToggle && (
- {showSecretToggle && ( - setIsVisible(v => !v)} - icon={} - /> - )} - {showClear && ( - { - if (controlledValue === undefined) { - setUncontrolledValue(''); - } - onChange?.(''); - }} - icon={} - /> - )} + setIsVisible(v => !v)} + icon={} + />
)} Date: Tue, 23 Sep 2025 20:43:05 +0300 Subject: [PATCH 058/193] fix: default notification recipient resolver exclusion Signed-off-by: Hellgren Heikki --- .changeset/slimy-signs-agree.md | 5 ++ ...faultNotificationRecipientResolver.test.ts | 10 +-- .../DefaultNotificationRecipientResolver.ts | 16 ++--- .../src/service/router.test.ts | 69 +++++++++++++++++++ 4 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 .changeset/slimy-signs-agree.md diff --git a/.changeset/slimy-signs-agree.md b/.changeset/slimy-signs-agree.md new file mode 100644 index 0000000000..c4307f0bd7 --- /dev/null +++ b/.changeset/slimy-signs-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +Fixed exclude entity reference not working in notification sending diff --git a/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts index cd17db3680..7faec0c23c 100644 --- a/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts +++ b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts @@ -34,7 +34,7 @@ describe('getUsersForEntityRef', () => { await expect( resolver.resolveNotificationRecipients({ entityRefs: ['user:foo', 'user:ignored'], - excludeEntityRefs: ['user:ignored'], + excludedEntityRefs: ['user:ignored'], }), ).resolves.toEqual({ userEntityRefs: ['user:foo'] }); expect(catalog.getEntitiesByRefs).not.toHaveBeenCalled(); @@ -87,7 +87,7 @@ describe('getUsersForEntityRef', () => { await expect( resolver.resolveNotificationRecipients({ entityRefs: ['group:default/parent_group'], - excludeEntityRefs: ['user:default/ignored'], + excludedEntityRefs: ['user:default/ignored'], }), ).resolves.toEqual({ userEntityRefs: ['user:default/foo', 'user:default/bar'], @@ -120,7 +120,7 @@ describe('getUsersForEntityRef', () => { await expect( resolver.resolveNotificationRecipients({ entityRefs: ['component:default/test_component'], - excludeEntityRefs: [], + excludedEntityRefs: [], }), ).resolves.toEqual({ userEntityRefs: ['user:default/foo'] }); }); @@ -164,7 +164,7 @@ describe('getUsersForEntityRef', () => { await expect( resolver.resolveNotificationRecipients({ entityRefs: ['component:default/test_component'], - excludeEntityRefs: [], + excludedEntityRefs: [], }), ).resolves.toEqual({ userEntityRefs: ['user:default/foo'] }); }); @@ -208,7 +208,7 @@ describe('getUsersForEntityRef', () => { await expect( resolver.resolveNotificationRecipients({ entityRefs: ['component:default/test_component'], - excludeEntityRefs: ['user:default/foo'], + excludedEntityRefs: ['user:default/foo'], }), ).resolves.toEqual({ userEntityRefs: [] }); }); diff --git a/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts index 2401ec7c1f..200b5dc4ea 100644 --- a/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts +++ b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts @@ -54,16 +54,16 @@ export class DefaultNotificationRecipientResolver async resolveNotificationRecipients(options: { entityRefs: string[]; - excludeEntityRefs?: string[]; + excludedEntityRefs?: string[]; }): Promise<{ userEntityRefs: string[] }> { - const { entityRefs, excludeEntityRefs = [] } = options; + const { entityRefs, excludedEntityRefs = [] } = options; const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(entityRefs); const users: string[] = userEntityRefs.filter( - ref => !excludeEntityRefs.includes(ref), + ref => !excludedEntityRefs.includes(ref), ); const filtered = otherEntityRefs.filter( - ref => !excludeEntityRefs.includes(ref), + ref => !excludedEntityRefs.includes(ref), ); const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; @@ -87,7 +87,7 @@ export class DefaultNotificationRecipientResolver } const currentEntityRef = stringifyEntityRef(entity); - if (excludeEntityRefs.includes(currentEntityRef)) { + if (excludedEntityRefs.includes(currentEntityRef)) { return []; } @@ -130,7 +130,7 @@ export class DefaultNotificationRecipientResolver const ret = [ ...new Set([...groupUsers, ...childGroupUsers.flat(2)]), - ].filter(ref => !excludeEntityRefs.includes(ref)); + ].filter(ref => !excludedEntityRefs.includes(ref)); cachedEntityRefs.set(currentEntityRef, ret); return ret; } @@ -145,7 +145,7 @@ export class DefaultNotificationRecipientResolver } if (isUserEntityRef(ownerRef)) { - if (excludeEntityRefs.includes(ownerRef)) { + if (excludedEntityRefs.includes(ownerRef)) { return []; } return [ownerRef]; @@ -171,7 +171,7 @@ export class DefaultNotificationRecipientResolver userEntityRefs: [...new Set(users)] .filter(Boolean) // Need to filter again after resolving users - .filter(ref => !excludeEntityRefs.includes(ref)), + .filter(ref => !excludedEntityRefs.includes(ref)), }; } } diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index 08aa021014..f42a7b4513 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -274,6 +274,31 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { expect(notifications).toHaveLength(1); }); + it('should not send to user entity if excluded', async () => { + const response = await sendNotification({ + recipients: { + type: 'entity', + entityRef: ['user:default/mock'], + excludeEntityRef: 'user:default/mock', + }, + payload: { + title: 'test notification', + metadata: { + attr: 1, + }, + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([]); + + const client = await database.getClient(); + const notifications = await client('notification') + .where('user', 'user:default/mock') + .select(); + expect(notifications).toHaveLength(0); + }); + it('should send to group entity', async () => { const response = await sendNotification({ recipients: { @@ -306,6 +331,50 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { expect(notifications).toHaveLength(1); }); + it('should send not send to group entity if excluded', async () => { + const response = await sendNotification({ + recipients: { + type: 'entity', + entityRef: ['group:default/mock'], + excludeEntityRef: 'group:default/mock', + }, + payload: { + title: 'test notification', + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([]); + + const client = await database.getClient(); + const notifications = await client('notification') + .where('user', 'user:default/mock') + .select(); + expect(notifications).toHaveLength(0); + }); + + it('should send not send to user entity if excluded', async () => { + const response = await sendNotification({ + recipients: { + type: 'entity', + entityRef: ['group:default/mock'], + excludeEntityRef: 'user:default/mock', + }, + payload: { + title: 'test notification', + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([]); + + const client = await database.getClient(); + const notifications = await client('notification') + .where('user', 'user:default/mock') + .select(); + expect(notifications).toHaveLength(0); + }); + it('should only send one notification per user', async () => { const response = await sendNotification({ recipients: { From 67a3e1a7a4285dda50c97b9a71b4a5949f400962 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Tue, 23 Sep 2025 11:58:54 -0400 Subject: [PATCH 059/193] Implement AbortController request cancellation for search. Signed-off-by: Sydney Achinger --- .changeset/ripe-yaks-brake.md | 6 + plugins/search-react/report.api.md | 14 +- plugins/search-react/src/api.ts | 10 +- .../SearchAutocomplete.test.tsx | 68 +++++--- .../components/SearchBar/SearchBar.test.tsx | 12 ++ .../SearchFilter/SearchFilter.test.tsx | 8 + .../SearchPagination.test.tsx | 12 ++ .../src/context/SearchContext.test.tsx | 151 +++++++++++------- .../src/context/SearchContext.tsx | 36 ++++- plugins/search/src/apis.test.ts | 17 +- plugins/search/src/apis.ts | 9 +- .../HomePageSearchBar.test.tsx | 3 + .../SearchModal/SearchModal.test.tsx | 21 ++- .../SearchType/SearchType.Accordion.test.tsx | 34 ++-- .../SearchType/SearchType.Accordion.tsx | 36 ++++- .../components/SearchType/SearchType.test.tsx | 13 +- 16 files changed, 322 insertions(+), 128 deletions(-) create mode 100644 .changeset/ripe-yaks-brake.md diff --git a/.changeset/ripe-yaks-brake.md b/.changeset/ripe-yaks-brake.md new file mode 100644 index 0000000000..953d0f3192 --- /dev/null +++ b/.changeset/ripe-yaks-brake.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-react': patch +'@backstage/plugin-search': patch +--- + +Implement AbortController request cancellation in SearchBar to prevent overlapping search requests. This change ensures that when users type quickly, previous search requests are properly canceled before new ones start. diff --git a/plugins/search-react/report.api.md b/plugins/search-react/report.api.md index f0ec6030fd..6c9a1cd09d 100644 --- a/plugins/search-react/report.api.md +++ b/plugins/search-react/report.api.md @@ -93,13 +93,23 @@ export class MockSearchApi implements SearchApi { // (undocumented) mockedResults?: SearchResultSet | undefined; // (undocumented) - query(): Promise; + query( + _query: SearchQuery, + _options?: { + signal?: AbortSignal; + }, + ): Promise; } // @public (undocumented) export interface SearchApi { // (undocumented) - query(query: SearchQuery): Promise; + query( + query: SearchQuery, + options?: { + signal?: AbortSignal; + }, + ): Promise; } // @public (undocumented) diff --git a/plugins/search-react/src/api.ts b/plugins/search-react/src/api.ts index 24a247a641..61da8a3d77 100644 --- a/plugins/search-react/src/api.ts +++ b/plugins/search-react/src/api.ts @@ -28,7 +28,10 @@ export const searchApiRef = createApiRef({ * @public */ export interface SearchApi { - query(query: SearchQuery): Promise; + query( + query: SearchQuery, + options?: { signal?: AbortSignal }, + ): Promise; } /** @@ -39,7 +42,10 @@ export interface SearchApi { export class MockSearchApi implements SearchApi { constructor(public mockedResults?: SearchResultSet) {} - query(): Promise { + query( + _query: SearchQuery, + _options?: { signal?: AbortSignal }, + ): Promise { return Promise.resolve(this.mockedResults || { results: [] }); } } diff --git a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx index 173d6c3450..36757dafff 100644 --- a/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx +++ b/plugins/search-react/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx @@ -95,12 +95,17 @@ describe('SearchAutocomplete', () => { ); await waitFor(() => { - expect(query).toHaveBeenCalledWith({ - filters: {}, - pageCursor: undefined, - term: options[0], - types: [], - }); + expect(query).toHaveBeenCalledWith( + { + filters: {}, + pageCursor: undefined, + term: options[0], + types: [], + }, + { + signal: expect.any(AbortSignal), + }, + ); }); }); @@ -117,23 +122,33 @@ describe('SearchAutocomplete', () => { ); await waitFor(() => { - expect(query).toHaveBeenCalledWith({ - filters: {}, - pageCursor: undefined, - term: options[0], - types: [], - }); + expect(query).toHaveBeenCalledWith( + { + filters: {}, + pageCursor: undefined, + term: options[0], + types: [], + }, + { + signal: expect.any(AbortSignal), + }, + ); }); await userEvent.click(screen.getByLabelText('Clear')); await waitFor(() => { - expect(query).toHaveBeenCalledWith({ - filters: {}, - pageCursor: undefined, - term: '', - types: [], - }); + expect(query).toHaveBeenCalledWith( + { + filters: {}, + pageCursor: undefined, + term: '', + types: [], + }, + { + signal: expect.any(AbortSignal), + }, + ); }); }); @@ -154,12 +169,17 @@ describe('SearchAutocomplete', () => { await userEvent.click(screen.getByText(options[0])); await waitFor(() => { - expect(query).toHaveBeenCalledWith({ - filters: {}, - pageCursor: undefined, - term: options[0], - types: [], - }); + expect(query).toHaveBeenCalledWith( + { + filters: {}, + pageCursor: undefined, + term: options[0], + types: [], + }, + { + signal: expect.any(AbortSignal), + }, + ); }); }); diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 54889792db..977f413f97 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -171,6 +171,9 @@ describe('SearchBar', () => { expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), + { + signal: expect.any(AbortSignal), + }, ); jest.runAllTimers(); @@ -203,6 +206,9 @@ describe('SearchBar', () => { expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ term: '' }), + { + signal: expect.any(AbortSignal), + }, ); }); @@ -254,6 +260,9 @@ describe('SearchBar', () => { await waitFor(() => expect(searchApiMock.query).not.toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), + expect.objectContaining({ + signal: expect.any(AbortSignal), + }), ), ); @@ -265,6 +274,9 @@ describe('SearchBar', () => { expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), + { + signal: expect.any(AbortSignal), + }, ); jest.runAllTimers(); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx index d8d3b75aa2..7191b842d3 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx @@ -178,6 +178,7 @@ describe('SearchFilter', () => { await waitFor(() => { expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: { field: [values[0]] } }), + { signal: expect.any(Object) }, ); }); @@ -186,6 +187,7 @@ describe('SearchFilter', () => { await waitFor(() => { expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters: {} }), + { signal: expect.any(Object) }, ); }); }); @@ -217,6 +219,7 @@ describe('SearchFilter', () => { expect.objectContaining({ filters: { ...filters, field: [values[0]] }, }), + { signal: expect.any(Object) }, ); }); @@ -225,6 +228,7 @@ describe('SearchFilter', () => { await waitFor(() => { expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters }), + { signal: expect.any(Object) }, ); }); }); @@ -421,6 +425,7 @@ describe('SearchFilter', () => { expect.objectContaining({ filters: { [name]: values[0] }, }), + { signal: expect.any(AbortSignal) }, ); }); @@ -437,6 +442,7 @@ describe('SearchFilter', () => { expect.objectContaining({ filters: {}, }), + { signal: expect.any(AbortSignal) }, ); }); }); @@ -479,6 +485,7 @@ describe('SearchFilter', () => { expect.objectContaining({ filters: { ...filters, [name]: values[0] }, }), + { signal: expect.any(AbortSignal) }, ); }); @@ -493,6 +500,7 @@ describe('SearchFilter', () => { await waitFor(() => { expect(searchApiMock.query).toHaveBeenLastCalledWith( expect.objectContaining({ filters }), + { signal: expect.any(AbortSignal) }, ); }); }); diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index d601caadd9..e482dc3a75 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -197,6 +197,9 @@ describe('SearchPagination', () => { expect.objectContaining({ pageLimit: 10, }), + { + signal: expect.any(AbortSignal), + }, ); }); @@ -229,6 +232,9 @@ describe('SearchPagination', () => { expect.objectContaining({ pageCursor: 'Mg==', // page: 2 }), + { + signal: expect.any(AbortSignal), + }, ); await userEvent.click(screen.getByLabelText('Previous page')); @@ -239,6 +245,9 @@ describe('SearchPagination', () => { expect.objectContaining({ pageCursor: 'MQ==', // page: 1 }), + { + signal: expect.any(AbortSignal), + }, ); }); @@ -271,6 +280,9 @@ describe('SearchPagination', () => { pageCursor: undefined, pageLimit: 10, }), + { + signal: expect.any(AbortSignal), + }, ); }); }); diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index f604183c7f..cbdf684b70 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -264,11 +264,14 @@ describe('SearchContext', () => { result.current.setTerm(term); }); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - term, - types: ['*'], - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + term, + types: ['*'], + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); }); it('When types is set', async () => { @@ -286,11 +289,14 @@ describe('SearchContext', () => { result.current.setTypes(types); }); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - types, - term: '', - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + types, + term: '', + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); }); it('When filters are set', async () => { @@ -308,11 +314,14 @@ describe('SearchContext', () => { result.current.setFilters(filters); }); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - filters, - term: '', - types: ['*'], - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + filters, + term: '', + types: ['*'], + }, + { signal: expect.any(AbortSignal) }, + ); }); it('When page limit is set', async () => { @@ -330,12 +339,15 @@ describe('SearchContext', () => { result.current.setPageLimit(pageLimit); }); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - pageLimit, - term: '', - types: ['*'], - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + pageLimit, + term: '', + types: ['*'], + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); }); it('When page cursor is set', async () => { @@ -353,12 +365,15 @@ describe('SearchContext', () => { result.current.setPageCursor(pageCursor); }); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - pageCursor, - term: '', - types: ['*'], - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + pageCursor, + term: '', + types: ['*'], + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); }); it('provides function for fetch the next page', async () => { @@ -382,12 +397,15 @@ describe('SearchContext', () => { result.current.fetchNextPage!(); }); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - term: '', - types: ['*'], - filters: {}, - pageCursor: 'NEXT', - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + term: '', + types: ['*'], + filters: {}, + pageCursor: 'NEXT', + }, + { signal: expect.any(AbortSignal) }, + ); }); it('provides function for fetch the previous page', async () => { @@ -410,12 +428,15 @@ describe('SearchContext', () => { result.current.fetchPreviousPage!(); }); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - term: '', - types: ['*'], - filters: {}, - pageCursor: 'PREVIOUS', - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + term: '', + types: ['*'], + filters: {}, + pageCursor: 'PREVIOUS', + }, + { signal: expect.any(AbortSignal) }, + ); }); }); @@ -456,11 +477,14 @@ describe('SearchContext', () => { }); await waitFor(() => { - expect(searchApiMock.query).toHaveBeenCalledWith({ - term: '', - types: ['*'], - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenCalledWith( + { + term: '', + types: ['*'], + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); @@ -470,11 +494,14 @@ describe('SearchContext', () => { }); await waitFor(() => { - expect(searchApiMock.query).toHaveBeenCalledWith({ - term: 'eva', - types: ['*'], - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenCalledWith( + { + term: 'eva', + types: ['*'], + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'search', subject: 'eva', @@ -493,11 +520,14 @@ describe('SearchContext', () => { }); await waitFor(() => { - expect(searchApiMock.query).toHaveBeenCalledWith({ - term: 'eva.m', - types: ['*'], - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenCalledWith( + { + term: 'eva.m', + types: ['*'], + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'search', subject: 'eva.m', @@ -531,11 +561,14 @@ describe('SearchContext', () => { }); await waitFor(() => { - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - term: 'term', - types: ['*'], - filters: {}, - }); + expect(searchApiMock.query).toHaveBeenLastCalledWith( + { + term: 'term', + types: ['*'], + filters: {}, + }, + { signal: expect.any(AbortSignal) }, + ); expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'search', subject: 'term', diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index 3bd29c881a..ba06010789 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -23,6 +23,7 @@ import { useCallback, useContext, useEffect, + useRef, useState, } from 'react'; import useAsync, { AsyncState } from 'react-use/esm/useAsync'; @@ -132,15 +133,28 @@ const useSearchContextValue = ( const prevTerm = usePrevious(term); const prevFilters = usePrevious(filters); + const abortControllerRef = useRef(null); const result = useAsync(async () => { - const resultSet = await searchApi.query({ - term, - types, - filters, - pageLimit, - pageCursor, - }); + // Here we cancel the previous request before making a new one + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const controller = new AbortController(); + abortControllerRef.current = controller; + + const resultSet = await searchApi.query( + { + term, + types, + filters, + pageLimit, + pageCursor, + }, + { signal: controller.signal }, + ); + if (term) { analytics.captureEvent('search', term, { value: resultSet.numberOfResults, @@ -162,6 +176,14 @@ const useSearchContextValue = ( setPageCursor(result.value?.previousPageCursor); }, [result.value?.previousPageCursor]); + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + useEffect(() => { // Any time a term is reset, we want to start from page 0. // Only reset the term if it has been modified by the user at least once, the initial state must not reset the term. diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 15745d2e68..1bb245dc9f 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -52,10 +52,19 @@ describe('apis', () => { identityApi.getCredentials.mockResolvedValue({}); await client.query(query); expect(getBaseUrl).toHaveBeenLastCalledWith('search'); - expect(mockFetch).toHaveBeenLastCalledWith( - `${baseUrl}/query?term=`, - undefined, - ); + expect(mockFetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, { + signal: undefined, + }); + }); + + it('Fetch is called with abort signal when provided', async () => { + identityApi.getCredentials.mockResolvedValue({}); + const abortController = new AbortController(); + await client.query(query, { signal: abortController.signal }); + expect(getBaseUrl).toHaveBeenLastCalledWith('search'); + expect(mockFetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, { + signal: abortController.signal, + }); }); it('Resolves JSON from fetch response', async () => { diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index b56d88a22c..03e6b6d1d4 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -30,12 +30,17 @@ export class SearchClient implements SearchApi { this.fetchApi = options.fetchApi; } - async query(query: SearchQuery): Promise { + async query( + query: SearchQuery, + options?: { signal?: AbortSignal }, + ): Promise { const queryString = qs.stringify(query); const url = `${await this.discoveryApi.getBaseUrl( 'search', )}/query?${queryString}`; - const response = await this.fetchApi.fetch(url); + const response = await this.fetchApi.fetch(url, { + signal: options?.signal, + }); if (!response.ok) { throw await ResponseError.fromResponse(response); diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.test.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.test.tsx index 37b7c13665..e7c47b7d30 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.test.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.test.tsx @@ -48,6 +48,9 @@ describe('', () => { expect(searchApiMock.query).toHaveBeenCalledWith( expect.objectContaining({ term: '' }), + { + signal: expect.any(AbortSignal), + }, ); await userEvent.type(screen.getByLabelText('Search'), 'term{enter}'); diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index c404f54e06..8d8e1bb9b9 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -88,7 +88,9 @@ describe('SearchModal', () => { ); expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(searchApiMock.query).toHaveBeenCalledWith(initialState); + expect(searchApiMock.query).toHaveBeenCalledWith(initialState, { + signal: expect.any(AbortSignal), + }); }); it('Should create a local search context if a parent is not defined', async () => { @@ -104,12 +106,15 @@ describe('SearchModal', () => { ); expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(searchApiMock.query).toHaveBeenCalledWith({ - term: '', - filters: {}, - types: [], - pageCursor: undefined, - }); + expect(searchApiMock.query).toHaveBeenCalledWith( + { + term: '', + filters: {}, + types: [], + pageCursor: undefined, + }, + { signal: expect.any(AbortSignal) }, + ); }); it('Should render a custom Modal correctly', async () => { @@ -200,6 +205,7 @@ describe('SearchModal', () => { expect(searchApiMock.query).toHaveBeenCalledWith( expect.objectContaining({ term: 'term' }), + { signal: expect.any(AbortSignal) }, ); const input = screen.getByLabelText('Search'); @@ -232,6 +238,7 @@ describe('SearchModal', () => { expect(searchApiMock.query).toHaveBeenCalledWith( expect.objectContaining({ term: 'term' }), + { signal: expect.any(AbortSignal) }, ); const fullResultsBtn = screen.getByRole('button', { diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index 1245ceed5a..a0dd2976e6 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -157,18 +157,28 @@ describe('SearchType.Accordion', () => {
, ); - expect(searchApiMock.query).toHaveBeenCalledWith({ - term: 'abc', - types: [], - filters: { foo: 'bar' }, - pageLimit: 0, - }); - expect(searchApiMock.query).toHaveBeenCalledWith({ - term: 'abc', - types: [expectedType.value], - filters: {}, - pageLimit: 0, - }); + expect(searchApiMock.query).toHaveBeenCalledWith( + { + term: 'abc', + types: [], + filters: { foo: 'bar' }, + pageLimit: 0, + }, + { + signal: expect.any(AbortSignal), + }, + ); + expect(searchApiMock.query).toHaveBeenCalledWith( + { + term: 'abc', + types: [expectedType.value], + filters: {}, + pageLimit: 0, + }, + { + signal: expect.any(AbortSignal), + }, + ); await waitFor(() => { const countLabels = getAllByText('1234 results'); expect(countLabels.length).toEqual(2); diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx index 65c96b1e60..8168418ed2 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { cloneElement, Fragment, useEffect, useState } from 'react'; +import { cloneElement, Fragment, useEffect, useRef, useState } from 'react'; import { useApi } from '@backstage/core-plugin-api'; import { searchApiRef, useSearch } from '@backstage/plugin-search-react'; import Accordion from '@material-ui/core/Accordion'; @@ -86,6 +86,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { const [expanded, setExpanded] = useState(true); const { defaultValue, name, showCounts, types: givenTypes } = props; const { t } = useTranslationRef(searchTranslationRef); + const abortControllerRef = useRef(null); const toggleExpanded = () => setExpanded(prevState => !prevState); const handleClick = (type: string) => { @@ -117,18 +118,29 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { if (!showCounts) { return {}; } + // Here we cancel the previous requests before making a new one + // All requests are made with a new AbortController signal + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const controller = new AbortController(); + abortControllerRef.current = controller; const counts = await Promise.all( definedTypes .map(type => type.value) .map(async type => { - const { numberOfResults } = await searchApi.query({ - term, - types: type ? [type] : [], - filters: - types.includes(type) || (!types.length && !type) ? filters : {}, - pageLimit: 0, - }); + const { numberOfResults } = await searchApi.query( + { + term, + types: type ? [type] : [], + filters: + types.includes(type) || (!types.length && !type) ? filters : {}, + pageLimit: 0, + }, + { signal: controller.signal }, + ); return [ type, @@ -145,6 +157,14 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { return Object.fromEntries(counts); }, [filters, showCounts, term, types]); + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + return ( diff --git a/plugins/search/src/components/SearchType/SearchType.test.tsx b/plugins/search/src/components/SearchType/SearchType.test.tsx index 231463c9bf..e62ac19fee 100644 --- a/plugins/search/src/components/SearchType/SearchType.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.test.tsx @@ -192,6 +192,9 @@ describe('SearchType', () => { expect.objectContaining({ types: [values[0]], }), + { + signal: expect.any(AbortSignal), + }, ); }); @@ -240,6 +243,9 @@ describe('SearchType', () => { expect.objectContaining({ types: [...typeValues, values[0]], }), + { + signal: expect.any(AbortSignal), + }, ); }); @@ -253,7 +259,12 @@ describe('SearchType', () => { await waitFor(() => { expect(searchApiMock.query).toHaveBeenLastCalledWith( - expect.objectContaining([]), + expect.objectContaining({ + types: typeValues, + }), + { + signal: expect.any(AbortSignal), + }, ); }); }); From e02473eeb39d4c48b4bbada88a9c67a6cd726232 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Tue, 23 Sep 2025 16:15:43 -0400 Subject: [PATCH 060/193] Update test Signed-off-by: Sydney Achinger --- .../search/components/TechDocsSearch.test.tsx | 92 +++++++++++-------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx b/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx index f7e67e12cf..c140e830ad 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx @@ -84,16 +84,21 @@ it('should trigger query when autocomplete input changed', async () => { ); await singleResult; - expect(querySpy).toHaveBeenCalledWith({ - filters: { - kind: 'Testable', - name: 'test', - namespace: 'testspace', + expect(querySpy).toHaveBeenCalledWith( + { + filters: { + kind: 'Testable', + name: 'test', + namespace: 'testspace', + }, + pageCursor: '', + term: '', + types: ['techdocs'], }, - pageCursor: '', - term: '', - types: ['techdocs'], - }); + { + signal: expect.any(AbortSignal), + }, + ); const autocomplete = screen.getByTestId('techdocs-search-bar'); const input = within(autocomplete).getByRole('textbox'); @@ -105,16 +110,21 @@ it('should trigger query when autocomplete input changed', async () => { await singleResult; await waitFor(() => - expect(querySpy).toHaveBeenCalledWith({ - filters: { - kind: 'Testable', - name: 'test', - namespace: 'testspace', + expect(querySpy).toHaveBeenCalledWith( + { + filters: { + kind: 'Testable', + name: 'test', + namespace: 'testspace', + }, + pageCursor: '', + term: 'asdf', + types: ['techdocs'], }, - pageCursor: '', - term: 'asdf', - types: ['techdocs'], - }), + { + signal: expect.any(AbortSignal), + }, + ), ); }); @@ -144,16 +154,21 @@ it('should update filter values when a new entityName is provided', async () => await renderInTestApp(); await singleResult; - expect(querySpy).toHaveBeenCalledWith({ - filters: { - kind: 'Testable', - name: 'test', - namespace: 'testspace', + expect(querySpy).toHaveBeenCalledWith( + { + filters: { + kind: 'Testable', + name: 'test', + namespace: 'testspace', + }, + pageCursor: '', + term: '', + types: ['techdocs'], }, - pageCursor: '', - term: '', - types: ['techdocs'], - }); + { + signal: expect.any(AbortSignal), + }, + ); const button = screen.getByText('Update Entity'); act(() => { @@ -162,15 +177,20 @@ it('should update filter values when a new entityName is provided', async () => await singleResult; await waitFor(() => - expect(querySpy).toHaveBeenCalledWith({ - filters: { - kind: 'TestableDiff', - name: 'test-diff', - namespace: 'testspace-diff', + expect(querySpy).toHaveBeenCalledWith( + { + filters: { + kind: 'TestableDiff', + name: 'test-diff', + namespace: 'testspace-diff', + }, + pageCursor: '', + term: '', + types: ['techdocs'], }, - pageCursor: '', - term: '', - types: ['techdocs'], - }), + { + signal: expect.any(AbortSignal), + }, + ), ); }); From 482df90524e6c665d3e053dc07c9b67cfcdb4724 Mon Sep 17 00:00:00 2001 From: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:53:07 -0400 Subject: [PATCH 061/193] Update changeset Signed-off-by: Sydney Achinger <78113809+squid-ney@users.noreply.github.com> Signed-off-by: Sydney Achinger --- .changeset/ripe-yaks-brake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/ripe-yaks-brake.md b/.changeset/ripe-yaks-brake.md index 953d0f3192..3b3097011b 100644 --- a/.changeset/ripe-yaks-brake.md +++ b/.changeset/ripe-yaks-brake.md @@ -3,4 +3,4 @@ '@backstage/plugin-search': patch --- -Implement AbortController request cancellation in SearchBar to prevent overlapping search requests. This change ensures that when users type quickly, previous search requests are properly canceled before new ones start. +Implemented AbortController request cancellation for overlapping search requests. This change ensures that when users type quickly, previous search requests are properly canceled before new ones start. From 3947797480cd22f361dec2c7a9a37abc6dfbe428 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 23 Sep 2025 23:55:35 +0200 Subject: [PATCH 062/193] docs: fix grid usage Signed-off-by: Vincenzo Scamporlino --- docs-ui/src/content/components/grid.props.ts | 54 ++++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/docs-ui/src/content/components/grid.props.ts b/docs-ui/src/content/components/grid.props.ts index 34d89785cc..6f037cd64f 100644 --- a/docs-ui/src/content/components/grid.props.ts +++ b/docs-ui/src/content/components/grid.props.ts @@ -43,12 +43,12 @@ export const gridItemPropDefs: Record = { values: [...columnsValues, 'full'], responsive: true, }, - start: { + colStart: { type: 'enum | string', values: [...columnsValues, 'auto'], responsive: true, }, - end: { + colEnd: { type: 'enum | string', values: [...columnsValues, 'auto'], responsive: true, @@ -60,7 +60,7 @@ export const gridItemPropDefs: Record = { export const gridUsageSnippet = `import { Grid } from '@backstage/ui'; -`; +`; export const gridDefaultSnippet = ` @@ -68,44 +68,44 @@ export const gridDefaultSnippet = ` `; -export const gridSimpleSnippet = ` +export const gridSimpleSnippet = ` Hello World Hello World Hello World -`; +`; -export const gridComplexSnippet = ` - - Hello World +export const gridComplexSnippet = ` + + Hello World - - Hello World + + Hello World -`; +`; -export const gridMixingRowsSnippet = ` - - Hello World +export const gridMixingRowsSnippet = ` + + Hello World - - Hello World + + Hello World - - Hello World + + Hello World -`; +`; -export const gridResponsiveSnippet = ` +export const gridResponsiveSnippet = ` - Hello World + - Hello World + Hello World -`; +`; -export const gridStartEndSnippet = ` - - Hello World +export const gridStartEndSnippet = ` + + Hello World -`; +`; From 93e1aa0133e9229b0257c38b0a5b3e5ce23b531a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Edeg=C3=A5rd?= Date: Wed, 24 Sep 2025 07:23:01 +0000 Subject: [PATCH 063/193] Simplify username usage. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Henrik Edegård --- .../src/lib/SlackNotificationProcessor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index f050b3b677..96d4b2ff3e 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -212,7 +212,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { const payload = toChatPostMessageArgs({ channel, payload: options.payload, - ...(this.username && { username: this.username }), + username: this.username, }); this.logger.debug( @@ -271,7 +271,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { toChatPostMessageArgs({ channel, payload: formattedPayload, - ...(this.username && { username: this.username }), + username: this.username, }), ); From 075e0648b8af051133ccd09742e136af69dbc904 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Wed, 24 Sep 2025 10:23:35 +0300 Subject: [PATCH 064/193] feat(scaffolder): add missing form fields for the nfs Signed-off-by: Hellgren Heikki --- .changeset/wide-flies-jog.md | 5 + plugins/scaffolder/report-alpha.api.md | 126 +++++++++++++++++- plugins/scaffolder/report.api.md | 75 ++++++++++- plugins/scaffolder/src/alpha/extensions.tsx | 67 +++++++++- .../src/alpha/fields/EntityNamePicker.ts | 28 ++++ .../src/alpha/fields/EntityPicker.ts | 24 ++++ .../src/alpha/fields/EntityTagsPicker.ts | 24 ++++ .../src/alpha/fields/MultiEntityPicker.ts | 24 ++++ .../src/alpha/fields/MyGroupsPicker.ts | 24 ++++ .../src/alpha/fields/OwnedEntityPicker.ts | 24 ++++ .../src/alpha/fields/OwnerPicker.ts | 24 ++++ .../src/alpha/fields/RepoBranchPicker.ts | 24 ++++ plugins/scaffolder/src/alpha/plugin.tsx | 20 ++- .../fields/EntityNamePicker/index.ts | 1 + .../fields/EntityNamePicker/schema.ts | 5 +- .../fields/EntityNamePicker/validation.ts | 3 + .../fields/MultiEntityPicker/schema.ts | 7 + .../scaffolder/src/components/fields/index.ts | 3 + 18 files changed, 496 insertions(+), 12 deletions(-) create mode 100644 .changeset/wide-flies-jog.md create mode 100644 plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts create mode 100644 plugins/scaffolder/src/alpha/fields/EntityPicker.ts create mode 100644 plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts create mode 100644 plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts create mode 100644 plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts create mode 100644 plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts create mode 100644 plugins/scaffolder/src/alpha/fields/OwnerPicker.ts create mode 100644 plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts diff --git a/.changeset/wide-flies-jog.md b/.changeset/wide-flies-jog.md new file mode 100644 index 0000000000..4fbafe66a1 --- /dev/null +++ b/.changeset/wide-flies-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Added missing form fields for the new frontend system. diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 534d136e6f..505688247f 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -226,6 +226,126 @@ const _default: OverridableFrontendPlugin< routeRef?: RouteRef; }; }>; + 'scaffolder-form-field:scaffolder/entity-name-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'entity-name-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/entity-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'entity-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/entity-tags-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'entity-tags-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/multi-entity-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'multi-entity-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/my-groups-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'my-groups-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/owned-entity-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'owned-entity-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/owner-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'owner-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/repo-branch-picker': ExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'repo-branch-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; 'scaffolder-form-field:scaffolder/repo-url-picker': ExtensionDefinition<{ kind: 'scaffolder-form-field'; name: 'repo-url-picker'; @@ -402,10 +522,10 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'ongoingTask.title': 'Run of'; readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; - readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; @@ -488,8 +608,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateListPage.pageTitle': 'Create a new component'; readonly 'templateListPage.templateGroups.defaultTitle': 'Templates'; readonly 'templateListPage.templateGroups.otherTitle': 'Other Templates'; - readonly 'templateListPage.contentHeader.supportButtonTitle': 'Create new software components using standard templates. Different templates create different kinds of components (services, websites, documentation, ...).'; readonly 'templateListPage.contentHeader.registerExistingButtonTitle': 'Register Existing Component'; + readonly 'templateListPage.contentHeader.supportButtonTitle': 'Create new software components using standard templates. Different templates create different kinds of components (services, websites, documentation, ...).'; readonly 'templateListPage.additionalLinksForEntity.viewTechDocsTitle': 'View TechDocs'; readonly 'templateWizardPage.title': 'Create a new component'; readonly 'templateWizardPage.subtitle': 'Create new software components using standard templates in your organization'; @@ -502,8 +622,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; - readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; readonly 'templateEditorToolbarFileMenu.button': 'File'; readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index a4a339f01e..b1a6b62e9d 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -70,6 +70,15 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent_2< any >; +// @public (undocumented) +export const EntityNamePickerFieldSchema: FieldSchema_2; + +// @public (undocumented) +export const entityNamePickerValidation: ( + value: string, + validation: FieldValidation, +) => void; + // @public export const EntityPickerFieldExtension: FieldExtensionComponent_2< string, @@ -228,6 +237,39 @@ export const MultiEntityPickerFieldExtension: FieldExtensionComponent_2< } >; +// @public (undocumented) +export const MultiEntityPickerFieldSchema: FieldSchema_2< + string[], + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + catalogFilter?: + | Record< + string, + | string + | string[] + | { + exists?: boolean | undefined; + } + > + | Record< + string, + | string + | string[] + | { + exists?: boolean | undefined; + } + >[] + | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; + +// @public +export type MultiEntityPickerUiOptions = NonNullable< + (typeof MultiEntityPickerFieldSchema.TProps.uiSchema)['ui:options'] +>; + // @public export const MyGroupsPickerFieldExtension: FieldExtensionComponent_2< string, @@ -380,9 +422,9 @@ export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2< | { azure?: string[] | undefined; github?: string[] | undefined; - gitlab?: string[] | undefined; bitbucket?: string[] | undefined; gerrit?: string[] | undefined; + gitlab?: string[] | undefined; gitea?: string[] | undefined; } | undefined; @@ -391,6 +433,33 @@ export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2< } >; +// @public (undocumented) +export const RepoBranchPickerFieldSchema: FieldSchema_2< + string, + { + requestUserCredentials?: + | { + secretsKey: string; + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + gitlab?: string[] | undefined; + gitea?: string[] | undefined; + } + | undefined; + } + | undefined; + } +>; + +// @public +export type RepoBranchPickerUiOptions = NonNullable< + (typeof RepoBranchPickerFieldSchema.TProps.uiSchema)['ui:options'] +>; + // @public export const repoPickerValidation: ( value: string, @@ -416,9 +485,9 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2< | { azure?: string[] | undefined; github?: string[] | undefined; - gitlab?: string[] | undefined; bitbucket?: string[] | undefined; gerrit?: string[] | undefined; + gitlab?: string[] | undefined; gitea?: string[] | undefined; } | undefined; @@ -443,9 +512,9 @@ export const RepoUrlPickerFieldSchema: FieldSchema_2< | { azure?: string[] | undefined; github?: string[] | undefined; - gitlab?: string[] | undefined; bitbucket?: string[] | undefined; gerrit?: string[] | undefined; + gitlab?: string[] | undefined; gitea?: string[] | undefined; } | undefined; diff --git a/plugins/scaffolder/src/alpha/extensions.tsx b/plugins/scaffolder/src/alpha/extensions.tsx index 803644d95a..cb69e691b8 100644 --- a/plugins/scaffolder/src/alpha/extensions.tsx +++ b/plugins/scaffolder/src/alpha/extensions.tsx @@ -19,13 +19,13 @@ import { convertLegacyRouteRef, } from '@backstage/core-compat-api'; import { - NavItemBlueprint, - PageBlueprint, ApiBlueprint, + createExtensionInput, discoveryApiRef, fetchApiRef, identityApiRef, - createExtensionInput, + NavItemBlueprint, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import { rootRouteRef } from '../routes'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; @@ -72,6 +72,67 @@ export const repoUrlPickerFormField = FormFieldBlueprint.make({ }, }); +export const entityNamePickerFormField = FormFieldBlueprint.make({ + name: 'entity-name-picker', + params: { + field: () => + import('./fields/EntityNamePicker').then(m => m.EntityNamePicker), + }, +}); + +export const entityPickerFormField = FormFieldBlueprint.make({ + name: 'entity-picker', + params: { + field: () => import('./fields/EntityPicker').then(m => m.EntityPicker), + }, +}); + +export const ownerPickerFormField = FormFieldBlueprint.make({ + name: 'owner-picker', + params: { + field: () => import('./fields/OwnerPicker').then(m => m.OwnerPicker), + }, +}); + +export const entityTagsPickerFormField = FormFieldBlueprint.make({ + name: 'entity-tags-picker', + params: { + field: () => + import('./fields/EntityTagsPicker').then(m => m.EntityTagsPicker), + }, +}); + +export const multiEntityPickerFormField = FormFieldBlueprint.make({ + name: 'multi-entity-picker', + params: { + field: () => + import('./fields/MultiEntityPicker').then(m => m.MultiEntityPicker), + }, +}); + +export const myGroupsPickerFormField = FormFieldBlueprint.make({ + name: 'my-groups-picker', + params: { + field: () => import('./fields/MyGroupsPicker').then(m => m.MyGroupsPicker), + }, +}); + +export const ownedEntityPickerFormField = FormFieldBlueprint.make({ + name: 'owned-entity-picker', + params: { + field: () => + import('./fields/OwnedEntityPicker').then(m => m.OwnedEntityPicker), + }, +}); + +export const repoBranchPickerFormField = FormFieldBlueprint.make({ + name: 'repo-branch-picker', + params: { + field: () => + import('./fields/RepoBranchPicker').then(m => m.RepoBranchPicker), + }, +}); + export const scaffolderApi = ApiBlueprint.make({ params: defineParams => defineParams({ diff --git a/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts b/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts new file mode 100644 index 0000000000..715cb068a0 --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { EntityNamePicker as Component } from '../../components/fields/EntityNamePicker/EntityNamePicker'; +import { + EntityNamePickerFieldSchema, + entityNamePickerValidation, +} from '../../components'; + +export const EntityNamePicker = createFormField({ + component: Component, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + schema: EntityNamePickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/fields/EntityPicker.ts b/plugins/scaffolder/src/alpha/fields/EntityPicker.ts new file mode 100644 index 0000000000..059e87fc19 --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/EntityPicker.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { EntityPicker as Component } from '../../components/fields/EntityPicker/EntityPicker'; +import { EntityPickerFieldSchema } from '../../components'; + +export const EntityPicker = createFormField({ + component: Component, + name: 'EntityPicker', + schema: EntityPickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts b/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts new file mode 100644 index 0000000000..c4d5f88abb --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { EntityTagsPicker as Component } from '../../components/fields/EntityTagsPicker/EntityTagsPicker'; +import { EntityTagsPickerFieldSchema } from '../../components'; + +export const EntityTagsPicker = createFormField({ + component: Component, + name: 'EntityTagsPicker', + schema: EntityTagsPickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts b/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts new file mode 100644 index 0000000000..32bc54f86e --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { MultiEntityPicker as Component } from '../../components/fields/MultiEntityPicker/MultiEntityPicker'; +import { MultiEntityPickerFieldSchema } from '../../components'; + +export const MultiEntityPicker = createFormField({ + component: Component, + name: 'MultiEntityPicker', + schema: MultiEntityPickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts b/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts new file mode 100644 index 0000000000..cc5f717c28 --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { MyGroupsPicker as Component } from '../../components/fields/MyGroupsPicker/MyGroupsPicker'; +import { MyGroupsPickerFieldSchema } from '../../components'; + +export const MyGroupsPicker = createFormField({ + component: Component, + name: 'MyGroupsPicker', + schema: MyGroupsPickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts b/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts new file mode 100644 index 0000000000..6f40b23803 --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { OwnedEntityPicker as Component } from '../../components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { OwnedEntityPickerFieldSchema } from '../../components'; + +export const OwnedEntityPicker = createFormField({ + component: Component, + name: 'OwnedEntityPicker', + schema: OwnedEntityPickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts b/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts new file mode 100644 index 0000000000..ea58f1f00f --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { OwnerPicker as Component } from '../../components/fields/OwnerPicker/OwnerPicker'; +import { OwnerPickerFieldSchema } from '../../components'; + +export const OwnerPicker = createFormField({ + component: Component, + name: 'OwnerPicker', + schema: OwnerPickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts b/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts new file mode 100644 index 0000000000..9694b6a8c4 --- /dev/null +++ b/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { RepoBranchPicker as Component } from '../../components/fields/RepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerFieldSchema } from '../../components'; + +export const RepoBranchPicker = createFormField({ + component: Component, + name: 'RepoBranchPicker', + schema: RepoBranchPickerFieldSchema, +}); diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index a414c61da5..3b41aa3893 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -17,10 +17,10 @@ import { convertLegacyRouteRefs } from '@backstage/core-compat-api'; import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; import { - rootRouteRef, actionsRouteRef, editRouteRef, registerComponentRouteRef, + rootRouteRef, scaffolderListTaskRouteRef, scaffolderTaskRouteRef, selectedTemplateRouteRef, @@ -28,10 +28,18 @@ import { viewTechDocRouteRef, } from '../routes'; import { + entityNamePickerFormField, + entityPickerFormField, + entityTagsPickerFormField, + multiEntityPickerFormField, + myGroupsPickerFormField, + ownedEntityPickerFormField, + ownerPickerFormField, + repoBranchPickerFormField, repoUrlPickerFormField, + scaffolderApi, scaffolderNavItem, scaffolderPage, - scaffolderApi, } from './extensions'; import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { formFieldsApi } from '@backstage/plugin-scaffolder-react/alpha'; @@ -73,5 +81,13 @@ export default createFrontendPlugin({ formDecoratorsApi, formFieldsApi, repoUrlPickerFormField, + entityNamePickerFormField, + entityPickerFormField, + ownerPickerFormField, + entityTagsPickerFormField, + multiEntityPickerFormField, + myGroupsPickerFormField, + ownedEntityPickerFormField, + repoBranchPickerFormField, ], }); diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts index 7076221480..82d9cb0b17 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { entityNamePickerValidation } from './validation'; +export { EntityNamePickerFieldSchema } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts index fe4765b8b8..0230f26879 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -15,7 +15,10 @@ */ import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; -const EntityNamePickerFieldSchema = makeFieldSchema({ +/** + * @public + */ +export const EntityNamePickerFieldSchema = makeFieldSchema({ output: z => z.string(), }); diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts index 9302e9abc1..e475606c11 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts @@ -17,6 +17,9 @@ import { FieldValidation } from '@rjsf/utils'; import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; +/** + * @public + */ export const entityNamePickerValidation = ( value: string, validation: FieldValidation, diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts index 667b4638b8..aa396229ee 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts @@ -16,6 +16,9 @@ import { z as zod } from 'zod'; import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; +/** + * @public + */ export const entityQueryFilterExpressionSchema = zod.record( zod .string() @@ -23,6 +26,9 @@ export const entityQueryFilterExpressionSchema = zod.record( .or(zod.array(zod.string())), ); +/** + * @public + */ export const MultiEntityPickerFieldSchema = makeFieldSchema({ output: z => z.array(z.string()), uiOptions: z => @@ -54,6 +60,7 @@ export const MultiEntityPickerFieldSchema = makeFieldSchema({ /** * The input props that can be specified under `ui:options` for the * `EntityPicker` field extension. + * @public */ export type MultiEntityPickerUiOptions = NonNullable< (typeof MultiEntityPickerFieldSchema.TProps.uiSchema)['ui:options'] diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 7f119f27be..dc1de86adb 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -14,10 +14,13 @@ * limitations under the License. */ export * from './EntityPicker'; +export * from './EntityNamePicker'; export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; +export * from './RepoBranchPicker'; +export * from './MultiEntityPicker'; export * from './MyGroupsPicker'; export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; From 48d409f4abe5df6d08f1731b8dc1366cbd424580 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Wed, 24 Sep 2025 10:25:14 +0300 Subject: [PATCH 065/193] fix: correct year for copyright headers Signed-off-by: Hellgren Heikki --- plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/EntityPicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/OwnerPicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts | 2 +- plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts b/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts index 715cb068a0..770e055948 100644 --- a/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts +++ b/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/EntityPicker.ts b/plugins/scaffolder/src/alpha/fields/EntityPicker.ts index 059e87fc19..d4ae6a5ecb 100644 --- a/plugins/scaffolder/src/alpha/fields/EntityPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/EntityPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts b/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts index c4d5f88abb..35b45a91a6 100644 --- a/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts b/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts index 32bc54f86e..0b89e7351b 100644 --- a/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts b/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts index cc5f717c28..a8f0e74b8c 100644 --- a/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts b/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts index 6f40b23803..becd48b5ce 100644 --- a/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts b/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts index ea58f1f00f..53ec16eb8b 100644 --- a/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts b/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts index 9694b6a8c4..176b4fdaa6 100644 --- a/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts b/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts index add8408b2d..829c20d7c0 100644 --- a/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -16,8 +16,8 @@ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; import { RepoUrlPicker as Component } from '../../components/fields/RepoUrlPicker/RepoUrlPicker'; import { - RepoUrlPickerFieldSchema, repoPickerValidation, + RepoUrlPickerFieldSchema, } from '../../components'; export const RepoUrlPicker = createFormField({ From c8cad6ac273cd1ea416330cfc137a2c2c87d7f8f Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Wed, 24 Sep 2025 09:46:02 +0200 Subject: [PATCH 066/193] prevent handlers overrides Signed-off-by: Juan Pablo Garcia Ripa --- .../external/ExternalAuthTokenHandler.test.ts | 70 +++++++++++++++++++ .../auth/external/ExternalAuthTokenHandler.ts | 33 ++++++--- .../src/entrypoints/auth/external/jwks.ts | 1 - 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts index f315b43314..e3f1acf270 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts @@ -354,6 +354,76 @@ describe('ExternalTokenHandler', () => { expect(verifyMock).toHaveBeenCalledWith(customToken, { context: 'a' }); }); + it('should fail if custom handler has same type as builtin handlers', async () => { + const logger = mockServices.logger.mock(); + const customStaticHandler: ExternalTokenHandler = + createExternalTokenHandler({ + type: 'static', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'custom-static-subject', + }), + }); + + const createHandler = () => + ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger, + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'static', + options: { + token: 'mytoken', + subject: 'static-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + externalTokenHandlers: [customStaticHandler], + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Duplicate external token handler type 'static', each handler must have a unique type"`, + ); + }); + it('should fail if 2 custom handlers have the same type', async () => { + const createHandler = () => + ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + externalTokenHandlers: [ + createExternalTokenHandler({ + type: 'internal-custom', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + }), + }), + createExternalTokenHandler({ + type: 'internal-custom', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + }), + }), + ], + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Duplicate external token handler type 'internal-custom', each handler must have a unique type"`, + ); + }); it('should fail if config contains types not declared', async () => { const createHandler = () => ExternalAuthTokenHandler.create({ diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts index 4b2cbb3e5a..21b3aed99d 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts @@ -42,11 +42,11 @@ export const externalTokenHandlersServiceRef = createServiceRef< multiton: true, }); -const defaultHandlers: ExternalTokenHandler[] = [ - staticTokenHandler, - legacyTokenHandler, - jwksTokenHandler, -]; +const defaultHandlers: Record> = { + static: staticTokenHandler, + legacy: legacyTokenHandler, + jwks: jwksTokenHandler, +}; type ContextMapEntry = { context: T; @@ -70,20 +70,35 @@ export class ExternalAuthTokenHandler { const { ownPluginId, config, - externalTokenHandlers: customHandlers, + externalTokenHandlers: customHandlers = [], logger, } = options; - const handlersTypes = [...defaultHandlers, ...(customHandlers ?? [])]; + const handlersTypes = customHandlers.reduce< + Record> + >( + (acc, handler) => { + if (acc[handler.type]) { + throw new Error( + `Duplicate external token handler type '${handler.type}', each handler must have a unique type`, + ); + } + acc[handler.type] = handler; + return acc; + }, + { ...defaultHandlers }, + ); const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? []; const contexts: ContextMapEntry[] = handlerConfigs.map( handlerConfig => { const type = handlerConfig.getString('type'); - const handler = handlersTypes.find(h => h.type === type); + const handler = handlersTypes[type]; if (!handler) { - const valid = handlersTypes.map(h => `'${h.type}'`).join(', '); + const valid = Object.keys(handlersTypes) + .map(h => `'${h}'`) + .join(', '); throw new Error( `Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, ); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts index 892fcf2181..33b55df775 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts @@ -75,7 +75,6 @@ export const jwksTokenHandler = createExternalTokenHandler({ : 'external:'; return { subject: `${prefix}${sub}`, - allAccessRestrictions: context.allAccessRestrictions, }; } } catch { From 49be37008ce3b553d5b96f72fdc71b22e664f464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Edeg=C3=A5rd?= Date: Wed, 24 Sep 2025 07:55:22 +0000 Subject: [PATCH 067/193] Simplified more. The Slack library will remove undefined values when sending the actual request. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Henrik Edegård --- .../src/lib/SlackNotificationProcessor.test.ts | 2 +- plugins/notifications-backend-module-slack/src/lib/util.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index c970aec7ba..ad4bf01e9f 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -829,7 +829,7 @@ describe('SlackNotificationProcessor', () => { const calls = (slack.chat.postMessage as jest.Mock).mock.calls; expect(calls).toHaveLength(1); - expect(calls[0][0]).not.toHaveProperty('username'); + expect(calls[0][0].username).toBeUndefined(); }); }); diff --git a/plugins/notifications-backend-module-slack/src/lib/util.ts b/plugins/notifications-backend-module-slack/src/lib/util.ts index 1116218b09..33293ea24f 100644 --- a/plugins/notifications-backend-module-slack/src/lib/util.ts +++ b/plugins/notifications-backend-module-slack/src/lib/util.ts @@ -30,7 +30,7 @@ export function toChatPostMessageArgs(options: { const args: ChatPostMessageArguments = { channel, text: payload.title, - ...(username && { username }), + username, attachments: [ { color: getColor(payload.severity), From 4ee97cf3ab270d583b0f8bf2f7aeb8b75e50dbe9 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Wed, 24 Sep 2025 10:43:15 +0300 Subject: [PATCH 068/193] chore(scaffolder): clean up api report from schemas and validators Signed-off-by: Hellgren Heikki --- plugins/scaffolder/report-alpha.api.md | 6 +- plugins/scaffolder/report.api.md | 79 ++----------------- .../src/alpha/fields/EntityNamePicker.ts | 4 +- .../src/alpha/fields/EntityPicker.ts | 6 +- .../src/alpha/fields/EntityTagsPicker.ts | 6 +- .../src/alpha/fields/MultiEntityPicker.ts | 6 +- .../src/alpha/fields/MyGroupsPicker.ts | 6 +- .../src/alpha/fields/OwnedEntityPicker.ts | 6 +- .../src/alpha/fields/OwnerPicker.ts | 6 +- .../src/alpha/fields/RepoBranchPicker.ts | 6 +- .../src/alpha/fields/RepoUrlPicker.ts | 4 +- .../fields/EntityNamePicker/index.ts | 1 + .../fields/EntityNamePicker/schema.ts | 3 - .../fields/EntityNamePicker/validation.ts | 3 - .../components/fields/EntityPicker/index.ts | 1 + .../fields/EntityTagsPicker/index.ts | 1 + .../fields/EntityTagsPicker/schema.ts | 1 - .../fields/MultiEntityPicker/index.ts | 1 + .../fields/MultiEntityPicker/schema.ts | 7 -- .../components/fields/MyGroupsPicker/index.ts | 1 + .../fields/MyGroupsPicker/schema.ts | 1 - .../fields/OwnedEntityPicker/index.ts | 1 + .../fields/OwnedEntityPicker/schema.ts | 3 +- .../components/fields/OwnerPicker/index.ts | 1 + .../components/fields/OwnerPicker/schema.ts | 1 - .../fields/RepoBranchPicker/index.ts | 2 +- .../fields/RepoBranchPicker/schema.ts | 5 -- .../components/fields/RepoUrlPicker/index.ts | 1 + .../fields/RepoUrlPicker/validation.ts | 1 - .../scaffolder/src/components/fields/index.ts | 10 --- plugins/scaffolder/src/components/index.ts | 28 ++++++- 31 files changed, 77 insertions(+), 131 deletions(-) diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 505688247f..a5dd2a22d1 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -522,10 +522,10 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'ongoingTask.title': 'Run of'; readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; - readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; @@ -608,8 +608,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateListPage.pageTitle': 'Create a new component'; readonly 'templateListPage.templateGroups.defaultTitle': 'Templates'; readonly 'templateListPage.templateGroups.otherTitle': 'Other Templates'; - readonly 'templateListPage.contentHeader.registerExistingButtonTitle': 'Register Existing Component'; readonly 'templateListPage.contentHeader.supportButtonTitle': 'Create new software components using standard templates. Different templates create different kinds of components (services, websites, documentation, ...).'; + readonly 'templateListPage.contentHeader.registerExistingButtonTitle': 'Register Existing Component'; readonly 'templateListPage.additionalLinksForEntity.viewTechDocsTitle': 'View TechDocs'; readonly 'templateWizardPage.title': 'Create a new component'; readonly 'templateWizardPage.subtitle': 'Create new software components using standard templates in your organization'; @@ -622,8 +622,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; - readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; readonly 'templateEditorToolbarFileMenu.button': 'File'; readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index b1a6b62e9d..27fa47a184 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -70,15 +70,6 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent_2< any >; -// @public (undocumented) -export const EntityNamePickerFieldSchema: FieldSchema_2; - -// @public (undocumented) -export const entityNamePickerValidation: ( - value: string, - validation: FieldValidation, -) => void; - // @public export const EntityPickerFieldExtension: FieldExtensionComponent_2< string, @@ -237,39 +228,6 @@ export const MultiEntityPickerFieldExtension: FieldExtensionComponent_2< } >; -// @public (undocumented) -export const MultiEntityPickerFieldSchema: FieldSchema_2< - string[], - { - defaultKind?: string | undefined; - defaultNamespace?: string | false | undefined; - catalogFilter?: - | Record< - string, - | string - | string[] - | { - exists?: boolean | undefined; - } - > - | Record< - string, - | string - | string[] - | { - exists?: boolean | undefined; - } - >[] - | undefined; - allowArbitraryValues?: boolean | undefined; - } ->; - -// @public -export type MultiEntityPickerUiOptions = NonNullable< - (typeof MultiEntityPickerFieldSchema.TProps.uiSchema)['ui:options'] ->; - // @public export const MyGroupsPickerFieldExtension: FieldExtensionComponent_2< string, @@ -422,9 +380,9 @@ export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2< | { azure?: string[] | undefined; github?: string[] | undefined; + gitlab?: string[] | undefined; bitbucket?: string[] | undefined; gerrit?: string[] | undefined; - gitlab?: string[] | undefined; gitea?: string[] | undefined; } | undefined; @@ -433,33 +391,6 @@ export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2< } >; -// @public (undocumented) -export const RepoBranchPickerFieldSchema: FieldSchema_2< - string, - { - requestUserCredentials?: - | { - secretsKey: string; - additionalScopes?: - | { - azure?: string[] | undefined; - github?: string[] | undefined; - bitbucket?: string[] | undefined; - gerrit?: string[] | undefined; - gitlab?: string[] | undefined; - gitea?: string[] | undefined; - } - | undefined; - } - | undefined; - } ->; - -// @public -export type RepoBranchPickerUiOptions = NonNullable< - (typeof RepoBranchPickerFieldSchema.TProps.uiSchema)['ui:options'] ->; - // @public export const repoPickerValidation: ( value: string, @@ -473,11 +404,11 @@ export const repoPickerValidation: ( export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2< string, { - allowedHosts?: string[] | undefined; allowedOrganizations?: string[] | undefined; allowedOwners?: string[] | undefined; allowedProjects?: string[] | undefined; allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; requestUserCredentials?: | { secretsKey: string; @@ -485,9 +416,9 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2< | { azure?: string[] | undefined; github?: string[] | undefined; + gitlab?: string[] | undefined; bitbucket?: string[] | undefined; gerrit?: string[] | undefined; - gitlab?: string[] | undefined; gitea?: string[] | undefined; } | undefined; @@ -500,11 +431,11 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent_2< export const RepoUrlPickerFieldSchema: FieldSchema_2< string, { - allowedHosts?: string[] | undefined; allowedOrganizations?: string[] | undefined; allowedOwners?: string[] | undefined; allowedProjects?: string[] | undefined; allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; requestUserCredentials?: | { secretsKey: string; @@ -512,9 +443,9 @@ export const RepoUrlPickerFieldSchema: FieldSchema_2< | { azure?: string[] | undefined; github?: string[] | undefined; + gitlab?: string[] | undefined; bitbucket?: string[] | undefined; gerrit?: string[] | undefined; - gitlab?: string[] | undefined; gitea?: string[] | undefined; } | undefined; diff --git a/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts b/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts index 770e055948..dcb68edd15 100644 --- a/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts +++ b/plugins/scaffolder/src/alpha/fields/EntityNamePicker.ts @@ -14,11 +14,11 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { EntityNamePicker as Component } from '../../components/fields/EntityNamePicker/EntityNamePicker'; import { + EntityNamePicker as Component, EntityNamePickerFieldSchema, entityNamePickerValidation, -} from '../../components'; +} from '../../components/fields/EntityNamePicker'; export const EntityNamePicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/EntityPicker.ts b/plugins/scaffolder/src/alpha/fields/EntityPicker.ts index d4ae6a5ecb..c80b38fe43 100644 --- a/plugins/scaffolder/src/alpha/fields/EntityPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/EntityPicker.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { EntityPicker as Component } from '../../components/fields/EntityPicker/EntityPicker'; -import { EntityPickerFieldSchema } from '../../components'; +import { + EntityPicker as Component, + EntityPickerFieldSchema, +} from '../../components/fields/EntityPicker'; export const EntityPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts b/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts index 35b45a91a6..9a8e86730e 100644 --- a/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/EntityTagsPicker.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { EntityTagsPicker as Component } from '../../components/fields/EntityTagsPicker/EntityTagsPicker'; -import { EntityTagsPickerFieldSchema } from '../../components'; +import { + EntityTagsPicker as Component, + EntityTagsPickerFieldSchema, +} from '../../components/fields/EntityTagsPicker'; export const EntityTagsPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts b/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts index 0b89e7351b..8e8c65d67c 100644 --- a/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/MultiEntityPicker.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { MultiEntityPicker as Component } from '../../components/fields/MultiEntityPicker/MultiEntityPicker'; -import { MultiEntityPickerFieldSchema } from '../../components'; +import { + MultiEntityPicker as Component, + MultiEntityPickerFieldSchema, +} from '../../components/fields/MultiEntityPicker'; export const MultiEntityPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts b/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts index a8f0e74b8c..a2cd5823b4 100644 --- a/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/MyGroupsPicker.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { MyGroupsPicker as Component } from '../../components/fields/MyGroupsPicker/MyGroupsPicker'; -import { MyGroupsPickerFieldSchema } from '../../components'; +import { + MyGroupsPicker as Component, + MyGroupsPickerFieldSchema, +} from '../../components/fields/MyGroupsPicker'; export const MyGroupsPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts b/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts index becd48b5ce..7b13797ce1 100644 --- a/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/OwnedEntityPicker.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { OwnedEntityPicker as Component } from '../../components/fields/OwnedEntityPicker/OwnedEntityPicker'; -import { OwnedEntityPickerFieldSchema } from '../../components'; +import { + OwnedEntityPicker as Component, + OwnedEntityPickerFieldSchema, +} from '../../components/fields/OwnedEntityPicker'; export const OwnedEntityPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts b/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts index 53ec16eb8b..00761207a0 100644 --- a/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/OwnerPicker.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { OwnerPicker as Component } from '../../components/fields/OwnerPicker/OwnerPicker'; -import { OwnerPickerFieldSchema } from '../../components'; +import { + OwnerPicker as Component, + OwnerPickerFieldSchema, +} from '../../components/fields/OwnerPicker'; export const OwnerPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts b/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts index 176b4fdaa6..b1cf2d8802 100644 --- a/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/RepoBranchPicker.ts @@ -14,8 +14,10 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { RepoBranchPicker as Component } from '../../components/fields/RepoBranchPicker/RepoBranchPicker'; -import { RepoBranchPickerFieldSchema } from '../../components'; +import { + RepoBranchPicker as Component, + RepoBranchPickerFieldSchema, +} from '../../components/fields/RepoBranchPicker'; export const RepoBranchPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts b/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts index 829c20d7c0..10394f7df0 100644 --- a/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts +++ b/plugins/scaffolder/src/alpha/fields/RepoUrlPicker.ts @@ -14,11 +14,11 @@ * limitations under the License. */ import { createFormField } from '@backstage/plugin-scaffolder-react/alpha'; -import { RepoUrlPicker as Component } from '../../components/fields/RepoUrlPicker/RepoUrlPicker'; import { repoPickerValidation, + RepoUrlPicker as Component, RepoUrlPickerFieldSchema, -} from '../../components'; +} from '../../components/fields/RepoUrlPicker'; export const RepoUrlPicker = createFormField({ component: Component, diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts index 82d9cb0b17..fc295d66c5 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts @@ -13,5 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { EntityNamePicker } from './EntityNamePicker'; export { entityNamePickerValidation } from './validation'; export { EntityNamePickerFieldSchema } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts index 0230f26879..68c8d5157e 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -15,9 +15,6 @@ */ import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; -/** - * @public - */ export const EntityNamePickerFieldSchema = makeFieldSchema({ output: z => z.string(), }); diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts index e475606c11..9302e9abc1 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts @@ -17,9 +17,6 @@ import { FieldValidation } from '@rjsf/utils'; import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; -/** - * @public - */ export const entityNamePickerValidation = ( value: string, validation: FieldValidation, diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts index b8df596c98..08583dca30 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { EntityPicker } from './EntityPicker'; export { EntityPickerFieldSchema, type EntityPickerUiOptions } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 52bad9a80d..77786f0e1a 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { EntityTagsPicker } from './EntityTagsPicker'; export { EntityTagsPickerFieldSchema, type EntityTagsPickerUiOptions, diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts index d1a300f5c8..0162804ce1 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts @@ -46,7 +46,6 @@ export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.TProps; /** * The input props that can be specified under `ui:options` for the * `EntityTagsPicker` field extension. - * * @public */ export type EntityTagsPickerUiOptions = NonNullable< diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/MultiEntityPicker/index.ts index 9a597b9b97..add40e4a81 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export { MultiEntityPicker } from './MultiEntityPicker'; export { MultiEntityPickerFieldSchema, type MultiEntityPickerUiOptions, diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts index aa396229ee..667b4638b8 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts @@ -16,9 +16,6 @@ import { z as zod } from 'zod'; import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; -/** - * @public - */ export const entityQueryFilterExpressionSchema = zod.record( zod .string() @@ -26,9 +23,6 @@ export const entityQueryFilterExpressionSchema = zod.record( .or(zod.array(zod.string())), ); -/** - * @public - */ export const MultiEntityPickerFieldSchema = makeFieldSchema({ output: z => z.array(z.string()), uiOptions: z => @@ -60,7 +54,6 @@ export const MultiEntityPickerFieldSchema = makeFieldSchema({ /** * The input props that can be specified under `ui:options` for the * `EntityPicker` field extension. - * @public */ export type MultiEntityPickerUiOptions = NonNullable< (typeof MultiEntityPickerFieldSchema.TProps.uiSchema)['ui:options'] diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/index.ts b/plugins/scaffolder/src/components/fields/MyGroupsPicker/index.ts index 00be19cf96..42982dcb6f 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export { MyGroupsPicker } from './MyGroupsPicker'; export { MyGroupsPickerSchema, type MyGroupsPickerUiOptions, diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/schema.ts b/plugins/scaffolder/src/components/fields/MyGroupsPicker/schema.ts index 6ccda91afb..41e8579328 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/schema.ts @@ -36,7 +36,6 @@ export type MyGroupsPickerUiOptions = NonNullable< /** * Props for the MyGroupsPicker. - * @public */ export type MyGroupsPickerProps = typeof MyGroupsPickerFieldSchema.TProps; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 985dae1d3c..6738a8a057 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { OwnedEntityPicker } from './OwnedEntityPicker'; export { OwnedEntityPickerFieldSchema, type OwnedEntityPickerUiOptions, diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts index 6dd00df49d..e5435c150f 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityPickerFieldSchema } from '../EntityPicker/schema'; +import { EntityPickerFieldSchema } from '../EntityPicker'; /** * @public @@ -23,7 +23,6 @@ export const OwnedEntityPickerFieldSchema = EntityPickerFieldSchema; /** * The input props that can be specified under `ui:options` for the * `OwnedEntityPicker` field extension. - * * @public */ export type OwnedEntityPickerUiOptions = NonNullable< diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index 9d94650e04..5080ee1602 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { OwnerPicker } from './OwnerPicker'; export { OwnerPickerFieldSchema, type OwnerPickerUiOptions } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts index 76389216ba..5a5b2e9cf6 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -54,7 +54,6 @@ export const OwnerPickerFieldSchema = makeFieldSchema({ /** * The input props that can be specified under `ui:options` for the * `OwnerPicker` field extension. - * * @public */ export type OwnerPickerUiOptions = NonNullable< diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts index 2b87b62ebd..e41b723aa9 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +export { RepoBranchPicker } from './RepoBranchPicker'; export { RepoBranchPickerFieldSchema, type RepoBranchPickerUiOptions, diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts index 62a3b66e14..4e76f4d988 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts @@ -15,9 +15,6 @@ */ import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; -/** - * @public - */ export const RepoBranchPickerFieldSchema = makeFieldSchema({ output: z => z.string(), uiOptions: z => @@ -69,8 +66,6 @@ export const RepoBranchPickerFieldSchema = makeFieldSchema({ /** * The input props that can be specified under `ui:options` for the * `RepoBranchPicker` field extension. - * - * @public */ export type RepoBranchPickerUiOptions = NonNullable< (typeof RepoBranchPickerFieldSchema.TProps.uiSchema)['ui:options'] diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 33e5ef1715..d4dc219b68 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { RepoUrlPicker } from './RepoUrlPicker'; export { RepoUrlPickerFieldSchema, type RepoUrlPickerUiOptions, diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts index 06c2fd5684..682fc6bb2a 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts @@ -22,7 +22,6 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; * The validation function for the `repoUrl` that is returned from the * field extension. Ensures that you have all the required fields filled for * the different providers that exist. - * * @public */ export const repoPickerValidation = ( diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index dc1de86adb..287f62022c 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -13,14 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './EntityPicker'; -export * from './EntityNamePicker'; -export * from './OwnerPicker'; -export * from './RepoUrlPicker'; -export * from './OwnedEntityPicker'; -export * from './EntityTagsPicker'; -export * from './RepoBranchPicker'; -export * from './MultiEntityPicker'; -export * from './MyGroupsPicker'; - export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/components/index.ts b/plugins/scaffolder/src/components/index.ts index 552a75c99b..abafd42ed2 100644 --- a/plugins/scaffolder/src/components/index.ts +++ b/plugins/scaffolder/src/components/index.ts @@ -14,7 +14,33 @@ * limitations under the License. */ export * from './fields'; -export type { RepoUrlPickerUiOptions } from './fields'; + +export { + EntityPickerFieldSchema, + type EntityPickerUiOptions, +} from './fields/EntityPicker'; +export { + EntityTagsPickerFieldSchema, + type EntityTagsPickerUiOptions, +} from './fields/EntityTagsPicker'; +export { + MyGroupsPickerFieldSchema, + MyGroupsPickerSchema, + type MyGroupsPickerUiOptions, +} from './fields/MyGroupsPicker'; +export { + OwnedEntityPickerFieldSchema, + type OwnedEntityPickerUiOptions, +} from './fields/OwnedEntityPicker'; +export { + OwnerPickerFieldSchema, + type OwnerPickerUiOptions, +} from './fields/OwnerPicker'; +export { + repoPickerValidation, + RepoUrlPickerFieldSchema, + type RepoUrlPickerUiOptions, +} from './fields/RepoUrlPicker'; export { TemplateTypePicker } from './TemplateTypePicker'; From faf0f07e02e4cae7017b77f4b9c831c9dc86fb7e Mon Sep 17 00:00:00 2001 From: Abhishek B Date: Wed, 24 Sep 2025 18:09:35 +0530 Subject: [PATCH 069/193] Correct the database name associated with the catalog plugin Signed-off-by: Abhishek B --- docs/tutorials/manual-knex-rollback.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/manual-knex-rollback.md b/docs/tutorials/manual-knex-rollback.md index b0f67ba3e0..442a48e2f5 100644 --- a/docs/tutorials/manual-knex-rollback.md +++ b/docs/tutorials/manual-knex-rollback.md @@ -23,7 +23,7 @@ You can interact with Knex running the commands below in the project root: We want to check the migration status: ```sh -$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ +$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ Using environment: production Found 2 Completed Migration file/files. 20211229105307_init.js @@ -34,7 +34,7 @@ No Pending Migration files Found. Now lets rollback a specific migration called `20240113144027_assets-namespace.js`: ```sh -$ node_modules/.bin/knex migrate:down 20240113144027_assets-namespace.js --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ +$ node_modules/.bin/knex migrate:down 20240113144027_assets-namespace.js --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ Using environment: production Batch 2 rolled back the following migrations: 20240113144027_assets-namespace.js @@ -43,7 +43,7 @@ Batch 2 rolled back the following migrations: Now we can check the migration status again to confirm the rollback: ```sh -$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ +$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ Using environment: production Found 1 Completed Migration file/files. 20211229105307_init.js @@ -54,7 +54,7 @@ Found 1 Pending Migration file/files. Now lets use `migrate:currentVersion` which retrieves the current migration version. If there aren't any migrations run yet, it will return "none". ```sh -$ node_modules/.bin/knex migrate:currentVersion --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg +$ node_modules/.bin/knex migrate:currentVersion --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg Using environment: production Current Version: 20240113144027 ``` From c9f3c47c7e15431e4f664dca77e24b7ba589d2a5 Mon Sep 17 00:00:00 2001 From: birdhb Date: Wed, 24 Sep 2025 16:40:39 +0100 Subject: [PATCH 070/193] Update eighty-cows-care.md Signed-off-by: birdhb --- .changeset/eighty-cows-care.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eighty-cows-care.md b/.changeset/eighty-cows-care.md index fc0a5c48bc..f3eed821ec 100644 --- a/.changeset/eighty-cows-care.md +++ b/.changeset/eighty-cows-care.md @@ -2,4 +2,4 @@ '@backstage/ui': patch --- -Added show/hide visibility and clear action icons to TextField +Added PasswordField to BUI with default show/hide visibilty on text input From 8ac36e8737587a95c29a6629f93c99b8b4ef16e3 Mon Sep 17 00:00:00 2001 From: Gustavo RPS <516827+gustavorps@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:27:31 -0300 Subject: [PATCH 071/193] Add PromiseRouter import to http-router.md Signed-off-by: Gustavo RPS <516827+gustavorps@users.noreply.github.com> --- docs/backend-system/core-services/http-router.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md index 5e065152a0..23304f663f 100644 --- a/docs/backend-system/core-services/http-router.md +++ b/docs/backend-system/core-services/http-router.md @@ -134,6 +134,7 @@ import { createAuthIntegrationRouter, createRateLimitMiddleware, } from '@backstage/backend-defaults/httpRouter'; +import PromiseRouter from 'express-promise-router'; import { createServiceFactory } from '@backstage/backend-plugin-api'; const backend = createBackend(); From 02f152703d09970af742c23e72421bbb4e20e6e2 Mon Sep 17 00:00:00 2001 From: Gustavo RPS <516827+gustavorps@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:32:07 -0300 Subject: [PATCH 072/193] Add express Handler import to http-router.md Signed-off-by: Gustavo RPS <516827+gustavorps@users.noreply.github.com> --- docs/backend-system/core-services/http-router.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md index 23304f663f..4f1e2284ad 100644 --- a/docs/backend-system/core-services/http-router.md +++ b/docs/backend-system/core-services/http-router.md @@ -135,6 +135,7 @@ import { createRateLimitMiddleware, } from '@backstage/backend-defaults/httpRouter'; import PromiseRouter from 'express-promise-router'; +import { Handler } from 'express'; import { createServiceFactory } from '@backstage/backend-plugin-api'; const backend = createBackend(); From 3dc8604ace962a93f3795547ab6a555205289c8d Mon Sep 17 00:00:00 2001 From: Gustavo RPS <516827+gustavorps@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:36:10 -0300 Subject: [PATCH 073/193] Add coreServices and HttpRouterServiceAuthPolicy imports Signed-off-by: Gustavo RPS <516827+gustavorps@users.noreply.github.com> --- docs/backend-system/core-services/http-router.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md index 4f1e2284ad..583cd0212d 100644 --- a/docs/backend-system/core-services/http-router.md +++ b/docs/backend-system/core-services/http-router.md @@ -136,7 +136,11 @@ import { } from '@backstage/backend-defaults/httpRouter'; import PromiseRouter from 'express-promise-router'; import { Handler } from 'express'; -import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { + createServiceFactory, + coreServices, + HttpRouterServiceAuthPolicy +} from '@backstage/backend-plugin-api'; const backend = createBackend(); From b58a488eff432b5e8e275dd130551bc86cae4d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Thu, 25 Sep 2025 10:20:05 +0200 Subject: [PATCH 074/193] fix(bui): fix button link internal routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .../src/components/ButtonLink/ButtonLink.tsx | 57 +++++++++++++++---- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.tsx index 153643a530..b5b5ab208d 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.tsx @@ -16,13 +16,16 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; -import { Link as RALink } from 'react-aria-components'; +import { Link as RALink, RouterProvider } from 'react-aria-components'; +import { useNavigate, useHref } from 'react-router-dom'; import type { ButtonLinkProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { isExternalLink } from '../../utils/isExternalLink'; /** @public */ export const ButtonLink = forwardRef( (props: ButtonLinkProps, ref: Ref) => { + const navigate = useNavigate(); const { size = 'small', variant = 'primary', @@ -30,6 +33,7 @@ export const ButtonLink = forwardRef( iconEnd, children, className, + href, ...rest } = props; @@ -40,17 +44,48 @@ export const ButtonLink = forwardRef( const { classNames: classNamesButtonLink } = useStyles('ButtonLink'); + const isExternal = isExternalLink(href); + + // If it's an external link, render RALink without RouterProvider + if (isExternal) { + return ( + + {iconStart} + {children} + {iconEnd} + + ); + } + + // For internal links, use RouterProvider return ( - - {iconStart} - {children} - {iconEnd} - + + + {iconStart} + {children} + {iconEnd} + + ); }, ); From 61b6281e5c17445414d8a7d2ae258cf7779d2497 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Thu, 25 Sep 2025 10:12:40 +0100 Subject: [PATCH 075/193] simplify changeset & update hasFile to accept number or string Signed-off-by: Claire Peng --- .changeset/twelve-oranges-grin.md | 11 +---------- .../catalog-backend-module-gitlab/src/lib/client.ts | 2 +- .../src/providers/GitlabDiscoveryEntityProvider.ts | 2 +- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.changeset/twelve-oranges-grin.md b/.changeset/twelve-oranges-grin.md index b508a59b0b..174dbdcd66 100644 --- a/.changeset/twelve-oranges-grin.md +++ b/.changeset/twelve-oranges-grin.md @@ -6,15 +6,6 @@ Update GitlabDiscoveryEntityProvider to use `project.id` rather than `project.pa Solves [#30147](https://github.com/backstage/backstage/issues/30147): -> Use of project.id avoids edgecases caused by path encoding or project structure changes +> Use of project.id avoids edge cases caused by path encoding or project structure changes > [...] > It also simplifies reasoning about the GitLab API usage, since IDs are immutable, while paths are not. - -```diff -const hasFile = await client.hasFile( -- project.path_with_namespace, -+ project.id.toString(), - project_branch, - this.config.catalogFile, -); -``` diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 46ff54846c..5c373dcf4a 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -349,7 +349,7 @@ export class GitLabClient { * @param filePath - The path to the file */ async hasFile( - projectIdentifier: string, + projectIdentifier: string | number, branch: string, filePath: string, ): Promise { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index e52c231d46..b3371dcc9e 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -590,7 +590,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { this.config.fallbackBranch; const hasFile = await client.hasFile( - project.id.toString(), + project.id, project_branch, this.config.catalogFile, ); From 077dee1902fb0c967bbf728497011db5f29c85af Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Sep 2025 18:53:05 +0200 Subject: [PATCH 076/193] plugins: scaffold bui-themer plugin Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 1 + packages/app/src/App.tsx | 2 + plugins/bui-themer/.eslintrc.js | 1 + plugins/bui-themer/README.md | 13 + plugins/bui-themer/catalog-info.yaml | 9 + plugins/bui-themer/dev/index.tsx | 26 ++ plugins/bui-themer/package.json | 53 +++ .../ExampleComponent.test.tsx | 38 +++ .../ExampleComponent/ExampleComponent.tsx | 52 +++ .../src/components/ExampleComponent/index.ts | 16 + .../ExampleFetchComponent.test.tsx | 34 ++ .../ExampleFetchComponent.tsx | 322 ++++++++++++++++++ .../components/ExampleFetchComponent/index.ts | 16 + plugins/bui-themer/src/index.ts | 16 + plugins/bui-themer/src/plugin.test.ts | 22 ++ plugins/bui-themer/src/plugin.ts | 37 ++ plugins/bui-themer/src/routes.ts | 20 ++ plugins/bui-themer/src/setupTests.ts | 16 + yarn.lock | 131 +++++-- 19 files changed, 805 insertions(+), 20 deletions(-) create mode 100644 plugins/bui-themer/.eslintrc.js create mode 100644 plugins/bui-themer/README.md create mode 100644 plugins/bui-themer/catalog-info.yaml create mode 100644 plugins/bui-themer/dev/index.tsx create mode 100644 plugins/bui-themer/package.json create mode 100644 plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 plugins/bui-themer/src/components/ExampleComponent/index.ts create mode 100644 plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 plugins/bui-themer/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/bui-themer/src/index.ts create mode 100644 plugins/bui-themer/src/plugin.test.ts create mode 100644 plugins/bui-themer/src/plugin.ts create mode 100644 plugins/bui-themer/src/routes.ts create mode 100644 plugins/bui-themer/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 56b5178692..56e619e65a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -46,6 +46,7 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-api-docs": "workspace:^", "@backstage/plugin-auth-react": "workspace:^", + "@backstage/plugin-bui-themer": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-graph": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ad525420a7..bd64dc504f 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -73,6 +73,7 @@ import { } from '@backstage/plugin-notifications'; import { CustomizableHomePage } from './components/home/CustomizableHomePage'; import { HomePage } from './components/home/HomePage'; +import { BuiThemerPage } from '@backstage/plugin-bui-themer'; const app = createApp({ apis, @@ -208,6 +209,7 @@ const routes = ( {customDevToolsPage} } /> + } /> ); diff --git a/plugins/bui-themer/.eslintrc.js b/plugins/bui-themer/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/bui-themer/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/bui-themer/README.md b/plugins/bui-themer/README.md new file mode 100644 index 0000000000..f73dd7875e --- /dev/null +++ b/plugins/bui-themer/README.md @@ -0,0 +1,13 @@ +# bui-themer + +Welcome to the bui-themer plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/bui-themer](http://localhost:3000/bui-themer). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/bui-themer/catalog-info.yaml b/plugins/bui-themer/catalog-info.yaml new file mode 100644 index 0000000000..15e907a0ac --- /dev/null +++ b/plugins/bui-themer/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-bui-themer + title: '@backstage/plugin-bui-themer' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/bui-themer/dev/index.tsx b/plugins/bui-themer/dev/index.tsx new file mode 100644 index 0000000000..ce3986f7da --- /dev/null +++ b/plugins/bui-themer/dev/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { buiThemerPlugin, BuiThemerPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(buiThemerPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/bui-themer', + }) + .render(); diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json new file mode 100644 index 0000000000..c0d49b0879 --- /dev/null +++ b/plugins/bui-themer/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-bui-themer", + "version": "0.1.0", + "license": "Apache-2.0", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin", + "pluginId": "bui-themer" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.61", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.0.0", + "msw": "^1.0.0", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..f5f7f185c5 --- /dev/null +++ b/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { ExampleComponent } from './ExampleComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { screen } from '@testing-library/react'; +import { registerMswTestHooks, renderInTestApp } from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + registerMswTestHooks(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + await renderInTestApp(); + expect(screen.getByText('Welcome to bui-themer!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..3a04bd7562 --- /dev/null +++ b/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx @@ -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 { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core-components'; +import { ExampleFetchComponent } from '../ExampleFetchComponent'; + +export const ExampleComponent = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); diff --git a/plugins/bui-themer/src/components/ExampleComponent/index.ts b/plugins/bui-themer/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..66b3fc6c97 --- /dev/null +++ b/plugins/bui-themer/src/components/ExampleComponent/index.ts @@ -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 { ExampleComponent } from './ExampleComponent'; diff --git a/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 0000000000..a3085e3dcb --- /dev/null +++ b/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,34 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { ExampleFetchComponent } from './ExampleFetchComponent'; + +describe('ExampleFetchComponent', () => { + it('renders the user table', async () => { + const { getAllByText, getByAltText, getByText, findByRole } = + await renderInTestApp(); + + // Wait for the table to render + const table = await findByRole('table'); + const nationality = getAllByText('GB'); + // Assert that the table contains the expected user data + expect(table).toBeInTheDocument(); + expect(getByAltText('Carolyn')).toBeInTheDocument(); + expect(getByText('Carolyn Moore')).toBeInTheDocument(); + expect(getByText('carolyn.moore@example.com')).toBeInTheDocument(); + expect(nationality[0]).toBeInTheDocument(); + }); +}); diff --git a/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 0000000000..6c9e77094d --- /dev/null +++ b/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,322 @@ +/* + * 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 { makeStyles } from '@material-ui/core/styles'; +import { + Table, + TableColumn, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; +import useAsync from 'react-use/lib/useAsync'; + +export const exampleUsers = { + results: [ + { + gender: 'female', + name: { + title: 'Miss', + first: 'Carolyn', + last: 'Moore', + }, + email: 'carolyn.moore@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Carolyn', + nat: 'GB', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Esma', + last: 'Berberoğlu', + }, + email: 'esma.berberoglu@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Esma', + nat: 'TR', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Isabella', + last: 'Rhodes', + }, + email: 'isabella.rhodes@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', + nat: 'GB', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Derrick', + last: 'Carter', + }, + email: 'derrick.carter@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Derrick', + nat: 'IE', + }, + { + gender: 'female', + name: { + title: 'Miss', + first: 'Mattie', + last: 'Lambert', + }, + email: 'mattie.lambert@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mattie', + nat: 'AU', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Mijat', + last: 'Rakić', + }, + email: 'mijat.rakic@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mijat', + nat: 'RS', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Javier', + last: 'Reid', + }, + email: 'javier.reid@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Javier', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Isabella', + last: 'Li', + }, + email: 'isabella.li@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', + nat: 'CA', + }, + { + gender: 'female', + name: { + title: 'Mrs', + first: 'Stephanie', + last: 'Garrett', + }, + email: 'stephanie.garrett@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Stephanie', + nat: 'AU', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Antonia', + last: 'Núñez', + }, + email: 'antonia.nunez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Antonia', + nat: 'ES', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Donald', + last: 'Young', + }, + email: 'donald.young@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Donald', + nat: 'US', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Iegor', + last: 'Holodovskiy', + }, + email: 'iegor.holodovskiy@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Iegor', + nat: 'UA', + }, + { + gender: 'female', + name: { + title: 'Madame', + first: 'Jessica', + last: 'David', + }, + email: 'jessica.david@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jessica', + nat: 'CH', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Eve', + last: 'Martinez', + }, + email: 'eve.martinez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Eve', + nat: 'FR', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Caleb', + last: 'Silva', + }, + email: 'caleb.silva@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Caleb', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Miss', + first: 'Marcia', + last: 'Jenkins', + }, + email: 'marcia.jenkins@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Marcia', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Mrs', + first: 'Mackenzie', + last: 'Jones', + }, + email: 'mackenzie.jones@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mackenzie', + nat: 'NZ', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Jeremiah', + last: 'Gutierrez', + }, + email: 'jeremiah.gutierrez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jeremiah', + nat: 'AU', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Luciara', + last: 'Souza', + }, + email: 'luciara.souza@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Luciara', + nat: 'BR', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Valgi', + last: 'da Cunha', + }, + email: 'valgi.dacunha@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Valgi', + nat: 'BR', + }, + ], +}; + +const useStyles = makeStyles({ + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +type User = { + gender: string; // "male" + name: { + title: string; // "Mr", + first: string; // "Duane", + last: string; // "Reed" + }; + email: string; // "duane.reed@example.com" + picture: string; // "https://api.dicebear.com/6.x/open-peeps/svg?seed=Duane" + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable = ({ users }: DenseTableProps) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map(user => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +export const ExampleFetchComponent = () => { + const { value, loading, error } = useAsync(async (): Promise => { + // Would use fetch in a real world example + return exampleUsers.results; + }, []); + + if (loading) { + return ; + } else if (error) { + return ; + } + + return ; +}; diff --git a/plugins/bui-themer/src/components/ExampleFetchComponent/index.ts b/plugins/bui-themer/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..b0be0a2a71 --- /dev/null +++ b/plugins/bui-themer/src/components/ExampleFetchComponent/index.ts @@ -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 { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/bui-themer/src/index.ts b/plugins/bui-themer/src/index.ts new file mode 100644 index 0000000000..26bc35ebb1 --- /dev/null +++ b/plugins/bui-themer/src/index.ts @@ -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 { buiThemerPlugin, BuiThemerPage } from './plugin'; diff --git a/plugins/bui-themer/src/plugin.test.ts b/plugins/bui-themer/src/plugin.test.ts new file mode 100644 index 0000000000..b19adaf007 --- /dev/null +++ b/plugins/bui-themer/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { buiThemerPlugin } from './plugin'; + +describe('bui-themer', () => { + it('should export plugin', () => { + expect(buiThemerPlugin).toBeDefined(); + }); +}); diff --git a/plugins/bui-themer/src/plugin.ts b/plugins/bui-themer/src/plugin.ts new file mode 100644 index 0000000000..d87d035b12 --- /dev/null +++ b/plugins/bui-themer/src/plugin.ts @@ -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 { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const buiThemerPlugin = createPlugin({ + id: 'bui-themer', + routes: { + root: rootRouteRef, + }, +}); + +export const BuiThemerPage = buiThemerPlugin.provide( + createRoutableExtension({ + name: 'BuiThemerPage', + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/bui-themer/src/routes.ts b/plugins/bui-themer/src/routes.ts new file mode 100644 index 0000000000..efd79e62be --- /dev/null +++ b/plugins/bui-themer/src/routes.ts @@ -0,0 +1,20 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'bui-themer', +}); diff --git a/plugins/bui-themer/src/setupTests.ts b/plugins/bui-themer/src/setupTests.ts new file mode 100644 index 0000000000..b57590b525 --- /dev/null +++ b/plugins/bui-themer/src/setupTests.ts @@ -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. + */ +import '@testing-library/jest-dom'; diff --git a/yarn.lock b/yarn.lock index c1a352407c..cb9a75fa61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4323,6 +4323,31 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-bui-themer@workspace:^, @backstage/plugin-bui-themer@workspace:plugins/bui-themer": + version: 0.0.0-use.local + resolution: "@backstage/plugin-bui-themer@workspace:plugins/bui-themer" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": "npm:^4.9.13" + "@material-ui/icons": "npm:^4.9.1" + "@material-ui/lab": "npm:^4.0.0-alpha.61" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^14.0.0" + "@testing-library/user-event": "npm:^14.0.0" + msw: "npm:^1.0.0" + react: "npm:^16.13.1 || ^17.0.0 || ^18.0.0" + react-use: "npm:^17.2.4" + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws" @@ -19238,6 +19263,22 @@ __metadata: languageName: node linkType: hard +"@testing-library/dom@npm:^9.0.0": + version: 9.3.4 + resolution: "@testing-library/dom@npm:9.3.4" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.1.3" + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + pretty-format: "npm:^27.0.2" + checksum: 10/510da752ea76f4a10a0a4e3a77917b0302cf03effe576cd3534cab7e796533ee2b0e9fb6fb11b911a1ebd7c70a0bb6f235bf4f816c9b82b95b8fe0cddfd10975 + languageName: node + linkType: hard + "@testing-library/jest-dom@npm:^6.0.0, @testing-library/jest-dom@npm:^6.6.3": version: 6.8.0 resolution: "@testing-library/jest-dom@npm:6.8.0" @@ -19274,6 +19315,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/react@npm:^14.0.0": + version: 14.3.1 + resolution: "@testing-library/react@npm:14.3.1" + dependencies: + "@babel/runtime": "npm:^7.12.5" + "@testing-library/dom": "npm:^9.0.0" + "@types/react-dom": "npm:^18.0.0" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10/83359dcdf9eaf067839f34604e1a181cbc14fc09f3a07672403700fcc6a900c4b8054ad1114fc24b4b9f89d84e2a09e1b7c9afce2306b1d4b4c9e30eb1cb12de + languageName: node + linkType: hard + "@testing-library/react@npm:^16.0.0": version: 16.3.0 resolution: "@testing-library/react@npm:16.3.0" @@ -23624,6 +23679,15 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:5.1.3": + version: 5.1.3 + resolution: "aria-query@npm:5.1.3" + dependencies: + deep-equal: "npm:^2.0.5" + checksum: 10/e5da608a7c4954bfece2d879342b6c218b6b207e2d9e5af270b5e38ef8418f02d122afdc948b68e32649b849a38377785252059090d66fa8081da95d1609c0d2 + languageName: node + linkType: hard + "aria-query@npm:5.3.0": version: 5.3.0 resolution: "aria-query@npm:5.3.0" @@ -23640,7 +23704,7 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": +"array-buffer-byte-length@npm:^1.0.0, array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" dependencies: @@ -27393,6 +27457,32 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:^2.0.5": + version: 2.2.3 + resolution: "deep-equal@npm:2.2.3" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + call-bind: "npm:^1.0.5" + es-get-iterator: "npm:^1.1.3" + get-intrinsic: "npm:^1.2.2" + is-arguments: "npm:^1.1.1" + is-array-buffer: "npm:^3.0.2" + is-date-object: "npm:^1.0.5" + is-regex: "npm:^1.1.4" + is-shared-array-buffer: "npm:^1.0.2" + isarray: "npm:^2.0.5" + object-is: "npm:^1.1.5" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.4" + regexp.prototype.flags: "npm:^1.5.1" + side-channel: "npm:^1.0.4" + which-boxed-primitive: "npm:^1.0.2" + which-collection: "npm:^1.0.1" + which-typed-array: "npm:^1.1.13" + checksum: 10/1ce49d0b71d0f14d8ef991a742665eccd488dfc9b3cada069d4d7a86291e591c92d2589c832811dea182b4015736b210acaaebce6184be356c1060d176f5a05f + languageName: node + linkType: hard + "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -28611,7 +28701,7 @@ __metadata: languageName: node linkType: hard -"es-get-iterator@npm:^1.0.2": +"es-get-iterator@npm:^1.0.2, es-get-iterator@npm:^1.1.3": version: 1.1.3 resolution: "es-get-iterator@npm:1.1.3" dependencies: @@ -29663,6 +29753,7 @@ __metadata: "@backstage/integration-react": "workspace:^" "@backstage/plugin-api-docs": "workspace:^" "@backstage/plugin-auth-react": "workspace:^" + "@backstage/plugin-bui-themer": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-graph": "workspace:^" @@ -31215,7 +31306,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -33148,7 +33239,7 @@ __metadata: languageName: node linkType: hard -"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": +"is-array-buffer@npm:^3.0.2, is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": version: 3.0.5 resolution: "is-array-buffer@npm:3.0.5" dependencies: @@ -33621,7 +33712,7 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.2.1": +"is-regex@npm:^1.1.4, is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" dependencies: @@ -33670,7 +33761,7 @@ __metadata: languageName: node linkType: hard -"is-shared-array-buffer@npm:^1.0.4": +"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.4": version: 1.0.4 resolution: "is-shared-array-buffer@npm:1.0.4" dependencies: @@ -43096,6 +43187,15 @@ __metadata: languageName: node linkType: hard +"react@npm:^16.13.1 || ^17.0.0 || ^18.0.0, react@npm:^18.0.2": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/261137d3f3993eaa2368a83110466fc0e558bc2c7f7ae7ca52d94f03aac945f45146bd85e5f481044db1758a1dbb57879e2fcdd33924e2dde1bdc550ce73f7bf + languageName: node + linkType: hard + "react@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0": version: 19.1.1 resolution: "react@npm:19.1.1" @@ -43113,15 +43213,6 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.0.2": - version: 18.3.1 - resolution: "react@npm:18.3.1" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10/261137d3f3993eaa2368a83110466fc0e558bc2c7f7ae7ca52d94f03aac945f45146bd85e5f481044db1758a1dbb57879e2fcdd33924e2dde1bdc550ce73f7bf - languageName: node - linkType: hard - "read-cmd-shim@npm:^2.0.0": version: 2.0.0 resolution: "read-cmd-shim@npm:2.0.0" @@ -43396,7 +43487,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.3": +"regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.3": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -44905,7 +44996,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": +"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" dependencies: @@ -49105,7 +49196,7 @@ __metadata: languageName: node linkType: hard -"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": +"which-boxed-primitive@npm:^1.0.2, which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1" dependencies: @@ -49139,7 +49230,7 @@ __metadata: languageName: node linkType: hard -"which-collection@npm:^1.0.2": +"which-collection@npm:^1.0.1, which-collection@npm:^1.0.2": version: 1.0.2 resolution: "which-collection@npm:1.0.2" dependencies: @@ -49161,7 +49252,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" dependencies: From 501b159de42bde4a06769f19653dafc51fa15948 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Sep 2025 19:03:54 +0200 Subject: [PATCH 077/193] bui-themer: trim example components and add NFS support Signed-off-by: Patrik Oldsberg --- plugins/bui-themer/package.json | 2 + .../BuiThemerPage.tsx} | 5 +- .../index.ts | 3 +- .../ExampleComponent.test.tsx | 38 --- .../ExampleComponent/ExampleComponent.tsx | 52 --- .../ExampleFetchComponent.test.tsx | 34 -- .../ExampleFetchComponent.tsx | 322 ------------------ plugins/bui-themer/src/index.ts | 2 + .../bui-themer/src/{plugin.ts => plugin.tsx} | 31 +- yarn.lock | 2 + 10 files changed, 41 insertions(+), 450 deletions(-) rename plugins/bui-themer/src/components/{ExampleFetchComponent/index.ts => BuiThemerPage/BuiThemerPage.tsx} (90%) rename plugins/bui-themer/src/components/{ExampleComponent => BuiThemerPage}/index.ts (91%) delete mode 100644 plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx delete mode 100644 plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx delete mode 100644 plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx delete mode 100644 plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx rename plugins/bui-themer/src/{plugin.ts => plugin.tsx} (59%) diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json index c0d49b0879..f6f5faffd4 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/bui-themer/package.json @@ -25,8 +25,10 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", diff --git a/plugins/bui-themer/src/components/ExampleFetchComponent/index.ts b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx similarity index 90% rename from plugins/bui-themer/src/components/ExampleFetchComponent/index.ts rename to plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index b0be0a2a71..0328a3e6d6 100644 --- a/plugins/bui-themer/src/components/ExampleFetchComponent/index.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { ExampleFetchComponent } from './ExampleFetchComponent'; + +export function BuiThemerPage() { + return
TODO
; +} diff --git a/plugins/bui-themer/src/components/ExampleComponent/index.ts b/plugins/bui-themer/src/components/BuiThemerPage/index.ts similarity index 91% rename from plugins/bui-themer/src/components/ExampleComponent/index.ts rename to plugins/bui-themer/src/components/BuiThemerPage/index.ts index 66b3fc6c97..3d32f11259 100644 --- a/plugins/bui-themer/src/components/ExampleComponent/index.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { ExampleComponent } from './ExampleComponent'; + +export { BuiThemerPage } from './BuiThemerPage'; diff --git a/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx deleted file mode 100644 index f5f7f185c5..0000000000 --- a/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { ExampleComponent } from './ExampleComponent'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { screen } from '@testing-library/react'; -import { registerMswTestHooks, renderInTestApp } from '@backstage/test-utils'; - -describe('ExampleComponent', () => { - const server = setupServer(); - // Enable sane handlers for network requests - registerMswTestHooks(server); - - // setup mock response - beforeEach(() => { - server.use( - rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), - ); - }); - - it('should render', async () => { - await renderInTestApp(); - expect(screen.getByText('Welcome to bui-themer!')).toBeInTheDocument(); - }); -}); diff --git a/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx deleted file mode 100644 index 3a04bd7562..0000000000 --- a/plugins/bui-themer/src/components/ExampleComponent/ExampleComponent.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 { Typography, Grid } from '@material-ui/core'; -import { - InfoCard, - Header, - Page, - Content, - ContentHeader, - HeaderLabel, - SupportButton, -} from '@backstage/core-components'; -import { ExampleFetchComponent } from '../ExampleFetchComponent'; - -export const ExampleComponent = () => ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - - - - - - - -
-); diff --git a/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx deleted file mode 100644 index a3085e3dcb..0000000000 --- a/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { renderInTestApp } from '@backstage/test-utils'; -import { ExampleFetchComponent } from './ExampleFetchComponent'; - -describe('ExampleFetchComponent', () => { - it('renders the user table', async () => { - const { getAllByText, getByAltText, getByText, findByRole } = - await renderInTestApp(); - - // Wait for the table to render - const table = await findByRole('table'); - const nationality = getAllByText('GB'); - // Assert that the table contains the expected user data - expect(table).toBeInTheDocument(); - expect(getByAltText('Carolyn')).toBeInTheDocument(); - expect(getByText('Carolyn Moore')).toBeInTheDocument(); - expect(getByText('carolyn.moore@example.com')).toBeInTheDocument(); - expect(nationality[0]).toBeInTheDocument(); - }); -}); diff --git a/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx deleted file mode 100644 index 6c9e77094d..0000000000 --- a/plugins/bui-themer/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx +++ /dev/null @@ -1,322 +0,0 @@ -/* - * 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 { makeStyles } from '@material-ui/core/styles'; -import { - Table, - TableColumn, - Progress, - ResponseErrorPanel, -} from '@backstage/core-components'; -import useAsync from 'react-use/lib/useAsync'; - -export const exampleUsers = { - results: [ - { - gender: 'female', - name: { - title: 'Miss', - first: 'Carolyn', - last: 'Moore', - }, - email: 'carolyn.moore@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Carolyn', - nat: 'GB', - }, - { - gender: 'female', - name: { - title: 'Ms', - first: 'Esma', - last: 'Berberoğlu', - }, - email: 'esma.berberoglu@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Esma', - nat: 'TR', - }, - { - gender: 'female', - name: { - title: 'Ms', - first: 'Isabella', - last: 'Rhodes', - }, - email: 'isabella.rhodes@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', - nat: 'GB', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Derrick', - last: 'Carter', - }, - email: 'derrick.carter@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Derrick', - nat: 'IE', - }, - { - gender: 'female', - name: { - title: 'Miss', - first: 'Mattie', - last: 'Lambert', - }, - email: 'mattie.lambert@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mattie', - nat: 'AU', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Mijat', - last: 'Rakić', - }, - email: 'mijat.rakic@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mijat', - nat: 'RS', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Javier', - last: 'Reid', - }, - email: 'javier.reid@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Javier', - nat: 'US', - }, - { - gender: 'female', - name: { - title: 'Ms', - first: 'Isabella', - last: 'Li', - }, - email: 'isabella.li@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', - nat: 'CA', - }, - { - gender: 'female', - name: { - title: 'Mrs', - first: 'Stephanie', - last: 'Garrett', - }, - email: 'stephanie.garrett@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Stephanie', - nat: 'AU', - }, - { - gender: 'female', - name: { - title: 'Ms', - first: 'Antonia', - last: 'Núñez', - }, - email: 'antonia.nunez@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Antonia', - nat: 'ES', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Donald', - last: 'Young', - }, - email: 'donald.young@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Donald', - nat: 'US', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Iegor', - last: 'Holodovskiy', - }, - email: 'iegor.holodovskiy@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Iegor', - nat: 'UA', - }, - { - gender: 'female', - name: { - title: 'Madame', - first: 'Jessica', - last: 'David', - }, - email: 'jessica.david@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jessica', - nat: 'CH', - }, - { - gender: 'female', - name: { - title: 'Ms', - first: 'Eve', - last: 'Martinez', - }, - email: 'eve.martinez@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Eve', - nat: 'FR', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Caleb', - last: 'Silva', - }, - email: 'caleb.silva@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Caleb', - nat: 'US', - }, - { - gender: 'female', - name: { - title: 'Miss', - first: 'Marcia', - last: 'Jenkins', - }, - email: 'marcia.jenkins@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Marcia', - nat: 'US', - }, - { - gender: 'female', - name: { - title: 'Mrs', - first: 'Mackenzie', - last: 'Jones', - }, - email: 'mackenzie.jones@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mackenzie', - nat: 'NZ', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Jeremiah', - last: 'Gutierrez', - }, - email: 'jeremiah.gutierrez@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jeremiah', - nat: 'AU', - }, - { - gender: 'female', - name: { - title: 'Ms', - first: 'Luciara', - last: 'Souza', - }, - email: 'luciara.souza@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Luciara', - nat: 'BR', - }, - { - gender: 'male', - name: { - title: 'Mr', - first: 'Valgi', - last: 'da Cunha', - }, - email: 'valgi.dacunha@example.com', - picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Valgi', - nat: 'BR', - }, - ], -}; - -const useStyles = makeStyles({ - avatar: { - height: 32, - width: 32, - borderRadius: '50%', - }, -}); - -type User = { - gender: string; // "male" - name: { - title: string; // "Mr", - first: string; // "Duane", - last: string; // "Reed" - }; - email: string; // "duane.reed@example.com" - picture: string; // "https://api.dicebear.com/6.x/open-peeps/svg?seed=Duane" - nat: string; // "AU" -}; - -type DenseTableProps = { - users: User[]; -}; - -export const DenseTable = ({ users }: DenseTableProps) => { - const classes = useStyles(); - - const columns: TableColumn[] = [ - { title: 'Avatar', field: 'avatar' }, - { title: 'Name', field: 'name' }, - { title: 'Email', field: 'email' }, - { title: 'Nationality', field: 'nationality' }, - ]; - - const data = users.map(user => { - return { - avatar: ( - {user.name.first} - ), - name: `${user.name.first} ${user.name.last}`, - email: user.email, - nationality: user.nat, - }; - }); - - return ( -
- ); -}; - -export const ExampleFetchComponent = () => { - const { value, loading, error } = useAsync(async (): Promise => { - // Would use fetch in a real world example - return exampleUsers.results; - }, []); - - if (loading) { - return ; - } else if (error) { - return ; - } - - return ; -}; diff --git a/plugins/bui-themer/src/index.ts b/plugins/bui-themer/src/index.ts index 26bc35ebb1..4cf05a7f23 100644 --- a/plugins/bui-themer/src/index.ts +++ b/plugins/bui-themer/src/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { buiThemerPlugin, BuiThemerPage } from './plugin'; +export { default } from './plugin'; diff --git a/plugins/bui-themer/src/plugin.ts b/plugins/bui-themer/src/plugin.tsx similarity index 59% rename from plugins/bui-themer/src/plugin.ts rename to plugins/bui-themer/src/plugin.tsx index d87d035b12..78d3196126 100644 --- a/plugins/bui-themer/src/plugin.ts +++ b/plugins/bui-themer/src/plugin.tsx @@ -13,13 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { + convertLegacyRouteRef, + convertLegacyRouteRefs, +} from '@backstage/core-compat-api'; import { createPlugin, createRoutableExtension, } from '@backstage/core-plugin-api'; - +import { + createFrontendPlugin, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; import { rootRouteRef } from './routes'; +// Old system export const buiThemerPlugin = createPlugin({ id: 'bui-themer', routes: { @@ -31,7 +40,25 @@ export const BuiThemerPage = buiThemerPlugin.provide( createRoutableExtension({ name: 'BuiThemerPage', component: () => - import('./components/ExampleComponent').then(m => m.ExampleComponent), + import('./components/BuiThemerPage').then(m => m.BuiThemerPage), mountPoint: rootRouteRef, }), ); + +// New system +export default createFrontendPlugin({ + pluginId: 'bui-themer', + extensions: [ + PageBlueprint.make({ + params: { + path: '/bui-themer', + loader: () => + import('./components/BuiThemerPage').then(m => ), + routeRef: convertLegacyRouteRef(rootRouteRef), + }, + }), + ], + routes: convertLegacyRouteRefs({ + root: rootRouteRef, + }), +}); diff --git a/yarn.lock b/yarn.lock index cb9a75fa61..f22d88ee20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4329,9 +4329,11 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@material-ui/core": "npm:^4.9.13" From c56cb0460e6abdeec4e38aae8d61931d0e4b3748 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Sep 2025 19:11:20 +0200 Subject: [PATCH 078/193] bui-themer: add implementation plan Signed-off-by: Patrik Oldsberg --- .../components/BuiThemerPage/BuiThemerPage.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 0328a3e6d6..8fb72eda1c 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -14,6 +14,21 @@ * limitations under the License. */ +/* + +IMPLEMENTATION PLAN: + +- create a utility that converts the MUI theme palette into printed CSS variables +populating the variables in packages/ui/src/css/core.css +- the conversion utility will be created in an adjacent `convertMuiToBuiTheme` file, which defines a `convertMuiToBuiTheme` function that accepts a MUI theme instance and returns CSS in text format +- create tests for the conversion utility +- Create a page that shows the generated CSS variables and allows the user to copy them +- The page should show coverted CSS variables for all installed themes in the app, which can be acquired via the AppThemeApi, defined at packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +- The AppThemeApi can be accessed via the useApi(appThemeApiRef) hook +- The app themes only provide a `Provider` component that in turn provides the MUI theme. The theme can be accessed by wrapping the component in the theme's `Provider` component and then using the `useTheme` hook from MUI + +*/ + export function BuiThemerPage() { return
TODO
; } From 05ff15d1c0bee41d9e3ac4219c4c142d42e31541 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Sep 2025 19:20:54 +0200 Subject: [PATCH 079/193] bui-themer: refined implementation plan Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/BuiThemerPage.tsx | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 8fb72eda1c..2da27cad36 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -18,14 +18,36 @@ IMPLEMENTATION PLAN: -- create a utility that converts the MUI theme palette into printed CSS variables -populating the variables in packages/ui/src/css/core.css -- the conversion utility will be created in an adjacent `convertMuiToBuiTheme` file, which defines a `convertMuiToBuiTheme` function that accepts a MUI theme instance and returns CSS in text format -- create tests for the conversion utility -- Create a page that shows the generated CSS variables and allows the user to copy them -- The page should show coverted CSS variables for all installed themes in the app, which can be acquired via the AppThemeApi, defined at packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts -- The AppThemeApi can be accessed via the useApi(appThemeApiRef) hook -- The app themes only provide a `Provider` component that in turn provides the MUI theme. The theme can be accessed by wrapping the component in the theme's `Provider` component and then using the `useTheme` hook from MUI +- Create a converter utility that maps a MUI v5 `Theme` to BUI CSS variables, + covering palette, typography, spacing, and shape tokens to match + `packages/ui/src/css/core.css`. +- Place it in an adjacent `convertMuiToBuiTheme.ts` file exporting + `convertMuiToBuiTheme(theme: Theme, options?): string`, returning + deterministic CSS text. +- Scope output blocks as: + - `:root { ... }` for light themes + - `[data-theme-mode='dark'] { ... }` for dark themes + Optionally prefix by theme id (e.g. `[data-app-theme='']`) so multiple + theme blocks can coexist without conflicts. +- Do not modify `packages/ui/src/css/core.css`; generate CSS for copy/download + only. +- Define a mapping from MUI fields to `--bui-*` tokens with sensible fallbacks + aligning with core defaults. Handle success/warning/error/background/text + surfaces, borders, and link/hover states. +- Add tests for light/dark and fallback behavior using MUI `createTheme` and + snapshot the generated CSS. +- Build the page to list all installed themes via `useApi(appThemeApiRef)`, + render a minimal child under each theme `Provider`, and read `useTheme()` to + get the runtime theme instance. Provide Copy and Download actions and an + optional live preview applying the generated variables. +- Performance: memoize generated CSS by theme id and regenerate when the + installed theme list changes. Observing `activeThemeId$()` is optional unless + previewing the active theme. + +REQUIREMENTS: + +- No changes can be made outside the plugins/bui-themer/src directory +- Only components from the `@backstage/ui` package at `packages/ui` can be used */ From 6262191013b65f95fb8d2d49089b880621c51ac6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Sep 2025 19:46:05 +0200 Subject: [PATCH 080/193] bui-themer: initial implementation plan execution Signed-off-by: Patrik Oldsberg --- plugins/bui-themer/package.json | 2 + .../BuiThemerPage/BuiThemerPage.tsx | 297 ++++++++++++++++-- .../convertMuiToBuiTheme.test.ts | 278 ++++++++++++++++ .../BuiThemerPage/convertMuiToBuiTheme.ts | 286 +++++++++++++++++ yarn.lock | 185 ++++++++++- 5 files changed, 1015 insertions(+), 33 deletions(-) create mode 100644 plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts create mode 100644 plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json index f6f5faffd4..ff97c92ecb 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/bui-themer/package.json @@ -30,9 +30,11 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/theme": "workspace:^", + "@backstage/ui": "workspace:^", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.61", + "@mui/material": "^7.3.2", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 2da27cad36..b36c550081 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -14,43 +14,276 @@ * limitations under the License. */ -/* +import { useMemo, useState, useCallback, useEffect } from 'react'; +import { useApi } from '@backstage/core-plugin-api'; +import { appThemeApiRef } from '@backstage/core-plugin-api'; +import { useTheme } from '@mui/material/styles'; +import { + Box, + Button, + Card, + Container, + Flex, + Grid, + HeaderPage, + Text, + TextField, + Switch, +} from '@backstage/ui'; +import { + convertMuiToBuiTheme, + ConvertMuiToBuiThemeOptions, +} from './convertMuiToBuiTheme'; -IMPLEMENTATION PLAN: +interface ThemePreviewProps { + themeId: string; + themeTitle: string; + variant: 'light' | 'dark'; + Provider: React.ComponentType<{ children: React.ReactNode }>; +} -- Create a converter utility that maps a MUI v5 `Theme` to BUI CSS variables, - covering palette, typography, spacing, and shape tokens to match - `packages/ui/src/css/core.css`. -- Place it in an adjacent `convertMuiToBuiTheme.ts` file exporting - `convertMuiToBuiTheme(theme: Theme, options?): string`, returning - deterministic CSS text. -- Scope output blocks as: - - `:root { ... }` for light themes - - `[data-theme-mode='dark'] { ... }` for dark themes - Optionally prefix by theme id (e.g. `[data-app-theme='']`) so multiple - theme blocks can coexist without conflicts. -- Do not modify `packages/ui/src/css/core.css`; generate CSS for copy/download - only. -- Define a mapping from MUI fields to `--bui-*` tokens with sensible fallbacks - aligning with core defaults. Handle success/warning/error/background/text - surfaces, borders, and link/hover states. -- Add tests for light/dark and fallback behavior using MUI `createTheme` and - snapshot the generated CSS. -- Build the page to list all installed themes via `useApi(appThemeApiRef)`, - render a minimal child under each theme `Provider`, and read `useTheme()` to - get the runtime theme instance. Provide Copy and Download actions and an - optional live preview applying the generated variables. -- Performance: memoize generated CSS by theme id and regenerate when the - installed theme list changes. Observing `activeThemeId$()` is optional unless - previewing the active theme. +// Memoization cache for generated CSS +const cssCache = new Map(); -REQUIREMENTS: +interface ThemeContentProps { + themeId: string; + themeTitle: string; + variant: 'light' | 'dark'; +} -- No changes can be made outside the plugins/bui-themer/src directory -- Only components from the `@backstage/ui` package at `packages/ui` can be used +function ThemeContent({ themeId, themeTitle, variant }: ThemeContentProps) { + const [generatedCss, setGeneratedCss] = useState(''); + const [isPreviewMode, setIsPreviewMode] = useState(false); + const [includeThemeId, setIncludeThemeId] = useState(false); + const muiTheme = useTheme(); -*/ + const css = useMemo(() => { + // Create cache key based on theme properties and options + const cacheKey = `${themeId}-${includeThemeId}-${JSON.stringify({ + palette: muiTheme.palette, + typography: muiTheme.typography, + spacing: muiTheme.spacing, + shape: muiTheme.shape, + })}`; + + // Check cache first + if (cssCache.has(cacheKey)) { + return cssCache.get(cacheKey)!; + } + + const options: ConvertMuiToBuiThemeOptions = { + themeId, + includeThemeId, + }; + const result = convertMuiToBuiTheme(muiTheme, options); + + // Cache the result + cssCache.set(cacheKey, result); + + // Clean up old cache entries (keep only last 50) + if (cssCache.size > 50) { + const firstKey = cssCache.keys().next().value; + if (firstKey) { + cssCache.delete(firstKey); + } + } + + return result; + }, [muiTheme, themeId, includeThemeId]); + + useEffect(() => { + setGeneratedCss(css); + }, [css]); + + const handleCopy = useCallback(() => { + window.navigator.clipboard.writeText(generatedCss); + }, [generatedCss]); + + const handleDownload = useCallback(() => { + const blob = new Blob([generatedCss], { type: 'text/css' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bui-theme-${themeId}.css`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, [generatedCss, themeId]); + + const handlePreviewToggle = useCallback(() => { + setIsPreviewMode(!isPreviewMode); + if (!isPreviewMode) { + // Apply the generated CSS to a style element + const styleId = `bui-theme-preview-${themeId}`; + let styleElement = document.getElementById(styleId); + if (!styleElement) { + styleElement = document.createElement('style'); + styleElement.id = styleId; + document.head.appendChild(styleElement); + } + styleElement.textContent = generatedCss; + } else { + // Remove the preview styles + const styleElement = document.getElementById( + `bui-theme-preview-${themeId}`, + ); + if (styleElement) { + styleElement.remove(); + } + } + }, [isPreviewMode, generatedCss, themeId]); + + return ( + + + + + {themeTitle} + + {variant} theme + + + + + + + + + + + + + + + + Generated CSS: + + + + + {isPreviewMode && ( + + + Live Preview: + + + Solid Button + + + Tint Button + + + Surface Card + + + )} + + + + ); +} + +function ThemePreview({ + themeId, + themeTitle, + variant, + Provider, +}: ThemePreviewProps) { + return ( + + + + ); +} export function BuiThemerPage() { - return
TODO
; + const appThemeApi = useApi(appThemeApiRef); + const installedThemes = appThemeApi.getInstalledThemes(); + + return ( + + + + + Convert MUI v5 themes to BUI CSS variables. Select a theme to generate + the corresponding BUI CSS variables that can be copied or downloaded. + + + + + {installedThemes.length === 0 ? ( + + + + No themes found. Please install some themes in your Backstage + app. + + + + ) : ( + + {installedThemes.map(theme => ( + + + + ))} + + )} + + + ); } diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts new file mode 100644 index 0000000000..e45f48f3a0 --- /dev/null +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts @@ -0,0 +1,278 @@ +/* + * 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 { createTheme } from '@mui/material/styles'; +import { convertMuiToBuiTheme } from './convertMuiToBuiTheme'; + +describe('convertMuiToBuiTheme', () => { + it('should generate CSS for light theme', () => { + const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#1976d2', + dark: '#115293', + }, + background: { + default: '#f5f5f5', + paper: '#ffffff', + }, + text: { + primary: '#000000', + secondary: '#666666', + }, + }, + typography: { + fontFamily: 'Roboto, sans-serif', + h1: { + fontSize: '2.5rem', + }, + body1: { + fontSize: '1rem', + }, + }, + spacing: 8, + shape: { + borderRadius: 4, + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain(':root {'); + expect(result).toContain('--bui-font-regular: Roboto, sans-serif;'); + expect(result).toContain('--bui-space: 8px;'); + expect(result).toContain('--bui-radius-3: 4px;'); + expect(result).toContain('--bui-bg: #f5f5f5;'); + expect(result).toContain('--bui-bg-surface-1: #ffffff;'); + expect(result).toContain('--bui-fg-primary: #000000;'); + expect(result).toContain('--bui-fg-secondary: #666666;'); + expect(result).toContain('--bui-bg-solid: #1976d2;'); + }); + + it('should generate CSS for dark theme', () => { + const theme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#90caf9', + dark: '#42a5f5', + }, + background: { + default: '#121212', + paper: '#1e1e1e', + }, + text: { + primary: '#ffffff', + secondary: '#b3b3b3', + }, + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain("[data-theme-mode='dark'] {"); + expect(result).toContain('--bui-bg: #121212;'); + expect(result).toContain('--bui-bg-surface-1: #1e1e1e;'); + expect(result).toContain('--bui-fg-primary: #ffffff;'); + expect(result).toContain('--bui-fg-secondary: #b3b3b3;'); + }); + + it('should include theme ID scoping when requested', () => { + const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#1976d2', + }, + }, + }); + + const result = convertMuiToBuiTheme(theme, { + themeId: 'my-theme', + includeThemeId: true, + }); + + expect(result).toContain("[data-app-theme='my-theme'] :root {"); + }); + + it('should handle missing theme properties gracefully', () => { + const theme = createTheme({}); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain(':root {'); + expect(result).toContain('--bui-font-weight-regular: 400;'); + expect(result).toContain('--bui-font-weight-bold: 700;'); + // Should have default values even when theme properties are missing + expect(result).toContain('--bui-black: #000;'); + expect(result).toContain('--bui-white: #fff;'); + }); + + it('should generate proper gray scale for light theme', () => { + const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#1976d2', + }, + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-gray-1: #f8f8f8;'); + expect(result).toContain('--bui-gray-8: #595959;'); + }); + + it('should generate proper gray scale for dark theme', () => { + const theme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#90caf9', + }, + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-gray-1: #191919;'); + expect(result).toContain('--bui-gray-8: #b4b4b4;'); + }); + + it('should generate surface colors correctly', () => { + const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#1976d2', + dark: '#115293', + }, + error: { + main: '#f44336', + light: '#ffcdd2', + }, + warning: { + main: '#ff9800', + light: '#ffe0b2', + }, + success: { + main: '#4caf50', + light: '#c8e6c9', + }, + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-bg-solid: #1976d2;'); + expect(result).toContain('--bui-bg-solid-hover: #115293;'); + expect(result).toContain('--bui-bg-danger: #ffcdd2;'); + expect(result).toContain('--bui-bg-warning: #ffe0b2;'); + expect(result).toContain('--bui-bg-success: #c8e6c9;'); + }); + + it('should generate foreground colors correctly', () => { + const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#1976d2', + dark: '#115293', + }, + error: { + main: '#f44336', + }, + warning: { + main: '#ff9800', + }, + success: { + main: '#4caf50', + }, + text: { + disabled: '#cccccc', + }, + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-fg-link: #1976d2;'); + expect(result).toContain('--bui-fg-link-hover: #115293;'); + expect(result).toContain('--bui-fg-disabled: #cccccc;'); + expect(result).toContain('--bui-fg-danger: #f44336;'); + expect(result).toContain('--bui-fg-warning: #ff9800;'); + expect(result).toContain('--bui-fg-success: #4caf50;'); + }); + + it('should generate border colors correctly', () => { + const theme = createTheme({ + palette: { + mode: 'light', + error: { + main: '#f44336', + }, + warning: { + main: '#ff9800', + }, + success: { + main: '#4caf50', + }, + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-border: rgba(0, 0, 0, 0.1);'); + expect(result).toContain('--bui-border-hover: rgba(0, 0, 0, 0.2);'); + expect(result).toContain('--bui-border-danger: #f44336;'); + expect(result).toContain('--bui-border-warning: #ff9800;'); + expect(result).toContain('--bui-border-success: #4caf50;'); + }); + + it('should handle function-based spacing', () => { + const theme = createTheme({ + spacing: (factor: number) => `${factor * 4}px`, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-space: 4px;'); + }); + + it('should handle string-based spacing', () => { + const theme = createTheme({ + spacing: '8px', + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-space: calc(1 * 8px);'); + }); + + it('should handle string-based border radius', () => { + const theme = createTheme({ + shape: { + borderRadius: '8px', + }, + }); + + const result = convertMuiToBuiTheme(theme); + + expect(result).toContain('--bui-radius-3: 8px;'); + }); +}); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts new file mode 100644 index 0000000000..e1f1618760 --- /dev/null +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts @@ -0,0 +1,286 @@ +/* + * 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 { Theme as Mui5Theme } from '@mui/material/styles'; + +export interface ConvertMuiToBuiThemeOptions { + /** + * Theme ID to use for scoping CSS variables + */ + themeId?: string; + /** + * Whether to include theme ID scoping in the CSS + */ + includeThemeId?: boolean; +} + +/** + * Converts a MUI v5 Theme to BUI CSS variables + * @param theme - The MUI v5 theme to convert + * @param options - Conversion options + * @returns CSS string with BUI variables + */ +export function convertMuiToBuiTheme( + theme: Mui5Theme, + options: ConvertMuiToBuiThemeOptions = {}, +): string { + const { themeId, includeThemeId = false } = options; + const isDark = theme.palette.mode === 'dark'; + + // Generate CSS variables based on theme + const variables = generateBuiVariables(theme); + + // Create CSS selector based on theme mode and ID + let selector = isDark ? "[data-theme-mode='dark']" : ':root'; + if (includeThemeId && themeId) { + selector = `[data-app-theme='${themeId}'] ${selector}`; + } + + return `${selector} {\n${variables}\n}`; +} + +/** + * Generates BUI CSS variables from MUI theme + */ +function generateBuiVariables(theme: Mui5Theme): string { + const variables: string[] = []; + + // Font families + if (theme.typography.fontFamily) { + variables.push(` --bui-font-regular: ${theme.typography.fontFamily};`); + } + + // Font weights + variables.push( + ` --bui-font-weight-regular: ${ + theme.typography.fontWeightRegular || 400 + };`, + ); + variables.push( + ` --bui-font-weight-bold: ${theme.typography.fontWeightBold || 600};`, + ); + + // Font sizes - map MUI typography scale to BUI scale + const fontSizeMap = { + h1: '--bui-font-size-10', + h2: '--bui-font-size-8', + h3: '--bui-font-size-7', + h4: '--bui-font-size-6', + h5: '--bui-font-size-5', + h6: '--bui-font-size-4', + body1: '--bui-font-size-4', + body2: '--bui-font-size-3', + caption: '--bui-font-size-2', + overline: '--bui-font-size-1', + }; + + Object.entries(fontSizeMap).forEach(([muiKey, buiVar]) => { + const typographyVariant = + theme.typography[muiKey as keyof typeof theme.typography]; + const fontSize = + typeof typographyVariant === 'object' && typographyVariant?.fontSize + ? typographyVariant.fontSize + : undefined; + if (fontSize) { + variables.push(` ${buiVar}: ${fontSize};`); + } + }); + + // Spacing - map MUI spacing to BUI spacing scale + if (theme.spacing) { + const spacingUnit = + typeof theme.spacing === 'function' ? theme.spacing(1) : theme.spacing; + const spacingValue = + typeof spacingUnit === 'number' ? `${spacingUnit}px` : spacingUnit; + variables.push(` --bui-space: ${spacingValue};`); + } + + // Border radius + if (theme.shape?.borderRadius) { + const radius = theme.shape.borderRadius; + const radiusValue = typeof radius === 'number' ? `${radius}px` : radius; + variables.push(` --bui-radius-3: ${radiusValue};`); + } + + // Colors - map MUI palette to BUI color tokens + const palette = theme.palette; + + // Base colors + if (palette.common?.black) { + variables.push(` --bui-black: ${palette.common.black};`); + } + if (palette.common?.white) { + variables.push(` --bui-white: ${palette.common.white};`); + } + + // Gray scale - generate from primary color or use defaults + const grayScale = generateGrayScale(palette.mode === 'dark'); + Object.entries(grayScale).forEach(([key, value]) => { + variables.push(` --bui-gray-${key}: ${value};`); + }); + + // Background colors + if (palette.background?.default) { + variables.push(` --bui-bg: ${palette.background.default};`); + } + if (palette.background?.paper) { + variables.push(` --bui-bg-surface-1: ${palette.background.paper};`); + } + + // Generate surface colors + const surfaceColors = generateSurfaceColors(palette); + Object.entries(surfaceColors).forEach(([key, value]) => { + variables.push(` --bui-bg-${key}: ${value};`); + }); + + // Foreground colors + if (palette.text?.primary) { + variables.push(` --bui-fg-primary: ${palette.text.primary};`); + } + if (palette.text?.secondary) { + variables.push(` --bui-fg-secondary: ${palette.text.secondary};`); + } + + // Generate foreground colors + const foregroundColors = generateForegroundColors(palette); + Object.entries(foregroundColors).forEach(([key, value]) => { + variables.push(` --bui-fg-${key}: ${value};`); + }); + + // Border colors + const borderColors = generateBorderColors(palette); + Object.entries(borderColors).forEach(([key, value]) => { + variables.push(` --bui-border${key ? `-${key}` : ''}: ${value};`); + }); + + // Special colors + if (palette.primary?.main) { + variables.push(` --bui-ring: ${palette.primary.main};`); + } + + return variables.join('\n'); +} + +/** + * Generates gray scale colors + */ +function generateGrayScale(isDark: boolean): Record { + if (isDark) { + return { + '1': '#191919', + '2': '#242424', + '3': '#373737', + '4': '#464646', + '5': '#575757', + '6': '#7b7b7b', + '7': '#9e9e9e', + '8': '#b4b4b4', + }; + } + + return { + '1': '#f8f8f8', + '2': '#ececec', + '3': '#d9d9d9', + '4': '#c1c1c1', + '5': '#9e9e9e', + '6': '#8c8c8c', + '7': '#757575', + '8': '#595959', + }; +} + +/** + * Generates surface background colors + */ +function generateSurfaceColors( + palette: Mui5Theme['palette'], +): Record { + const isDark = palette.mode === 'dark'; + + // Helper function to get tint colors with proper fallbacks + const getTintColor = (opacity: string) => { + if (palette.primary?.main) { + return `${palette.primary.main}${opacity}`; + } + return isDark + ? `rgba(156, 201, 255, ${opacity === '40' ? '0.12' : '0.16'})` + : `rgba(31, 84, 147, ${opacity === '40' ? '0.4' : '0.6'})`; + }; + + // Helper function to get fallback colors based on theme mode + const getFallbackColor = (lightColor: string, darkColor: string) => { + return isDark ? darkColor : lightColor; + }; + + return { + 'surface-2': getFallbackColor('#ececec', '#242424'), + solid: palette.primary?.main || getFallbackColor('#1f5493', '#9cc9ff'), + 'solid-hover': + palette.primary?.dark || getFallbackColor('#163a66', '#83b9fd'), + 'solid-pressed': + palette.primary?.dark || getFallbackColor('#0f2b4e', '#83b9fd'), + 'solid-disabled': getFallbackColor('#ebebeb', '#222222'), + tint: 'transparent', + 'tint-hover': getTintColor('40'), + 'tint-pressed': getTintColor('60'), + 'tint-disabled': getFallbackColor('#ebebeb', 'transparent'), + danger: palette.error?.light || getFallbackColor('#feebe7', '#3b1219'), + warning: palette.warning?.light || getFallbackColor('#fff2b2', '#302008'), + success: palette.success?.light || getFallbackColor('#e6f6eb', '#132d21'), + }; +} + +/** + * Generates foreground colors + */ +function generateForegroundColors( + palette: Mui5Theme['palette'], +): Record { + const isDark = palette.mode === 'dark'; + + return { + link: palette.primary?.main || (isDark ? '#9cc9ff' : '#1f5493'), + 'link-hover': palette.primary?.dark || (isDark ? '#7eb5f7' : '#1f2d5c'), + disabled: palette.text?.disabled || (isDark ? '#9e9e9e' : '#9e9e9e'), + solid: isDark ? '#101821' : '#ffffff', + 'solid-disabled': isDark ? '#575757' : '#9c9c9c', + tint: palette.primary?.main || (isDark ? '#9cc9ff' : '#1f5493'), + 'tint-disabled': isDark ? '#575757' : '#9e9e9e', + danger: palette.error?.main || '#e22b2b', + warning: palette.warning?.main || '#e36d05', + success: palette.success?.main || '#1db954', + }; +} + +/** + * Generates border colors + */ +function generateBorderColors( + palette: Mui5Theme['palette'], +): Record { + const isDark = palette.mode === 'dark'; + + return { + '': isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)', + hover: isDark ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.2)', + pressed: isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.4)', + disabled: isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)', + danger: palette.error?.main || '#f87a7a', + warning: palette.warning?.main || '#e36d05', + success: palette.success?.main || '#53db83', + }; +} diff --git a/yarn.lock b/yarn.lock index f22d88ee20..41f7dc6d64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2435,6 +2435,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.28.3": + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 + languageName: node + linkType: hard + "@babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" @@ -4336,9 +4343,11 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" + "@backstage/ui": "workspace:^" "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:^4.0.0-alpha.61" + "@mui/material": "npm:^7.3.2" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^14.0.0" "@testing-library/user-event": "npm:^14.0.0" @@ -8184,6 +8193,19 @@ __metadata: languageName: node linkType: hard +"@emotion/cache@npm:^11.14.0": + version: 11.14.0 + resolution: "@emotion/cache@npm:11.14.0" + dependencies: + "@emotion/memoize": "npm:^0.9.0" + "@emotion/sheet": "npm:^1.4.0" + "@emotion/utils": "npm:^1.4.2" + "@emotion/weak-memoize": "npm:^0.4.0" + stylis: "npm:4.2.0" + checksum: 10/52336b28a27b07dde8fcdfd80851cbd1487672bbd4db1e24cca1440c95d8a6a968c57b0453c2b7c88d9b432b717f99554dbecc05b5cdef27933299827e69fd8e + languageName: node + linkType: hard + "@emotion/hash@npm:^0.8.0": version: 0.8.0 resolution: "@emotion/hash@npm:0.8.0" @@ -11170,6 +11192,13 @@ __metadata: languageName: node linkType: hard +"@mui/core-downloads-tracker@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/core-downloads-tracker@npm:7.3.2" + checksum: 10/01c0976e5fb6a8f0b59ebd58019af5452d7761e97daf0f2ef121bc3323508edf2fea1b13ea1a54b0098d6ee5eef70041c9722fe43291b237b989b6813a24b71e + languageName: node + linkType: hard + "@mui/material@npm:^5.12.2": version: 5.16.14 resolution: "@mui/material@npm:5.16.14" @@ -11203,6 +11232,42 @@ __metadata: languageName: node linkType: hard +"@mui/material@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/material@npm:7.3.2" + dependencies: + "@babel/runtime": "npm:^7.28.3" + "@mui/core-downloads-tracker": "npm:^7.3.2" + "@mui/system": "npm:^7.3.2" + "@mui/types": "npm:^7.4.6" + "@mui/utils": "npm:^7.3.2" + "@popperjs/core": "npm:^2.11.8" + "@types/react-transition-group": "npm:^4.4.12" + clsx: "npm:^2.1.1" + csstype: "npm:^3.1.3" + prop-types: "npm:^15.8.1" + react-is: "npm:^19.1.1" + react-transition-group: "npm:^4.4.5" + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@mui/material-pigment-css": ^7.3.2 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@mui/material-pigment-css": + optional: true + "@types/react": + optional: true + checksum: 10/e9a5348060d6e6e6cb1ce0fe299a582fa9b37b56dff46b62f72d029d5078acefd56a258e559b29bb3d12e89d0a81fd4f1194ce07cc136bbf03fdb4878fd9b3c0 + languageName: node + linkType: hard + "@mui/private-theming@npm:^5.16.14": version: 5.16.14 resolution: "@mui/private-theming@npm:5.16.14" @@ -11220,6 +11285,23 @@ __metadata: languageName: node linkType: hard +"@mui/private-theming@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/private-theming@npm:7.3.2" + dependencies: + "@babel/runtime": "npm:^7.28.3" + "@mui/utils": "npm:^7.3.2" + prop-types: "npm:^15.8.1" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/5b733fc9e09e5b056af3e448899c665c6d1560ec56f593811fc88b9c5499ad42d8365551b182473880de6141a206da4f1f9e5b2ebcea12461dc6d8954e2a3ed7 + languageName: node + linkType: hard + "@mui/styled-engine@npm:^5.16.14": version: 5.16.14 resolution: "@mui/styled-engine@npm:5.16.14" @@ -11241,6 +11323,29 @@ __metadata: languageName: node linkType: hard +"@mui/styled-engine@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/styled-engine@npm:7.3.2" + dependencies: + "@babel/runtime": "npm:^7.28.3" + "@emotion/cache": "npm:^11.14.0" + "@emotion/serialize": "npm:^1.3.3" + "@emotion/sheet": "npm:^1.4.0" + csstype: "npm:^3.1.3" + prop-types: "npm:^15.8.1" + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: 10/f31a6080b950cc66edd19a5285d30e0557fc16ef07a5e9027d3479f18dda4a569a62976c0fa396a99b077c01000c4fced3c8ab3ea0f6bcd82d6db06b821503b2 + languageName: node + linkType: hard + "@mui/styles@npm:^5.14.18": version: 5.16.14 resolution: "@mui/styles@npm:5.16.14" @@ -11300,6 +11405,34 @@ __metadata: languageName: node linkType: hard +"@mui/system@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/system@npm:7.3.2" + dependencies: + "@babel/runtime": "npm:^7.28.3" + "@mui/private-theming": "npm:^7.3.2" + "@mui/styled-engine": "npm:^7.3.2" + "@mui/types": "npm:^7.4.6" + "@mui/utils": "npm:^7.3.2" + clsx: "npm:^2.1.1" + csstype: "npm:^3.1.3" + prop-types: "npm:^15.8.1" + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 10/1e157027a693877a246c36e5c98dd988172daedb3430f95843a508fa3f8a707c394fe216078a17868d5d25690ea2f4c5af007dffff639436c36d982fb16e920a + languageName: node + linkType: hard + "@mui/types@npm:^7.2.15": version: 7.2.19 resolution: "@mui/types@npm:7.2.19" @@ -11312,6 +11445,20 @@ __metadata: languageName: node linkType: hard +"@mui/types@npm:^7.4.6": + version: 7.4.6 + resolution: "@mui/types@npm:7.4.6" + dependencies: + "@babel/runtime": "npm:^7.28.3" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/6263ef077332b75e01c1b347339f100f8743d8135efdee4a6c167ce70c69ca29d9face54c53395cbc1c8a8f5453fe8ef3f4457842cf0c05318f6ef6cde44456e + languageName: node + linkType: hard + "@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.16.14": version: 5.16.14 resolution: "@mui/utils@npm:5.16.14" @@ -11332,6 +11479,26 @@ __metadata: languageName: node linkType: hard +"@mui/utils@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/utils@npm:7.3.2" + dependencies: + "@babel/runtime": "npm:^7.28.3" + "@mui/types": "npm:^7.4.6" + "@types/prop-types": "npm:^15.7.15" + clsx: "npm:^2.1.1" + prop-types: "npm:^15.8.1" + react-is: "npm:^19.1.1" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/defcebcb8e49cdbdf271e49066d78046d2d25d4c8177176a6c5d3368fe85e6a280c7be71dbf356b434863d96d69236956afe6a4a47bc394de6aeecfa8bc24fda + languageName: node + linkType: hard + "@n1ru4l/push-pull-async-iterable-iterator@npm:^3.1.0": version: 3.1.0 resolution: "@n1ru4l/push-pull-async-iterable-iterator@npm:3.1.0" @@ -20765,7 +20932,7 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.3": +"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.15, @types/prop-types@npm:^15.7.3": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" checksum: 10/31aa2f59b28f24da6fb4f1d70807dae2aedfce090ec63eaf9ea01727a9533ef6eaf017de5bff99fbccad7d1c9e644f52c6c2ba30869465dd22b1a7221c29f356 @@ -20879,6 +21046,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.12": + version: 4.4.12 + resolution: "@types/react-transition-group@npm:4.4.12" + peerDependencies: + "@types/react": "*" + checksum: 10/ea14bc84f529a3887f9954b753843820ac8a3c49fcdfec7840657ecc6a8800aad98afdbe4b973eb96c7252286bde38476fcf64b1c09527354a9a9366e516d9a2 + languageName: node + linkType: hard + "@types/react-virtualized-auto-sizer@npm:^1.0.1": version: 1.0.4 resolution: "@types/react-virtualized-auto-sizer@npm:1.0.4" @@ -42751,6 +42927,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^19.1.1": + version: 19.1.1 + resolution: "react-is@npm:19.1.1" + checksum: 10/44e0937da1f0da1d5dbd4f01972870768ef207f8a49717f4491f5022454f34c956cb66be560aee5286387169853b0283a81e1f419b51dc62654c8710dc98065a + languageName: node + linkType: hard + "react-json-view@npm:^1.21.3": version: 1.21.3 resolution: "react-json-view@npm:1.21.3" From 0d4ded62f81e76c674bbf05ab41857aeb10df11b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 12:34:35 +0200 Subject: [PATCH 081/193] bui-themer: get rid of most hardcoded defaults Signed-off-by: Patrik Oldsberg --- .../convertMuiToBuiTheme.test.ts | 60 +++++ .../BuiThemerPage/convertMuiToBuiTheme.ts | 228 +++++++++++++++--- 2 files changed, 261 insertions(+), 27 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts index e45f48f3a0..ffbd4fc392 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts @@ -275,4 +275,64 @@ describe('convertMuiToBuiTheme', () => { expect(result).toContain('--bui-radius-3: 8px;'); }); + + describe('early validation', () => { + it('should throw error when theme is undefined', () => { + expect(() => convertMuiToBuiTheme(undefined as any)).toThrow( + 'Theme is required', + ); + }); + + it('should throw error when theme palette is missing', () => { + const theme = { typography: {}, spacing: () => 8, shape: {} } as any; + expect(() => convertMuiToBuiTheme(theme)).toThrow( + 'Theme palette is required', + ); + }); + + it('should throw error when theme palette mode is missing', () => { + const theme = { + palette: {}, + typography: {}, + spacing: () => 8, + shape: {}, + } as any; + expect(() => convertMuiToBuiTheme(theme)).toThrow( + 'Theme palette mode is required', + ); + }); + + it('should throw error when theme typography is missing', () => { + const theme = { + palette: { mode: 'light' }, + spacing: () => 8, + shape: {}, + } as any; + expect(() => convertMuiToBuiTheme(theme)).toThrow( + 'Theme typography is required', + ); + }); + + it('should throw error when theme spacing is missing', () => { + const theme = { + palette: { mode: 'light' }, + typography: {}, + shape: {}, + } as any; + expect(() => convertMuiToBuiTheme(theme)).toThrow( + 'Theme spacing is required', + ); + }); + + it('should throw error when theme shape is missing', () => { + const theme = { + palette: { mode: 'light' }, + typography: {}, + spacing: () => 8, + } as any; + expect(() => convertMuiToBuiTheme(theme)).toThrow( + 'Theme shape is required', + ); + }); + }); }); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts index e1f1618760..dee1c1fd72 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts @@ -15,6 +15,7 @@ */ import { Theme as Mui5Theme } from '@mui/material/styles'; +import { themes } from '@backstage/theme'; export interface ConvertMuiToBuiThemeOptions { /** @@ -37,6 +38,31 @@ export function convertMuiToBuiTheme( theme: Mui5Theme, options: ConvertMuiToBuiThemeOptions = {}, ): string { + // Early validation of required theme properties + if (!theme) { + throw new Error('Theme is required'); + } + + if (!theme.palette) { + throw new Error('Theme palette is required'); + } + + if (!theme.palette.mode) { + throw new Error('Theme palette mode is required'); + } + + if (!theme.typography) { + throw new Error('Theme typography is required'); + } + + if (!theme.spacing) { + throw new Error('Theme spacing is required'); + } + + if (!theme.shape) { + throw new Error('Theme shape is required'); + } + const { themeId, includeThemeId = false } = options; const isDark = theme.palette.mode === 'dark'; @@ -211,36 +237,92 @@ function generateSurfaceColors( ): Record { const isDark = palette.mode === 'dark'; - // Helper function to get tint colors with proper fallbacks + // Get the default Backstage theme for fallback values + const defaultTheme = isDark ? themes.dark : themes.light; + const defaultMuiTheme = defaultTheme.getTheme('v5'); + + if (!defaultMuiTheme) { + throw new Error( + `Failed to get MUI v5 theme from Backstage ${ + isDark ? 'dark' : 'light' + } theme`, + ); + } + + const defaultPalette = defaultMuiTheme.palette; + if (!defaultPalette) { + throw new Error( + `Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`, + ); + } + + // Helper function to get tint colors const getTintColor = (opacity: string) => { - if (palette.primary?.main) { - return `${palette.primary.main}${opacity}`; + const primaryColor = palette.primary?.main || defaultPalette.primary?.main; + if (!primaryColor) { + throw new Error('Primary color not found in current or default theme'); } - return isDark - ? `rgba(156, 201, 255, ${opacity === '40' ? '0.12' : '0.16'})` - : `rgba(31, 84, 147, ${opacity === '40' ? '0.4' : '0.6'})`; + return `${primaryColor}${opacity}`; }; - // Helper function to get fallback colors based on theme mode - const getFallbackColor = (lightColor: string, darkColor: string) => { + // Helper function to get colors based on theme mode + const getThemeColor = (lightColor: string, darkColor: string) => { return isDark ? darkColor : lightColor; }; return { - 'surface-2': getFallbackColor('#ececec', '#242424'), - solid: palette.primary?.main || getFallbackColor('#1f5493', '#9cc9ff'), + 'surface-2': getThemeColor('#ececec', '#242424'), + solid: + palette.primary?.main || + defaultPalette.primary?.main || + (() => { + throw new Error('Primary color not found in current or default theme'); + })(), 'solid-hover': - palette.primary?.dark || getFallbackColor('#163a66', '#83b9fd'), + palette.primary?.dark || + defaultPalette.primary?.dark || + (() => { + throw new Error( + 'Primary dark color not found in current or default theme', + ); + })(), 'solid-pressed': - palette.primary?.dark || getFallbackColor('#0f2b4e', '#83b9fd'), - 'solid-disabled': getFallbackColor('#ebebeb', '#222222'), + palette.primary?.dark || + defaultPalette.primary?.dark || + (() => { + throw new Error( + 'Primary dark color not found in current or default theme', + ); + })(), + 'solid-disabled': getThemeColor('#ebebeb', '#222222'), tint: 'transparent', 'tint-hover': getTintColor('40'), 'tint-pressed': getTintColor('60'), - 'tint-disabled': getFallbackColor('#ebebeb', 'transparent'), - danger: palette.error?.light || getFallbackColor('#feebe7', '#3b1219'), - warning: palette.warning?.light || getFallbackColor('#fff2b2', '#302008'), - success: palette.success?.light || getFallbackColor('#e6f6eb', '#132d21'), + 'tint-disabled': getThemeColor('#ebebeb', 'transparent'), + danger: + palette.error?.light || + defaultPalette.error?.light || + (() => { + throw new Error( + 'Error light color not found in current or default theme', + ); + })(), + warning: + palette.warning?.light || + defaultPalette.warning?.light || + (() => { + throw new Error( + 'Warning light color not found in current or default theme', + ); + })(), + success: + palette.success?.light || + defaultPalette.success?.light || + (() => { + throw new Error( + 'Success light color not found in current or default theme', + ); + })(), }; } @@ -252,17 +334,75 @@ function generateForegroundColors( ): Record { const isDark = palette.mode === 'dark'; + // Get the default Backstage theme for fallback values + const defaultTheme = isDark ? themes.dark : themes.light; + const defaultMuiTheme = defaultTheme.getTheme('v5'); + + if (!defaultMuiTheme) { + throw new Error( + `Failed to get MUI v5 theme from Backstage ${ + isDark ? 'dark' : 'light' + } theme`, + ); + } + + const defaultPalette = defaultMuiTheme.palette; + if (!defaultPalette) { + throw new Error( + `Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`, + ); + } + return { - link: palette.primary?.main || (isDark ? '#9cc9ff' : '#1f5493'), - 'link-hover': palette.primary?.dark || (isDark ? '#7eb5f7' : '#1f2d5c'), - disabled: palette.text?.disabled || (isDark ? '#9e9e9e' : '#9e9e9e'), + link: + palette.primary?.main || + defaultPalette.primary?.main || + (() => { + throw new Error('Primary color not found in current or default theme'); + })(), + 'link-hover': + palette.primary?.dark || + defaultPalette.primary?.dark || + (() => { + throw new Error( + 'Primary dark color not found in current or default theme', + ); + })(), + disabled: + palette.text?.disabled || + defaultPalette.text?.disabled || + (() => { + throw new Error( + 'Text disabled color not found in current or default theme', + ); + })(), solid: isDark ? '#101821' : '#ffffff', 'solid-disabled': isDark ? '#575757' : '#9c9c9c', - tint: palette.primary?.main || (isDark ? '#9cc9ff' : '#1f5493'), + tint: + palette.primary?.main || + defaultPalette.primary?.main || + (() => { + throw new Error('Primary color not found in current or default theme'); + })(), 'tint-disabled': isDark ? '#575757' : '#9e9e9e', - danger: palette.error?.main || '#e22b2b', - warning: palette.warning?.main || '#e36d05', - success: palette.success?.main || '#1db954', + danger: + palette.error?.main || + defaultPalette.error?.main || + (() => { + throw new Error('Error color not found in current or default theme'); + })(), + warning: + palette.warning?.main || + defaultPalette.warning?.main || + (() => { + throw new Error('Warning color not found in current or default theme'); + })(), + success: + palette.success?.main || + defaultPalette.success?.main || + (() => { + throw new Error('Success color not found in current or default theme'); + })(), }; } @@ -274,13 +414,47 @@ function generateBorderColors( ): Record { const isDark = palette.mode === 'dark'; + // Get the default Backstage theme for fallback values + const defaultTheme = isDark ? themes.dark : themes.light; + const defaultMuiTheme = defaultTheme.getTheme('v5'); + + if (!defaultMuiTheme) { + throw new Error( + `Failed to get MUI v5 theme from Backstage ${ + isDark ? 'dark' : 'light' + } theme`, + ); + } + + const defaultPalette = defaultMuiTheme.palette; + if (!defaultPalette) { + throw new Error( + `Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`, + ); + } + return { '': isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)', hover: isDark ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.2)', pressed: isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.4)', disabled: isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)', - danger: palette.error?.main || '#f87a7a', - warning: palette.warning?.main || '#e36d05', - success: palette.success?.main || '#53db83', + danger: + palette.error?.main || + defaultPalette.error?.main || + (() => { + throw new Error('Error color not found in current or default theme'); + })(), + warning: + palette.warning?.main || + defaultPalette.warning?.main || + (() => { + throw new Error('Warning color not found in current or default theme'); + })(), + success: + palette.success?.main || + defaultPalette.success?.main || + (() => { + throw new Error('Success color not found in current or default theme'); + })(), }; } From 71cb5327c54aae23936e404e81bb99234df684d4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 12:53:08 +0200 Subject: [PATCH 082/193] bui-themer: skip gray scale Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/convertMuiToBuiTheme.ts | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts index dee1c1fd72..dbd60c6ea6 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts @@ -152,12 +152,6 @@ function generateBuiVariables(theme: Mui5Theme): string { variables.push(` --bui-white: ${palette.common.white};`); } - // Gray scale - generate from primary color or use defaults - const grayScale = generateGrayScale(palette.mode === 'dark'); - Object.entries(grayScale).forEach(([key, value]) => { - variables.push(` --bui-gray-${key}: ${value};`); - }); - // Background colors if (palette.background?.default) { variables.push(` --bui-bg: ${palette.background.default};`); @@ -200,35 +194,6 @@ function generateBuiVariables(theme: Mui5Theme): string { return variables.join('\n'); } -/** - * Generates gray scale colors - */ -function generateGrayScale(isDark: boolean): Record { - if (isDark) { - return { - '1': '#191919', - '2': '#242424', - '3': '#373737', - '4': '#464646', - '5': '#575757', - '6': '#7b7b7b', - '7': '#9e9e9e', - '8': '#b4b4b4', - }; - } - - return { - '1': '#f8f8f8', - '2': '#ececec', - '3': '#d9d9d9', - '4': '#c1c1c1', - '5': '#9e9e9e', - '6': '#8c8c8c', - '7': '#757575', - '8': '#595959', - }; -} - /** * Generates surface background colors */ From 6c0a51b3f7606d42bf1d27eb38767233132fbd97 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 15:07:43 +0200 Subject: [PATCH 083/193] bui-themer: simplify theme conversion Signed-off-by: Patrik Oldsberg --- plugins/bui-themer/package.json | 1 + .../convertMuiToBuiTheme.test.ts | 104 +----- .../BuiThemerPage/convertMuiToBuiTheme.ts | 306 +++--------------- yarn.lock | 1 + 4 files changed, 45 insertions(+), 367 deletions(-) diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json index ff97c92ecb..64cad56775 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/bui-themer/package.json @@ -35,6 +35,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.61", "@mui/material": "^7.3.2", + "@mui/system": "^7.3.2", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts index ffbd4fc392..7c11dd7d25 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts @@ -54,7 +54,8 @@ describe('convertMuiToBuiTheme', () => { expect(result).toContain(':root {'); expect(result).toContain('--bui-font-regular: Roboto, sans-serif;'); - expect(result).toContain('--bui-space: 8px;'); + // Spacing is skipped for default 8px + expect(result).not.toContain('--bui-space:'); expect(result).toContain('--bui-radius-3: 4px;'); expect(result).toContain('--bui-bg: #f5f5f5;'); expect(result).toContain('--bui-bg-surface-1: #ffffff;'); @@ -122,38 +123,6 @@ describe('convertMuiToBuiTheme', () => { expect(result).toContain('--bui-white: #fff;'); }); - it('should generate proper gray scale for light theme', () => { - const theme = createTheme({ - palette: { - mode: 'light', - primary: { - main: '#1976d2', - }, - }, - }); - - const result = convertMuiToBuiTheme(theme); - - expect(result).toContain('--bui-gray-1: #f8f8f8;'); - expect(result).toContain('--bui-gray-8: #595959;'); - }); - - it('should generate proper gray scale for dark theme', () => { - const theme = createTheme({ - palette: { - mode: 'dark', - primary: { - main: '#90caf9', - }, - }, - }); - - const result = convertMuiToBuiTheme(theme); - - expect(result).toContain('--bui-gray-1: #191919;'); - expect(result).toContain('--bui-gray-8: #b4b4b4;'); - }); - it('should generate surface colors correctly', () => { const theme = createTheme({ palette: { @@ -180,7 +149,7 @@ describe('convertMuiToBuiTheme', () => { const result = convertMuiToBuiTheme(theme); expect(result).toContain('--bui-bg-solid: #1976d2;'); - expect(result).toContain('--bui-bg-solid-hover: #115293;'); + expect(result).toContain('--bui-bg-solid-hover: rgb(21, 100, 179);'); expect(result).toContain('--bui-bg-danger: #ffcdd2;'); expect(result).toContain('--bui-bg-warning: #ffe0b2;'); expect(result).toContain('--bui-bg-success: #c8e6c9;'); @@ -237,8 +206,7 @@ describe('convertMuiToBuiTheme', () => { const result = convertMuiToBuiTheme(theme); - expect(result).toContain('--bui-border: rgba(0, 0, 0, 0.1);'); - expect(result).toContain('--bui-border-hover: rgba(0, 0, 0, 0.2);'); + // Base border colors are no longer generated expect(result).toContain('--bui-border-danger: #f44336;'); expect(result).toContain('--bui-border-warning: #ff9800;'); expect(result).toContain('--bui-border-success: #4caf50;'); @@ -251,7 +219,7 @@ describe('convertMuiToBuiTheme', () => { const result = convertMuiToBuiTheme(theme); - expect(result).toContain('--bui-space: 4px;'); + expect(result).toContain('--bui-space: calc(4px * 0.5);'); }); it('should handle string-based spacing', () => { @@ -261,7 +229,7 @@ describe('convertMuiToBuiTheme', () => { const result = convertMuiToBuiTheme(theme); - expect(result).toContain('--bui-space: calc(1 * 8px);'); + expect(result).toContain('--bui-space: calc(calc(1 * 8px) * 0.5);'); }); it('should handle string-based border radius', () => { @@ -275,64 +243,4 @@ describe('convertMuiToBuiTheme', () => { expect(result).toContain('--bui-radius-3: 8px;'); }); - - describe('early validation', () => { - it('should throw error when theme is undefined', () => { - expect(() => convertMuiToBuiTheme(undefined as any)).toThrow( - 'Theme is required', - ); - }); - - it('should throw error when theme palette is missing', () => { - const theme = { typography: {}, spacing: () => 8, shape: {} } as any; - expect(() => convertMuiToBuiTheme(theme)).toThrow( - 'Theme palette is required', - ); - }); - - it('should throw error when theme palette mode is missing', () => { - const theme = { - palette: {}, - typography: {}, - spacing: () => 8, - shape: {}, - } as any; - expect(() => convertMuiToBuiTheme(theme)).toThrow( - 'Theme palette mode is required', - ); - }); - - it('should throw error when theme typography is missing', () => { - const theme = { - palette: { mode: 'light' }, - spacing: () => 8, - shape: {}, - } as any; - expect(() => convertMuiToBuiTheme(theme)).toThrow( - 'Theme typography is required', - ); - }); - - it('should throw error when theme spacing is missing', () => { - const theme = { - palette: { mode: 'light' }, - typography: {}, - shape: {}, - } as any; - expect(() => convertMuiToBuiTheme(theme)).toThrow( - 'Theme spacing is required', - ); - }); - - it('should throw error when theme shape is missing', () => { - const theme = { - palette: { mode: 'light' }, - typography: {}, - spacing: () => 8, - } as any; - expect(() => convertMuiToBuiTheme(theme)).toThrow( - 'Theme shape is required', - ); - }); - }); }); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts index dbd60c6ea6..1d6e581b57 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts @@ -15,7 +15,7 @@ */ import { Theme as Mui5Theme } from '@mui/material/styles'; -import { themes } from '@backstage/theme'; +import { blend, alpha } from '@mui/system/colorManipulator'; export interface ConvertMuiToBuiThemeOptions { /** @@ -38,31 +38,6 @@ export function convertMuiToBuiTheme( theme: Mui5Theme, options: ConvertMuiToBuiThemeOptions = {}, ): string { - // Early validation of required theme properties - if (!theme) { - throw new Error('Theme is required'); - } - - if (!theme.palette) { - throw new Error('Theme palette is required'); - } - - if (!theme.palette.mode) { - throw new Error('Theme palette mode is required'); - } - - if (!theme.typography) { - throw new Error('Theme typography is required'); - } - - if (!theme.spacing) { - throw new Error('Theme spacing is required'); - } - - if (!theme.shape) { - throw new Error('Theme shape is required'); - } - const { themeId, includeThemeId = false } = options; const isDark = theme.palette.mode === 'dark'; @@ -125,13 +100,10 @@ function generateBuiVariables(theme: Mui5Theme): string { } }); - // Spacing - map MUI spacing to BUI spacing scale - if (theme.spacing) { - const spacingUnit = - typeof theme.spacing === 'function' ? theme.spacing(1) : theme.spacing; - const spacingValue = - typeof spacingUnit === 'number' ? `${spacingUnit}px` : spacingUnit; - variables.push(` --bui-space: ${spacingValue};`); + const spacing = theme.spacing(1); + // Skip spacing if the theme is using the default + if (spacing !== '8px') { + variables.push(` --bui-space: calc(${spacing} * 0.5);`); } // Border radius @@ -161,8 +133,21 @@ function generateBuiVariables(theme: Mui5Theme): string { } // Generate surface colors - const surfaceColors = generateSurfaceColors(palette); - Object.entries(surfaceColors).forEach(([key, value]) => { + Object.entries({ + 'surface-1': palette.background.default, + 'surface-2': palette.background.paper, + solid: palette.primary.main, + 'solid-hover': blend(palette.primary.main, palette.primary.dark, 0.5), + 'solid-pressed': palette.primary.dark, + 'solid-disabled': palette.action.disabledBackground, + tint: 'transparent', + 'tint-hover': alpha(palette.primary.main, 0.4), + 'tint-pressed': alpha(palette.primary.main, 0.6), + 'tint-disabled': palette.action.disabledBackground, + danger: palette.error.light, + warning: palette.warning.light, + success: palette.success.light, + }).forEach(([key, value]) => { variables.push(` --bui-bg-${key}: ${value};`); }); @@ -175,14 +160,27 @@ function generateBuiVariables(theme: Mui5Theme): string { } // Generate foreground colors - const foregroundColors = generateForegroundColors(palette); - Object.entries(foregroundColors).forEach(([key, value]) => { + Object.entries({ + link: palette.primary.main, + 'link-hover': palette.primary.dark, + disabled: palette.text.disabled, + solid: palette.text.primary, + 'solid-disabled': palette.action.disabled, + tint: palette.primary.main, + 'tint-disabled': palette.action.disabled, + danger: palette.error.main, + warning: palette.warning.main, + success: palette.success.main, + }).forEach(([key, value]) => { variables.push(` --bui-fg-${key}: ${value};`); }); // Border colors - const borderColors = generateBorderColors(palette); - Object.entries(borderColors).forEach(([key, value]) => { + Object.entries({ + danger: palette.error.main, + warning: palette.warning.main, + success: palette.success.main, + }).forEach(([key, value]) => { variables.push(` --bui-border${key ? `-${key}` : ''}: ${value};`); }); @@ -193,233 +191,3 @@ function generateBuiVariables(theme: Mui5Theme): string { return variables.join('\n'); } - -/** - * Generates surface background colors - */ -function generateSurfaceColors( - palette: Mui5Theme['palette'], -): Record { - const isDark = palette.mode === 'dark'; - - // Get the default Backstage theme for fallback values - const defaultTheme = isDark ? themes.dark : themes.light; - const defaultMuiTheme = defaultTheme.getTheme('v5'); - - if (!defaultMuiTheme) { - throw new Error( - `Failed to get MUI v5 theme from Backstage ${ - isDark ? 'dark' : 'light' - } theme`, - ); - } - - const defaultPalette = defaultMuiTheme.palette; - if (!defaultPalette) { - throw new Error( - `Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`, - ); - } - - // Helper function to get tint colors - const getTintColor = (opacity: string) => { - const primaryColor = palette.primary?.main || defaultPalette.primary?.main; - if (!primaryColor) { - throw new Error('Primary color not found in current or default theme'); - } - return `${primaryColor}${opacity}`; - }; - - // Helper function to get colors based on theme mode - const getThemeColor = (lightColor: string, darkColor: string) => { - return isDark ? darkColor : lightColor; - }; - - return { - 'surface-2': getThemeColor('#ececec', '#242424'), - solid: - palette.primary?.main || - defaultPalette.primary?.main || - (() => { - throw new Error('Primary color not found in current or default theme'); - })(), - 'solid-hover': - palette.primary?.dark || - defaultPalette.primary?.dark || - (() => { - throw new Error( - 'Primary dark color not found in current or default theme', - ); - })(), - 'solid-pressed': - palette.primary?.dark || - defaultPalette.primary?.dark || - (() => { - throw new Error( - 'Primary dark color not found in current or default theme', - ); - })(), - 'solid-disabled': getThemeColor('#ebebeb', '#222222'), - tint: 'transparent', - 'tint-hover': getTintColor('40'), - 'tint-pressed': getTintColor('60'), - 'tint-disabled': getThemeColor('#ebebeb', 'transparent'), - danger: - palette.error?.light || - defaultPalette.error?.light || - (() => { - throw new Error( - 'Error light color not found in current or default theme', - ); - })(), - warning: - palette.warning?.light || - defaultPalette.warning?.light || - (() => { - throw new Error( - 'Warning light color not found in current or default theme', - ); - })(), - success: - palette.success?.light || - defaultPalette.success?.light || - (() => { - throw new Error( - 'Success light color not found in current or default theme', - ); - })(), - }; -} - -/** - * Generates foreground colors - */ -function generateForegroundColors( - palette: Mui5Theme['palette'], -): Record { - const isDark = palette.mode === 'dark'; - - // Get the default Backstage theme for fallback values - const defaultTheme = isDark ? themes.dark : themes.light; - const defaultMuiTheme = defaultTheme.getTheme('v5'); - - if (!defaultMuiTheme) { - throw new Error( - `Failed to get MUI v5 theme from Backstage ${ - isDark ? 'dark' : 'light' - } theme`, - ); - } - - const defaultPalette = defaultMuiTheme.palette; - if (!defaultPalette) { - throw new Error( - `Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`, - ); - } - - return { - link: - palette.primary?.main || - defaultPalette.primary?.main || - (() => { - throw new Error('Primary color not found in current or default theme'); - })(), - 'link-hover': - palette.primary?.dark || - defaultPalette.primary?.dark || - (() => { - throw new Error( - 'Primary dark color not found in current or default theme', - ); - })(), - disabled: - palette.text?.disabled || - defaultPalette.text?.disabled || - (() => { - throw new Error( - 'Text disabled color not found in current or default theme', - ); - })(), - solid: isDark ? '#101821' : '#ffffff', - 'solid-disabled': isDark ? '#575757' : '#9c9c9c', - tint: - palette.primary?.main || - defaultPalette.primary?.main || - (() => { - throw new Error('Primary color not found in current or default theme'); - })(), - 'tint-disabled': isDark ? '#575757' : '#9e9e9e', - danger: - palette.error?.main || - defaultPalette.error?.main || - (() => { - throw new Error('Error color not found in current or default theme'); - })(), - warning: - palette.warning?.main || - defaultPalette.warning?.main || - (() => { - throw new Error('Warning color not found in current or default theme'); - })(), - success: - palette.success?.main || - defaultPalette.success?.main || - (() => { - throw new Error('Success color not found in current or default theme'); - })(), - }; -} - -/** - * Generates border colors - */ -function generateBorderColors( - palette: Mui5Theme['palette'], -): Record { - const isDark = palette.mode === 'dark'; - - // Get the default Backstage theme for fallback values - const defaultTheme = isDark ? themes.dark : themes.light; - const defaultMuiTheme = defaultTheme.getTheme('v5'); - - if (!defaultMuiTheme) { - throw new Error( - `Failed to get MUI v5 theme from Backstage ${ - isDark ? 'dark' : 'light' - } theme`, - ); - } - - const defaultPalette = defaultMuiTheme.palette; - if (!defaultPalette) { - throw new Error( - `Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`, - ); - } - - return { - '': isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)', - hover: isDark ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.2)', - pressed: isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.4)', - disabled: isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)', - danger: - palette.error?.main || - defaultPalette.error?.main || - (() => { - throw new Error('Error color not found in current or default theme'); - })(), - warning: - palette.warning?.main || - defaultPalette.warning?.main || - (() => { - throw new Error('Warning color not found in current or default theme'); - })(), - success: - palette.success?.main || - defaultPalette.success?.main || - (() => { - throw new Error('Success color not found in current or default theme'); - })(), - }; -} diff --git a/yarn.lock b/yarn.lock index 41f7dc6d64..9dd2bd9e16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4348,6 +4348,7 @@ __metadata: "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:^4.0.0-alpha.61" "@mui/material": "npm:^7.3.2" + "@mui/system": "npm:^7.3.2" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^14.0.0" "@testing-library/user-event": "npm:^14.0.0" From 09e65c6266b4458bb40f303e919de87dbe44f01f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 15:43:15 +0200 Subject: [PATCH 084/193] bui-themer: switch to horizontal layout Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/BuiThemerPage.tsx | 116 ++++++++++-------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index b36c550081..4ae25b60ce 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -24,10 +24,8 @@ import { Card, Container, Flex, - Grid, HeaderPage, Text, - TextField, Switch, } from '@backstage/ui'; import { @@ -136,16 +134,16 @@ function ThemeContent({ themeId, themeTitle, variant }: ThemeContentProps) { return ( - - + + - {themeTitle} + {themeTitle} {variant} theme - + - + @@ -172,53 +170,70 @@ function ThemeContent({ themeId, themeTitle, variant }: ThemeContentProps) { Generated CSS: - - - - {isPreviewMode && ( - + {generatedCss} + + + + {isPreviewMode && ( + + Live Preview: - Solid Button - - - Tint Button - - - Surface Card + + + Solid Button + + + Tint Button + + + Surface Card + + )} @@ -270,18 +285,17 @@ export function BuiThemerPage() { ) : ( - + {installedThemes.map(theme => ( - - - + ))} - + )} From d5c170eaf48e916ec95c4ef71ce30d332ee40458 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 16:32:07 +0200 Subject: [PATCH 085/193] bui-themer: extract mui theme and then unmount theme provider Signed-off-by: Patrik Oldsberg --- .../src/unified/UnifiedThemeProvider.tsx | 14 +++- .../BuiThemerPage/BuiThemerPage.tsx | 66 +++++++++++-------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx index a80e36cf35..006c45605e 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx @@ -74,12 +74,22 @@ export function UnifiedThemeProvider( const themeName = 'backstage'; useEffect(() => { + const oldMode = document.body.getAttribute('data-theme-mode'); + const oldName = document.body.getAttribute('data-theme-name'); document.body.setAttribute('data-theme-mode', themeMode); document.body.setAttribute('data-theme-name', themeName); return () => { - document.body.removeAttribute('data-theme-mode'); - document.body.removeAttribute('data-theme-name'); + if (oldMode) { + document.body.setAttribute('data-theme-mode', oldMode); + } else { + document.body.removeAttribute('data-theme-mode'); + } + if (oldName) { + document.body.setAttribute('data-theme-name', oldName); + } else { + document.body.removeAttribute('data-theme-name'); + } }; }, [themeMode, themeName]); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 4ae25b60ce..4df3aae006 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -15,9 +15,9 @@ */ import { useMemo, useState, useCallback, useEffect } from 'react'; -import { useApi } from '@backstage/core-plugin-api'; +import { AppTheme, useApi } from '@backstage/core-plugin-api'; import { appThemeApiRef } from '@backstage/core-plugin-api'; -import { useTheme } from '@mui/material/styles'; +import { Theme, useTheme } from '@mui/material/styles'; import { Box, Button, @@ -33,13 +33,6 @@ import { ConvertMuiToBuiThemeOptions, } from './convertMuiToBuiTheme'; -interface ThemePreviewProps { - themeId: string; - themeTitle: string; - variant: 'light' | 'dark'; - Provider: React.ComponentType<{ children: React.ReactNode }>; -} - // Memoization cache for generated CSS const cssCache = new Map(); @@ -47,13 +40,18 @@ interface ThemeContentProps { themeId: string; themeTitle: string; variant: 'light' | 'dark'; + muiTheme: Theme; } -function ThemeContent({ themeId, themeTitle, variant }: ThemeContentProps) { +function ThemeContent({ + themeId, + themeTitle, + variant, + muiTheme, +}: ThemeContentProps) { const [generatedCss, setGeneratedCss] = useState(''); const [isPreviewMode, setIsPreviewMode] = useState(false); const [includeThemeId, setIncludeThemeId] = useState(false); - const muiTheme = useTheme(); const css = useMemo(() => { // Create cache key based on theme properties and options @@ -243,23 +241,29 @@ function ThemeContent({ themeId, themeTitle, variant }: ThemeContentProps) { ); } -function ThemePreview({ - themeId, - themeTitle, - variant, - Provider, -}: ThemePreviewProps) { +function MuiThemeExtractor(props: { + appTheme: AppTheme; + children: (theme: Theme) => JSX.Element; +}): JSX.Element { + const { appTheme, children } = props; + const [theme, setTheme] = useState(null); + const { Provider } = appTheme; + if (theme) { + return children(theme); + } + return ( - + ); } +function MuiThemeExtractorInner(props: { setTheme: (theme: Theme) => void }) { + props.setTheme(useTheme()); + return null; +} + export function BuiThemerPage() { const appThemeApi = useApi(appThemeApiRef); const installedThemes = appThemeApi.getInstalledThemes(); @@ -287,13 +291,17 @@ export function BuiThemerPage() { ) : ( {installedThemes.map(theme => ( - + + {muiTheme => ( + + )} + ))} )} From 5161dbc757b6ef3c59f9420cef27321d74b9dab2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 16:37:44 +0200 Subject: [PATCH 086/193] bui-themer: remove unnecessary css cache Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/BuiThemerPage.tsx | 39 ++----------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 4df3aae006..061ea379cd 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -28,13 +28,7 @@ import { Text, Switch, } from '@backstage/ui'; -import { - convertMuiToBuiTheme, - ConvertMuiToBuiThemeOptions, -} from './convertMuiToBuiTheme'; - -// Memoization cache for generated CSS -const cssCache = new Map(); +import { convertMuiToBuiTheme } from './convertMuiToBuiTheme'; interface ThemeContentProps { themeId: string; @@ -54,37 +48,10 @@ function ThemeContent({ const [includeThemeId, setIncludeThemeId] = useState(false); const css = useMemo(() => { - // Create cache key based on theme properties and options - const cacheKey = `${themeId}-${includeThemeId}-${JSON.stringify({ - palette: muiTheme.palette, - typography: muiTheme.typography, - spacing: muiTheme.spacing, - shape: muiTheme.shape, - })}`; - - // Check cache first - if (cssCache.has(cacheKey)) { - return cssCache.get(cacheKey)!; - } - - const options: ConvertMuiToBuiThemeOptions = { + return convertMuiToBuiTheme(muiTheme, { themeId, includeThemeId, - }; - const result = convertMuiToBuiTheme(muiTheme, options); - - // Cache the result - cssCache.set(cacheKey, result); - - // Clean up old cache entries (keep only last 50) - if (cssCache.size > 50) { - const firstKey = cssCache.keys().next().value; - if (firstKey) { - cssCache.delete(firstKey); - } - } - - return result; + }); }, [muiTheme, themeId, includeThemeId]); useEffect(() => { From 8ceba35b280d7daac1ad66ae9fab1b954f175998 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 16:42:36 +0200 Subject: [PATCH 087/193] bui-themer: switch theme cards to use tabs Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/BuiThemerPage.tsx | 184 +++++++++++------- 1 file changed, 109 insertions(+), 75 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 061ea379cd..90e7d21902 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -27,6 +27,10 @@ import { HeaderPage, Text, Switch, + Tabs, + TabList, + Tab, + TabPanel, } from '@backstage/ui'; import { convertMuiToBuiTheme } from './convertMuiToBuiTheme'; @@ -46,6 +50,7 @@ function ThemeContent({ const [generatedCss, setGeneratedCss] = useState(''); const [isPreviewMode, setIsPreviewMode] = useState(false); const [includeThemeId, setIncludeThemeId] = useState(false); + const [activeTab, setActiveTab] = useState('css'); const css = useMemo(() => { return convertMuiToBuiTheme(muiTheme, { @@ -123,85 +128,114 @@ function ThemeContent({ - - - - Generated CSS: - - - {generatedCss} - - + setActiveTab(key as string)} + > + + Generated CSS + Live Preview + - {isPreviewMode && ( - - - Live Preview: - - - - - Solid Button - - - Tint Button - - - Surface Card - - + + + + Generated CSS: + + + {generatedCss} + - - )} + + + + + + + + + {isPreviewMode ? ( + + + Live Preview: + + + + + Solid Button + + + Tint Button + + + Surface Card + + + + + ) : ( + + + Click "Start Preview" to see the theme applied to sample + components. + + + )} + + + From 3cb8a3aa88f004975f75d6f5a808425860dd92c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 17:19:13 +0200 Subject: [PATCH 088/193] bui-themer: new and improved preview Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/BuiThemerPage.tsx | 227 ++++++++++-------- 1 file changed, 133 insertions(+), 94 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 90e7d21902..a65c35d665 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -31,6 +31,13 @@ import { TabList, Tab, TabPanel, + TextField, + Select, + Checkbox, + Radio, + RadioGroup, + CardHeader, + CardBody, } from '@backstage/ui'; import { convertMuiToBuiTheme } from './convertMuiToBuiTheme'; @@ -41,6 +48,128 @@ interface ThemeContentProps { muiTheme: Theme; } +interface IsolatedPreviewProps { + mode: 'light' | 'dark'; + css: string; +} + +function BuiThemePreview({ mode, css }: IsolatedPreviewProps) { + return ( +
line.trim().startsWith('--bui-')) + .map(line => { + const [key, value] = line.trim().split(':'); + return [key?.trim(), value?.replace(';', '').trim()]; + }) + .filter(([key, value]) => key && value), + ), + width: '100%', + backgroundColor: 'var(--bui-bg-surface-2)', + padding: 'var(--bui-space-3)', + borderRadius: 'var(--bui-radius-2)', + }} + > + + + Theme Preview + + + This preview shows how your theme will look with various Backstage + UI components + + + + + + Button Variants + + + + + + + + + + + Form Inputs + + + + + + + + Option 1 + + Option 2 + + Option 3 + + + + + + + Surface Variations + + + + Surface 1 + + + Surface 2 + + + Solid + + + + + +
+ ); +} diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx index 856790161b..bb3e797bb1 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -14,264 +14,11 @@ * limitations under the License. */ -import { useMemo, useState, useCallback } from 'react'; -import { AppTheme, useApi } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { appThemeApiRef } from '@backstage/core-plugin-api'; -import { Theme, useTheme } from '@mui/material/styles'; -import { - Box, - Button, - Card, - Container, - Flex, - HeaderPage, - Text, - Tabs, - TabList, - Tab, - TabPanel, - TextField, - Select, - Checkbox, - Radio, - RadioGroup, - CardHeader, - CardBody, -} from '@backstage/ui'; -import { convertMuiToBuiTheme } from './convertMuiToBuiTheme'; - -interface ThemeContentProps { - themeId: string; - themeTitle: string; - variant: 'light' | 'dark'; - muiTheme: Theme; -} - -interface IsolatedPreviewProps { - mode: 'light' | 'dark'; - styleObject: Record; -} - -function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) { - return ( -
- - - Theme Preview - - - This preview shows how your theme will look with various Backstage - UI components - - - - - - Button Variants - - - - - - - - - - - Form Inputs - - - - +
+ + + ); + }, +); + +PasswordField.displayName = 'PasswordField'; diff --git a/packages/ui/src/components/PasswordField/index.ts b/packages/ui/src/components/PasswordField/index.ts new file mode 100644 index 0000000000..df7874e1c2 --- /dev/null +++ b/packages/ui/src/components/PasswordField/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 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 * from './PasswordField'; +export * from './types'; diff --git a/packages/ui/src/components/PasswordField/types.ts b/packages/ui/src/components/PasswordField/types.ts new file mode 100644 index 0000000000..f1f5066955 --- /dev/null +++ b/packages/ui/src/components/PasswordField/types.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TextFieldProps as AriaTextFieldProps } from 'react-aria-components'; +import { ReactNode } from 'react'; +import type { Breakpoint } from '../../types'; +import type { FieldLabelProps } from '../FieldLabel/types'; + +/** @public */ +export interface PasswordFieldProps + extends AriaTextFieldProps, + Omit { + /** + * An icon to render before the input + */ + icon?: ReactNode; + + /** + * The size of the password field + * @defaultValue 'medium' + */ + size?: 'small' | 'medium' | Partial>; + + /** + * Text to display in the input when it has no value + */ + placeholder?: string; + + /** + * A tertiary label to display on the right side of the field label + */ + tertiaryLabel?: string; + + /** + * The visual tone of the message below the field, if any + */ + tone?: 'positive' | 'critical' | 'neutral' | 'caution'; + + /** + * A message to display below the field, such as validation feedback + */ + message?: string; +} diff --git a/packages/ui/src/components/TextField/TextField.styles.css b/packages/ui/src/components/TextField/TextField.styles.css index c5765bf8ac..792d5d85c9 100644 --- a/packages/ui/src/components/TextField/TextField.styles.css +++ b/packages/ui/src/components/TextField/TextField.styles.css @@ -116,26 +116,3 @@ border: 1px solid var(--bui-border-disabled); } } - -.bui-InputAction { - position: absolute; - right: var(--bui-space-1); - top: 50%; - transform: translateY(-50%); - color: var(--bui-fg-primary); - flex-shrink: 0; - /* To animate the icon when the input is collapsed */ - transition: right 0.2s ease-in-out; - - &[data-size='small'], - &[data-size='small'] svg { - width: 1rem; - height: 1rem; - } - - &[data-size='medium'], - &[data-size='medium'] svg { - width: 1.25rem; - height: 1.25rem; - } -} diff --git a/packages/ui/src/components/TextField/TextField.tsx b/packages/ui/src/components/TextField/TextField.tsx index c4fc81f029..2756c3c85f 100644 --- a/packages/ui/src/components/TextField/TextField.tsx +++ b/packages/ui/src/components/TextField/TextField.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { forwardRef, useEffect, useState } from 'react'; +import { forwardRef, useEffect } from 'react'; import { Input, TextField as AriaTextField } from 'react-aria-components'; import clsx from 'clsx'; import { FieldLabel } from '../FieldLabel'; @@ -22,8 +22,6 @@ import { FieldError } from '../FieldError'; import type { TextFieldProps } from './types'; import { useStyles } from '../../hooks/useStyles'; -import { Icon } from '../Icon'; -import { ButtonIcon } from '../ButtonIcon'; /** @public */ export const TextField = forwardRef( @@ -36,7 +34,6 @@ export const TextField = forwardRef( secondaryLabel, description, isRequired, - enableVisibility, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, placeholder, @@ -59,10 +56,6 @@ export const TextField = forwardRef( const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null); - // Manage secret visibility toggle - const [isVisible, setIsVisible] = useState(false); - const showSecretToggle = Boolean(enableVisibility); - return ( ( {icon} )} - {showSecretToggle && ( -
- setIsVisible(v => !v)} - icon={} - /> -
- )} diff --git a/packages/ui/src/components/TextField/types.ts b/packages/ui/src/components/TextField/types.ts index d64be41050..75a5c68aaa 100644 --- a/packages/ui/src/components/TextField/types.ts +++ b/packages/ui/src/components/TextField/types.ts @@ -38,14 +38,4 @@ export interface TextFieldProps * Text to display in the input when it has no value */ placeholder?: string; - - /** - * Displays icon to toggle between showing and hiding the input value - */ - enableVisibility?: boolean; - - /** - * Displays a clear button when there is content in the input - */ - isClearable?: boolean; } diff --git a/packages/ui/src/css/components.css b/packages/ui/src/css/components.css index 8aa81a8217..97dec3603f 100644 --- a/packages/ui/src/css/components.css +++ b/packages/ui/src/css/components.css @@ -39,6 +39,7 @@ @import '../components/TagGroup/TagGroup.styles.css'; @import '../components/Text/styles.css'; @import '../components/TextField/TextField.styles.css'; +@import '../components/TextField/PasswordField.styles.css'; @import '../components/SearchField/SearchField.styles.css'; @import '../components/Skeleton/Skeleton.styles.css'; @import '../components/Tooltip/Tooltip.styles.css'; diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index bf01a0dc63..f67ee781dc 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -293,6 +293,19 @@ export const componentDefinitions = { disabled: [true, false] as const, }, }, + PasswordField: { + classNames: { + root: 'bui-PasswordField', + inputWrapper: 'bui-InputWrapper', + input: 'bui-Input', + inputIcon: 'bui-InputIcon', + inputAction: 'bui-InputAction', + }, + dataAttributes: { + invalid: [true, false] as const, + disabled: [true, false] as const, + }, + }, Tooltip: { classNames: { tooltip: 'bui-Tooltip', From bd68474c8014fe479aca081829e5f1dd4ba23241 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Thu, 25 Sep 2025 10:46:15 +0100 Subject: [PATCH 109/193] Deprecate types search and password from TextField Signed-off-by: Hermione Bird --- .../ui/src/components/TextField/TextField.tsx | 14 ++++++++++++++ packages/ui/src/components/TextField/types.ts | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/ui/src/components/TextField/TextField.tsx b/packages/ui/src/components/TextField/TextField.tsx index 2756c3c85f..f44295e247 100644 --- a/packages/ui/src/components/TextField/TextField.tsx +++ b/packages/ui/src/components/TextField/TextField.tsx @@ -48,6 +48,20 @@ export const TextField = forwardRef( } }, [label, ariaLabel, ariaLabelledBy]); + useEffect(() => { + const inputType = (props as { type?: string }).type; + if (inputType === 'search') { + console.warn( + 'TextField type="search" is deprecated. Use SearchField instead.', + ); + } + if (inputType === 'password') { + console.warn( + 'TextField type="password" is deprecated. Use PasswordField instead.', + ); + } + }, [props]); + const { classNames, dataAttributes } = useStyles('TextField', { size, }); diff --git a/packages/ui/src/components/TextField/types.ts b/packages/ui/src/components/TextField/types.ts index 75a5c68aaa..0c34634e2d 100644 --- a/packages/ui/src/components/TextField/types.ts +++ b/packages/ui/src/components/TextField/types.ts @@ -23,6 +23,22 @@ import type { FieldLabelProps } from '../FieldLabel/types'; export interface TextFieldProps extends AriaTextFieldProps, Omit { + /** + * The HTML input type for the text field + * + * @remarks + * The values 'search' and 'password' are deprecated. Use `SearchField` for + * search inputs and `PasswordField` for password inputs. + */ + type?: + | 'text' + | 'email' + | 'tel' + | 'url' + /** @deprecated Use `SearchField` instead */ + | 'search' + /** @deprecated Use `PasswordField` instead */ + | 'password'; /** * An icon to render before the input */ From ed5829bc30b8867e7aab4e2b8c3a0338d3ab7c22 Mon Sep 17 00:00:00 2001 From: birdhb Date: Thu, 25 Sep 2025 11:09:44 +0100 Subject: [PATCH 110/193] Update eighty-cows-care.md Signed-off-by: birdhb --- .changeset/eighty-cows-care.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eighty-cows-care.md b/.changeset/eighty-cows-care.md index f3eed821ec..8fd5c0adb0 100644 --- a/.changeset/eighty-cows-care.md +++ b/.changeset/eighty-cows-care.md @@ -2,4 +2,4 @@ '@backstage/ui': patch --- -Added PasswordField to BUI with default show/hide visibilty on text input +Added PasswordField to BUI with default show/hide visibility on text input From 6d67d79072a40e3baac569318ff0e024ba5a5621 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Thu, 25 Sep 2025 11:16:49 +0100 Subject: [PATCH 111/193] Fix stylesheet import Signed-off-by: Hermione Bird --- packages/ui/src/css/components.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/css/components.css b/packages/ui/src/css/components.css index 97dec3603f..9696425431 100644 --- a/packages/ui/src/css/components.css +++ b/packages/ui/src/css/components.css @@ -39,7 +39,7 @@ @import '../components/TagGroup/TagGroup.styles.css'; @import '../components/Text/styles.css'; @import '../components/TextField/TextField.styles.css'; -@import '../components/TextField/PasswordField.styles.css'; +@import '../components/PasswordField/PasswordField.styles.css'; @import '../components/SearchField/SearchField.styles.css'; @import '../components/Skeleton/Skeleton.styles.css'; @import '../components/Tooltip/Tooltip.styles.css'; From 7f436b46e1aa3c8c95e91d9fd84df0f7c431cf19 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Thu, 25 Sep 2025 11:22:00 +0100 Subject: [PATCH 112/193] remove unecessary props Signed-off-by: Hermione Bird --- .../components/PasswordField/PasswordField.tsx | 3 --- packages/ui/src/components/PasswordField/types.ts | 15 --------------- 2 files changed, 18 deletions(-) diff --git a/packages/ui/src/components/PasswordField/PasswordField.tsx b/packages/ui/src/components/PasswordField/PasswordField.tsx index a43f30e41e..46b907c010 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.tsx +++ b/packages/ui/src/components/PasswordField/PasswordField.tsx @@ -34,11 +34,8 @@ export const PasswordField = forwardRef( size = 'small', label, secondaryLabel, - tertiaryLabel, description, isRequired, - tone, - message, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, placeholder, diff --git a/packages/ui/src/components/PasswordField/types.ts b/packages/ui/src/components/PasswordField/types.ts index f1f5066955..8af9a6134c 100644 --- a/packages/ui/src/components/PasswordField/types.ts +++ b/packages/ui/src/components/PasswordField/types.ts @@ -38,19 +38,4 @@ export interface PasswordFieldProps * Text to display in the input when it has no value */ placeholder?: string; - - /** - * A tertiary label to display on the right side of the field label - */ - tertiaryLabel?: string; - - /** - * The visual tone of the message below the field, if any - */ - tone?: 'positive' | 'critical' | 'neutral' | 'caution'; - - /** - * A message to display below the field, such as validation feedback - */ - message?: string; } From 0447b8d496c89647c7b39899c0db500f75426a1a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Sep 2025 11:36:20 +0200 Subject: [PATCH 113/193] plugins/bui-themer: rename to mui-to-bui Signed-off-by: Patrik Oldsberg --- .../bui-themer-bui-palette-additions.md | 6 +- packages/app/package.json | 2 +- packages/app/src/App.tsx | 4 +- plugins/bui-themer/README.md | 18 +++--- plugins/bui-themer/catalog-info.yaml | 4 +- plugins/bui-themer/dev/index.tsx | 2 +- plugins/bui-themer/package.json | 6 +- plugins/bui-themer/report.api.md | 4 +- plugins/bui-themer/src/plugin.test.ts | 2 +- plugins/bui-themer/src/plugin.tsx | 6 +- plugins/bui-themer/src/routes.ts | 2 +- yarn.lock | 64 +++++++++---------- 12 files changed, 59 insertions(+), 61 deletions(-) diff --git a/.changeset/bui-themer-bui-palette-additions.md b/.changeset/bui-themer-bui-palette-additions.md index efd6d4a139..645641b787 100644 --- a/.changeset/bui-themer-bui-palette-additions.md +++ b/.changeset/bui-themer-bui-palette-additions.md @@ -1,7 +1,5 @@ --- -'@backstage/plugin-bui-themer': minor +'@backstage/plugin-mui-to-bui': minor --- -Introduce the Backstage UI Themer plugin. It adds a new page at `/bui-themer` -that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, -with live preview and copy/download. +This is the first release of the Material UI to Backstage UI migration helper plugin. It adds a new page at `/mui-to-bui` that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, with live preview and copy/download. diff --git a/packages/app/package.json b/packages/app/package.json index 56e619e65a..1623ca09e5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -46,7 +46,6 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-api-docs": "workspace:^", "@backstage/plugin-auth-react": "workspace:^", - "@backstage/plugin-bui-themer": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-graph": "workspace:^", @@ -57,6 +56,7 @@ "@backstage/plugin-home": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-kubernetes-cluster": "workspace:^", + "@backstage/plugin-mui-to-bui": "workspace:^", "@backstage/plugin-notifications": "workspace:^", "@backstage/plugin-org": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index bd64dc504f..45f91ce8a9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -73,7 +73,7 @@ import { } from '@backstage/plugin-notifications'; import { CustomizableHomePage } from './components/home/CustomizableHomePage'; import { HomePage } from './components/home/HomePage'; -import { BuiThemerPage } from '@backstage/plugin-bui-themer'; +import { BuiThemerPage } from '@backstage/plugin-mui-to-bui'; const app = createApp({ apis, @@ -209,7 +209,7 @@ const routes = ( {customDevToolsPage} } /> - } /> + } /> ); diff --git a/plugins/bui-themer/README.md b/plugins/bui-themer/README.md index 6efc45db52..4e11db20b6 100644 --- a/plugins/bui-themer/README.md +++ b/plugins/bui-themer/README.md @@ -1,4 +1,4 @@ -# bui-themer +# mui-to-bui ## Description @@ -11,7 +11,7 @@ The Backstage UI Themer helps you convert an existing MUI v5 theme into Backstag Run this from your Backstage repo root: ```bash -yarn add --cwd packages/app @backstage/plugin-bui-themer +yarn add --cwd packages/app @backstage/plugin-mui-to-bui ``` ### 2) Wire it up depending on your frontend system @@ -25,27 +25,27 @@ Add a route for the page in your app: import React from 'react'; import { Route } from 'react-router-dom'; import { FlatRoutes } from '@backstage/core-app-api'; -import { BuiThemerPage } from '@backstage/plugin-bui-themer'; +import { BuiThemerPage } from '@backstage/plugin-mui-to-bui'; export const App = () => ( {/* ...your other routes */} - } /> + } /> ); ``` #### New frontend system -If package discovery is enabled in your app, this plugin is picked up automatically after installation — no code changes required. Just navigate to `/bui-themer`. +If package discovery is enabled in your app, this plugin is picked up automatically after installation — no code changes required. Just navigate to `/mui-to-bui`. -If you prefer explicit registration (or don't use discovery), register the plugin as a feature. The page route (`/bui-themer`) is provided by the plugin. +If you prefer explicit registration (or don't use discovery), register the plugin as a feature. The page route (`/mui-to-bui`) is provided by the plugin. ```tsx // packages/app/src/App.tsx (or your app entry where you call createApp) import React from 'react'; import { createApp } from '@backstage/frontend-defaults'; -import buiThemerPlugin from '@backstage/plugin-bui-themer'; +import buiThemerPlugin from '@backstage/plugin-mui-to-bui'; const app = createApp({ features: [ @@ -59,5 +59,5 @@ export default app.createRoot(); ## Accessing the Themer page -- Navigate to `/bui-themer` in your Backstage app (for example `http://localhost:3000/bui-themer`). -- Optional: Add a sidebar/link in your app that points to `/bui-themer` if you want a permanent navigation entry. +- Navigate to `/mui-to-bui` in your Backstage app (for example `http://localhost:3000/mui-to-bui`). +- Optional: Add a sidebar/link in your app that points to `/mui-to-bui` if you want a permanent navigation entry. diff --git a/plugins/bui-themer/catalog-info.yaml b/plugins/bui-themer/catalog-info.yaml index 15e907a0ac..9e3605267d 100644 --- a/plugins/bui-themer/catalog-info.yaml +++ b/plugins/bui-themer/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: backstage-plugin-bui-themer - title: '@backstage/plugin-bui-themer' + name: backstage-plugin-mui-to-bui + title: '@backstage/plugin-mui-to-bui' spec: lifecycle: experimental type: backstage-frontend-plugin diff --git a/plugins/bui-themer/dev/index.tsx b/plugins/bui-themer/dev/index.tsx index ce3986f7da..8837d540c5 100644 --- a/plugins/bui-themer/dev/index.tsx +++ b/plugins/bui-themer/dev/index.tsx @@ -21,6 +21,6 @@ createDevApp() .addPage({ element: , title: 'Root Page', - path: '/bui-themer', + path: '/mui-to-bui', }) .render(); diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json index 7f8f12f464..a04bfbf474 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/bui-themer/package.json @@ -1,11 +1,11 @@ { - "name": "@backstage/plugin-bui-themer", + "name": "@backstage/plugin-mui-to-bui", "version": "0.1.0", "backstage": { "role": "frontend-plugin", - "pluginId": "bui-themer", + "pluginId": "mui-to-bui", "pluginPackages": [ - "@backstage/plugin-bui-themer" + "@backstage/plugin-mui-to-bui" ] }, "publishConfig": { diff --git a/plugins/bui-themer/report.api.md b/plugins/bui-themer/report.api.md index c02fa1298d..593130ea86 100644 --- a/plugins/bui-themer/report.api.md +++ b/plugins/bui-themer/report.api.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-bui-themer" +## API Report File for "@backstage/plugin-mui-to-bui" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). @@ -31,7 +31,7 @@ const _default: OverridableFrontendPlugin< }, {}, { - 'page:bui-themer': ExtensionDefinition<{ + 'page:mui-to-bui': ExtensionDefinition<{ kind: 'page'; name: undefined; config: { diff --git a/plugins/bui-themer/src/plugin.test.ts b/plugins/bui-themer/src/plugin.test.ts index b19adaf007..0d742fa02a 100644 --- a/plugins/bui-themer/src/plugin.test.ts +++ b/plugins/bui-themer/src/plugin.test.ts @@ -15,7 +15,7 @@ */ import { buiThemerPlugin } from './plugin'; -describe('bui-themer', () => { +describe('mui-to-bui', () => { it('should export plugin', () => { expect(buiThemerPlugin).toBeDefined(); }); diff --git a/plugins/bui-themer/src/plugin.tsx b/plugins/bui-themer/src/plugin.tsx index d485d8df0f..aca19c9978 100644 --- a/plugins/bui-themer/src/plugin.tsx +++ b/plugins/bui-themer/src/plugin.tsx @@ -31,7 +31,7 @@ import { rootRouteRef } from './routes'; // Old system /** @public */ export const buiThemerPlugin = createPlugin({ - id: 'bui-themer', + id: 'mui-to-bui', routes: { root: rootRouteRef, }, @@ -50,11 +50,11 @@ export const BuiThemerPage = buiThemerPlugin.provide( // New system /** @public */ export default createFrontendPlugin({ - pluginId: 'bui-themer', + pluginId: 'mui-to-bui', extensions: [ PageBlueprint.make({ params: { - path: '/bui-themer', + path: '/mui-to-bui', loader: () => import('./components/BuiThemerPage').then(m => ), routeRef: convertLegacyRouteRef(rootRouteRef), diff --git a/plugins/bui-themer/src/routes.ts b/plugins/bui-themer/src/routes.ts index efd79e62be..ea7c6784f7 100644 --- a/plugins/bui-themer/src/routes.ts +++ b/plugins/bui-themer/src/routes.ts @@ -16,5 +16,5 @@ import { createRouteRef } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ - id: 'bui-themer', + id: 'mui-to-bui', }); diff --git a/yarn.lock b/yarn.lock index 09dca70063..edb265f511 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4323,37 +4323,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-bui-themer@workspace:^, @backstage/plugin-bui-themer@workspace:plugins/bui-themer": - version: 0.0.0-use.local - resolution: "@backstage/plugin-bui-themer@workspace:plugins/bui-themer" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/core-compat-api": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" - "@backstage/dev-utils": "workspace:^" - "@backstage/frontend-plugin-api": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@backstage/theme": "workspace:^" - "@backstage/ui": "workspace:^" - "@mui/material": "npm:^5.12.2" - "@mui/system": "npm:^5.16.14" - "@testing-library/jest-dom": "npm:^6.0.0" - "@testing-library/react": "npm:^16.0.0" - "@types/react": "npm:^18.0.0" - react: "npm:^18.0.2" - react-dom: "npm:^18.0.2" - react-router-dom: "npm:^6.3.0" - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - react-router-dom: ^6.3.0 - peerDependenciesMeta: - "@types/react": - optional: true - languageName: unknown - linkType: soft - "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws" @@ -5739,6 +5708,37 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-mui-to-bui@workspace:^, @backstage/plugin-mui-to-bui@workspace:plugins/bui-themer": + version: 0.0.0-use.local + resolution: "@backstage/plugin-mui-to-bui@workspace:plugins/bui-themer" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-compat-api": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@backstage/ui": "workspace:^" + "@mui/material": "npm:^5.12.2" + "@mui/system": "npm:^5.16.14" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^16.0.0" + "@types/react": "npm:^18.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.3.0" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email": version: 0.0.0-use.local resolution: "@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email" @@ -29694,7 +29694,6 @@ __metadata: "@backstage/integration-react": "workspace:^" "@backstage/plugin-api-docs": "workspace:^" "@backstage/plugin-auth-react": "workspace:^" - "@backstage/plugin-bui-themer": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-graph": "workspace:^" @@ -29705,6 +29704,7 @@ __metadata: "@backstage/plugin-home": "workspace:^" "@backstage/plugin-kubernetes": "workspace:^" "@backstage/plugin-kubernetes-cluster": "workspace:^" + "@backstage/plugin-mui-to-bui": "workspace:^" "@backstage/plugin-notifications": "workspace:^" "@backstage/plugin-org": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" From c890dc6edbea7dc5e85df0c3d1dae37e78117486 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 25 Sep 2025 11:58:22 +0200 Subject: [PATCH 114/193] bui-themer: remove unnecessary matchMedia mocks Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/BuiThemePreview.test.tsx | 15 --------------- .../BuiThemerPage/BuiThemerPage.test.tsx | 15 --------------- .../BuiThemerPage/ThemeContent.test.tsx | 16 ---------------- 3 files changed, 46 deletions(-) diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx index 79a2f9e83e..375f9411a7 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx @@ -20,21 +20,6 @@ import { renderInTestApp } from '@backstage/test-utils'; import { BuiThemePreview } from './BuiThemePreview'; describe('BuiThemePreview', () => { - beforeAll(() => { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), - }); - }); it('renders headings and sample components', async () => { const { container } = await renderInTestApp( { - beforeAll(() => { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), - }); - }); it('renders empty state when no themes installed', async () => { const apis = [[appThemeApiRef, { getInstalledThemes: () => [] }]] as const; await renderInTestApp( diff --git a/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx b/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx index 8ece77ad0e..3a075b80d3 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx @@ -30,22 +30,6 @@ jest.mock('./convertMuiToBuiTheme', () => ({ describe('ThemeContent', () => { const muiTheme = createTheme(); - beforeAll(() => { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), - }); - }); - it('renders title, variant and tabs', async () => { await renderInTestApp( Date: Thu, 25 Sep 2025 15:33:33 +0300 Subject: [PATCH 115/193] feat(catalog): add entity validation action one of the most requested feature as in validating your catalog-info files locally would be possible with this action using the backstage mcp server. additionally refactored the actions so that no need to add them individually in the plugin. Signed-off-by: Hellgren Heikki --- .changeset/red-dodos-work.md | 9 + .../createValidateEntityAction.test.ts | 284 ++++++++++++++++++ .../src/actions/createValidateEntityAction.ts | 99 ++++++ plugins/catalog-backend/src/actions/index.ts | 27 ++ .../src/service/CatalogPlugin.ts | 4 +- 5 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 .changeset/red-dodos-work.md create mode 100644 plugins/catalog-backend/src/actions/createValidateEntityAction.test.ts create mode 100644 plugins/catalog-backend/src/actions/createValidateEntityAction.ts create mode 100644 plugins/catalog-backend/src/actions/index.ts diff --git a/.changeset/red-dodos-work.md b/.changeset/red-dodos-work.md new file mode 100644 index 0000000000..1bc92d2d59 --- /dev/null +++ b/.changeset/red-dodos-work.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Added new `catalog:validate-entity` action to actions registry. + +This action can be used to validate entities against the software catalog. +This is useful for validating `catalog-info.yaml` file changes locally using the +Backstage MCP server. diff --git a/plugins/catalog-backend/src/actions/createValidateEntityAction.test.ts b/plugins/catalog-backend/src/actions/createValidateEntityAction.test.ts new file mode 100644 index 0000000000..19ec699cb7 --- /dev/null +++ b/plugins/catalog-backend/src/actions/createValidateEntityAction.test.ts @@ -0,0 +1,284 @@ +/* + * 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 { createValidateEntityAction } from './createValidateEntityAction'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; + +describe('createValidateEntityAction', () => { + const validEntityYaml = ` +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-component + namespace: default +spec: + type: service + lifecycle: production +`; + + const invalidYaml = ` +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-component + namespace: default +spec: + type: service + lifecycle: production + invalid: yaml: syntax: error +`; + + const validEntityObject = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + namespace: 'default', + }, + spec: { + type: 'service', + lifecycle: 'production', + }, + }; + + it('should validate a valid entity YAML and return success', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + // Mock validateEntity to return valid response + mockCatalog.validateEntity.mockResolvedValue({ + valid: true, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + }); + + expect(result.output).toEqual({ + isValid: true, + isValidYaml: true, + errors: [], + entity: validEntityObject, + }); + + expect(mockCatalog.validateEntity).toHaveBeenCalledWith( + validEntityObject, + 'url:https://localhost/entity-validator', + { credentials: expect.any(Object) }, + ); + }); + + it('should validate entity with custom location', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + mockCatalog.validateEntity.mockResolvedValue({ + valid: true, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const customLocation = 'https://github.com/example/repo/catalog-info.yaml'; + + await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { + entity: validEntityYaml, + location: customLocation, + }, + }); + + expect(mockCatalog.validateEntity).toHaveBeenCalledWith( + validEntityObject, + customLocation, + { credentials: expect.any(Object) }, + ); + }); + + it('should return validation errors when entity is invalid', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + const validationErrors = [ + { name: 'ValidationError', message: 'Missing required field: spec.type' }, + { name: 'ValidationError', message: 'Invalid lifecycle value' }, + ]; + + mockCatalog.validateEntity.mockResolvedValue({ + valid: false, + errors: validationErrors, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: true, + errors: ['Missing required field: spec.type', 'Invalid lifecycle value'], + entity: undefined, + }); + }); + + it('should handle invalid YAML syntax', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: invalidYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: [expect.stringContaining('YAML parsing error')], + }); + + // validateEntity should not be called if YAML parsing fails + expect(mockCatalog.validateEntity).not.toHaveBeenCalled(); + }); + + it('should handle catalog service errors gracefully', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + const catalogError = new Error('Catalog service unavailable'); + mockCatalog.validateEntity.mockRejectedValue(catalogError); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: ['Validation error: Catalog service unavailable'], + }); + }); + + it('should handle empty YAML input', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: '' }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: [expect.stringContaining('Validation error')], + }); + }); + + it('should handle malformed YAML with special characters', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const malformedYaml = ` +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-component + annotations: + invalid: "unclosed quote +spec: + type: service +`; + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: malformedYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: [expect.stringContaining('YAML parsing error')], + }); + }); + + it('should pass credentials to catalog service', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + mockCatalog.validateEntity.mockResolvedValue({ + valid: true, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const mockCredentials: BackstageCredentials = { + $$type: '@backstage/BackstageCredentials', + principal: { type: 'user', userEntityRef: 'user:default/test' }, + }; + + await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + credentials: mockCredentials, + }); + + expect(mockCatalog.validateEntity).toHaveBeenCalledWith( + validEntityObject, + 'url:https://localhost/entity-validator', + { credentials: mockCredentials }, + ); + }); +}); diff --git a/plugins/catalog-backend/src/actions/createValidateEntityAction.ts b/plugins/catalog-backend/src/actions/createValidateEntityAction.ts new file mode 100644 index 0000000000..a77a932e25 --- /dev/null +++ b/plugins/catalog-backend/src/actions/createValidateEntityAction.ts @@ -0,0 +1,99 @@ +/* + * 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import yaml from 'yaml'; +import { CatalogService } from '@backstage/plugin-catalog-node'; + +export const createValidateEntityAction = (options: { + actionsRegistry: ActionsRegistryService; + catalog: CatalogService; +}) => { + const { actionsRegistry, catalog } = options; + actionsRegistry.register({ + name: 'validate-entity', + title: 'Validate Catalog Entity', + description: ` + This action can be used to validate catalog-info.yaml file contents meant to be used with the software catalog. + It checks both that the YAML syntax is correct, and that the entity content is valid according to the catalog's rules. + `, + attributes: { + readOnly: true, + idempotent: true, + destructive: false, + }, + schema: { + input: z => + z.object({ + entity: z.string().describe('Entity YAML content to validate'), + location: z + .string() + .url() + .optional() + .describe('Location to validate'), + }), + output: z => + z.object({ + isValid: z.boolean().describe('Whether the entity is valid'), + isValidYaml: z.boolean().describe('Whether the YAML syntax is valid'), + errors: z.array(z.string()).describe('Array of validation errors'), + entity: z + .record(z.any()) + .optional() + .describe('Parsed entity object if valid'), + }), + }, + action: async ({ input, credentials }) => { + const { entity: content, location } = input; + try { + let entity; + try { + entity = yaml.parse(content); + } catch (yamlError) { + return { + output: { + isValid: false, + isValidYaml: false, + errors: [`YAML parsing error: ${yamlError.message}`], + }, + }; + } + + const resp = await catalog.validateEntity( + entity, + location ?? 'url:https://localhost/entity-validator', + { credentials }, + ); + + return { + output: { + isValid: resp.valid, + isValidYaml: true, + errors: resp.valid ? [] : resp.errors.map(e => e.message), + entity: resp.valid ? entity : undefined, + }, + }; + } catch (error) { + return { + output: { + isValid: false, + isValidYaml: false, + errors: [`Validation error: ${error.message}`], + }, + }; + } + }, + }); +}; diff --git a/plugins/catalog-backend/src/actions/index.ts b/plugins/catalog-backend/src/actions/index.ts new file mode 100644 index 0000000000..c53f956f62 --- /dev/null +++ b/plugins/catalog-backend/src/actions/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { CatalogService } from '@backstage/plugin-catalog-node'; +import { createGetCatalogEntityAction } from './createGetCatalogEntityAction.ts'; +import { createValidateEntityAction } from './createValidateEntityAction.ts'; + +export const createCatalogActions = (options: { + actionsRegistry: ActionsRegistryService; + catalog: CatalogService; +}) => { + createGetCatalogEntityAction(options); + createValidateEntityAction(options); +}; diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 24147e3cc5..cfe05712d9 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -45,7 +45,7 @@ import { Permission } from '@backstage/plugin-permission-common'; import { merge } from 'lodash'; import { CatalogBuilder } from './CatalogBuilder'; import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; -import { createGetCatalogEntityAction } from '../actions/createGetCatalogEntityAction'; +import { createCatalogActions } from '../actions'; class CatalogLocationsExtensionPointImpl implements CatalogLocationsExtensionPoint @@ -320,7 +320,7 @@ export const catalogPlugin = createBackendPlugin({ httpRouter.use(router); - createGetCatalogEntityAction({ + createCatalogActions({ catalog, actionsRegistry, }); From 6493c980ae4df3c6c78b1332d0fdd32ba3a77464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 25 Sep 2025 15:52:45 +0200 Subject: [PATCH 116/193] Log before provider-orphaning eviction happens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/brown-turkeys-send.md | 5 + ...evictEntitiesFromOrphanedProviders.test.ts | 198 +++++++++++++----- .../evictEntitiesFromOrphanedProviders.ts | 18 +- 3 files changed, 172 insertions(+), 49 deletions(-) create mode 100644 .changeset/brown-turkeys-send.md diff --git a/.changeset/brown-turkeys-send.md b/.changeset/brown-turkeys-send.md new file mode 100644 index 0000000000..87a1d03b01 --- /dev/null +++ b/.changeset/brown-turkeys-send.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Log before provider-orphaning eviction happens diff --git a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts index 1580194639..d28f39f5e1 100644 --- a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts +++ b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts @@ -13,62 +13,166 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityProvider } from '@backstage/plugin-catalog-node'; -import { mockServices } from '@backstage/backend-test-utils'; + +import { mockServices, TestDatabases } from '@backstage/backend-test-utils'; import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase'; -import { evictEntitiesFromOrphanedProviders } from './evictEntitiesFromOrphanedProviders'; +import { + evictEntitiesFromOrphanedProviders, + getOrphanedEntityProviderNames, +} from './evictEntitiesFromOrphanedProviders'; +import { applyDatabaseMigrations } from '../database/migrations'; +import { Knex } from 'knex'; -describe('evictEntitiesFromOrphanedProviders', () => { - const db = { - transaction: jest.fn().mockImplementation(cb => cb((() => {}) as any)), - replaceUnprocessedEntities: jest.fn(), - listReferenceSourceKeys: jest.fn(), - } as unknown as jest.Mocked; +jest.setTimeout(60_000); - const providers = [ - { getProviderName: () => 'provider1' }, - { getProviderName: () => 'provider2' }, - ] as unknown as EntityProvider[]; - const logger = mockServices.logger.mock(); +const databases = TestDatabases.create(); +const logger = mockServices.logger.mock(); - it('replaces unprocessed entities for orphaned providers with empty items', async () => { - db.listReferenceSourceKeys.mockResolvedValue(['foo', 'bar']); +afterEach(() => { + jest.resetAllMocks(); +}); - await evictEntitiesFromOrphanedProviders({ db, providers, logger }); +describe.each(databases.eachSupportedId())('%p', databaseId => { + let knex: Knex; + let db: DefaultProviderDatabase; - expect(db.replaceUnprocessedEntities).toHaveBeenCalledTimes(2); - expect(db.replaceUnprocessedEntities).toHaveBeenNthCalledWith( - 1, - expect.anything(), - { - sourceKey: 'foo', - type: 'full', - items: [], - }, - ); - expect(db.replaceUnprocessedEntities).toHaveBeenNthCalledWith( - 2, - expect.anything(), - { - sourceKey: 'bar', - type: 'full', - items: [], - }, - ); + beforeEach(async () => { + knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + db = new DefaultProviderDatabase({ database: knex, logger }); }); - it('does not replace unprocessed entities for providers that are not orphaned', async () => { - db.listReferenceSourceKeys.mockResolvedValue(['foo', 'provider1']); + afterEach(async () => { + await knex.destroy(); + }); - await evictEntitiesFromOrphanedProviders({ db, providers, logger }); + describe('getOrphanedEntityProviderNames', () => { + it('correctly locates and logs orphaned providers', async () => { + const providers = [ + { getProviderName: () => 'provider1', connect: jest.fn() }, + { getProviderName: () => 'provider2', connect: jest.fn() }, + ]; - expect(db.replaceUnprocessedEntities).not.toHaveBeenCalledWith( - expect.anything(), - { - sourceKey: 'provider1', - type: 'full', - items: [], - }, - ); + await knex('refresh_state').insert([ + { + entity_id: 'x', + entity_ref: 'x', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + ]); + await knex('refresh_state_references').insert([ + { source_key: 'provider2', target_entity_ref: 'x' }, + { source_key: 'provider3', target_entity_ref: 'x' }, + ]); + + await expect( + getOrphanedEntityProviderNames({ + db, + providers, + logger, + }), + ).resolves.toEqual(['provider3']); + + expect(logger.warn).toHaveBeenCalledTimes(4); + expect(logger.warn).toHaveBeenCalledWith( + `Found 1 orphaned entity provider(s)`, + ); + expect(logger.warn).toHaveBeenCalledWith( + `Database contained providers: 'provider2', 'provider3'`, + ); + expect(logger.warn).toHaveBeenCalledWith( + `Installed providers were: 'provider1', 'provider2'`, + ); + expect(logger.warn).toHaveBeenCalledWith( + `Orphaned providers were thus: 'provider3'`, + ); + }); + }); + + describe('evictEntitiesFromOrphanedProviders', () => { + it('replaces unprocessed entities for orphaned providers with empty items', async () => { + jest.spyOn(db, 'replaceUnprocessedEntities'); + + const providers = [ + { getProviderName: () => 'provider1', connect: jest.fn() }, + { getProviderName: () => 'provider2', connect: jest.fn() }, + ]; + + await knex('refresh_state').insert([ + { + entity_id: 'x', + entity_ref: 'x', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + ]); + await knex('refresh_state_references').insert([ + { source_key: 'foo', target_entity_ref: 'x' }, + { source_key: 'bar', target_entity_ref: 'x' }, + ]); + + await evictEntitiesFromOrphanedProviders({ db, providers, logger }); + + expect(db.replaceUnprocessedEntities).toHaveBeenCalledTimes(2); + expect(db.replaceUnprocessedEntities).toHaveBeenCalledWith( + expect.anything(), + { + sourceKey: 'foo', + type: 'full', + items: [], + }, + ); + expect(db.replaceUnprocessedEntities).toHaveBeenCalledWith( + expect.anything(), + { + sourceKey: 'bar', + type: 'full', + items: [], + }, + ); + }); + + it('does not replace unprocessed entities for providers that are not orphaned', async () => { + jest.spyOn(db, 'replaceUnprocessedEntities'); + + const providers = [ + { getProviderName: () => 'provider1', connect: jest.fn() }, + { getProviderName: () => 'provider2', connect: jest.fn() }, + ]; + + await knex('refresh_state').insert([ + { + entity_id: 'x', + entity_ref: 'x', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + ]); + await knex('refresh_state_references').insert([ + { source_key: 'foo', target_entity_ref: 'x' }, + { source_key: 'provider1', target_entity_ref: 'x' }, + ]); + + await evictEntitiesFromOrphanedProviders({ db, providers, logger }); + + expect(db.replaceUnprocessedEntities).not.toHaveBeenCalledWith( + expect.anything(), + { + sourceKey: 'provider1', + type: 'full', + items: [], + }, + ); + }); }); }); diff --git a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts index a6bda5e8c5..753a266250 100644 --- a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts +++ b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts @@ -18,12 +18,14 @@ import { EntityProvider } from '@backstage/plugin-catalog-node'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ProviderDatabase } from '../database/types'; -async function getOrphanedEntityProviderNames({ +export async function getOrphanedEntityProviderNames({ db, providers, + logger, }: { db: ProviderDatabase; providers: EntityProvider[]; + logger: LoggerService; }): Promise { const dbProviderNames = await db.transaction(async tx => db.listReferenceSourceKeys(tx), @@ -31,9 +33,21 @@ async function getOrphanedEntityProviderNames({ const providerNames = providers.map(p => p.getProviderName()); - return dbProviderNames.filter( + const orphaned = dbProviderNames.filter( dbProviderName => !providerNames.includes(dbProviderName), ); + + if (orphaned.length) { + const dbProviderNamesString = dbProviderNames.map(p => `'${p}'`).join(', '); + const providerNamesString = providerNames.map(p => `'${p}'`).join(', '); + const orphanedString = orphaned.map(p => `'${p}'`).join(', '); + logger.warn(`Found ${orphaned.length} orphaned entity provider(s)`); + logger.warn(`Database contained providers: ${dbProviderNamesString}`); + logger.warn(`Installed providers were: ${providerNamesString}`); + logger.warn(`Orphaned providers were thus: ${orphanedString}`); + } + + return orphaned; } async function removeEntitiesForProvider({ From 1bb69243494d1ac4edbaf3e107a8e4861a7d184a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Thu, 25 Sep 2025 17:11:34 +0200 Subject: [PATCH 117/193] fix: remove default selection of tab for tabs that have href defined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .../ui/src/components/Tabs/Tabs.stories.tsx | 57 +++++++++++++++++++ packages/ui/src/components/Tabs/Tabs.tsx | 15 ++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Tabs/Tabs.stories.tsx b/packages/ui/src/components/Tabs/Tabs.stories.tsx index cbfe3d8562..c818d0fe4b 100644 --- a/packages/ui/src/components/Tabs/Tabs.stories.tsx +++ b/packages/ui/src/components/Tabs/Tabs.stories.tsx @@ -455,3 +455,60 @@ export const RootPathMatching: Story = { ), }; + +export const AutoSelectionOfTabs: Story = { + args: { + children: '', + }, + render: () => ( + +
+ + Current URL: /random-page + + + {/* Without hrefs */} + + {' '} + Case 1: Without hrefs + + + + Settings + Preferences + Advanced + + + Settings content - React Aria manages this selection + + + Preferences content - works normally + + + Advanced content - local state only + + + + {/* With hrefs */} + + {' '} + Case 2: With hrefs By default no selection is shown + because the URL doesn't match any tab's href.{' '} + + + + + Catalog + + + Create + + + Docs + + + +
+
+ ), +}; diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx index dacb355fc9..371de4ef41 100644 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ b/packages/ui/src/components/Tabs/Tabs.tsx @@ -115,9 +115,22 @@ export const Tabs = (props: TabsProps) => { } } } + + //No route matches - check if all tabs have hrefs (pure navigation) + const allTabsHaveHref = tabListChildren.every( + child => isValidElement(child) && child.props.href, + ); + + if (allTabsHaveHref) { + // Pure navigation tabs, no route match + return null; + } else { + // Mixed tabs or pure local state + return undefined; + } } } - return null; + return undefined; })(); if (!children) return null; From 68412799e4ed1971677165d9a3c92a3e1285206b Mon Sep 17 00:00:00 2001 From: Marley Date: Fri, 26 Sep 2025 08:03:15 +0100 Subject: [PATCH 118/193] fix: JSON filter syntax in catalog-customization.md Removed unnecessary braces from JSON filter example. Signed-off-by: Marley --- docs/features/software-catalog/catalog-customization.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 1ef865039f..2ed4b34822 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -571,10 +571,8 @@ Finally, entity predicates also support value operators that can be used in plac ```json { "filter": { - { - "kind": "component", - "spec.type": { "$in": ["service", "website"] } - }, + "kind": "component", + "spec.type": { "$in": ["service", "website"] } } } ``` From a5f4d995e71be6fa120ac6503518498f0ac14cf5 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 26 Sep 2025 11:38:04 +0200 Subject: [PATCH 119/193] fix: gendocu public apis error Signed-off-by: Camila Belo --- package.json | 1 + yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b510a5a864..c41ba744b2 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch", + "GendocuPublicApis": "npm:gendocu-public-apis@^1.0.0", "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch" }, "dependencies": { diff --git a/yarn.lock b/yarn.lock index c1a352407c..fcf73ef736 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22991,13 +22991,13 @@ __metadata: languageName: node linkType: hard -"GendocuPublicApis@https://git.gendocu.com/gendocu/GendocuPublicApis.git#master": - version: 1.0.0 - resolution: "GendocuPublicApis@https://git.gendocu.com/gendocu/GendocuPublicApis.git#commit=6e8d4c1d2342556a2e8e719f19385b266001ebae" +"GendocuPublicApis@npm:gendocu-public-apis@^1.0.0": + version: 1.0.3 + resolution: "gendocu-public-apis@npm:1.0.3" dependencies: "@improbable-eng/grpc-web": "npm:^0.14.0" google-protobuf: "npm:^3.15.8" - checksum: 10/a34386a6137d92f392121305fb899f63ba1631a1b60c80781f6e56e160c1d462a8ce4b3ee93cb67c73c079b53c6de57ba19f514f29dd11c9783cd0d8227c6968 + checksum: 10/261a5cc97b599a7dfb492ac048e6bfce3f3bf64627f2b80ef195d5b6cefdbd4668d2b2ccd10676f01eb4f7862a5d19da567dd6ade9fc39a15268e81862308f7c languageName: node linkType: hard From afc8c18f3922e8e89ce3d61f6b74c3eafb840a09 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 26 Sep 2025 11:39:09 +0200 Subject: [PATCH 120/193] docs: clarify legacy backend system steps Signed-off-by: Vincenzo Scamporlino --- docs/integrations/gitlab/discovery.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 0fce7b864e..5e02c853fa 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -27,7 +27,7 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab Then add the following to your backend initialization: -```ts title="packages/backend/src/index.ts +```ts title="packages/backend/src/index.ts" // optional if you want HTTP endpoints to receive external events // backend.add(import('@backstage/plugin-events-backend')); // optional if you want to use AWS SQS instead of HTTP endpoints to receive external events @@ -49,7 +49,7 @@ Further documentation: - [Events Plugin](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [GitLab Module for the Events Plugin](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) -### Installation with Legacy Backend System +### Installation with Legacy Backend System (skip if you are using Backstage v1.31.0 or later) #### Installation without Events Support From ab96bb770f57e8d7e84375047d9a9e1ed49105f4 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 24 Sep 2025 09:57:46 +0200 Subject: [PATCH 121/193] feat: add package start entry dir option Signed-off-by: Camila Belo --- .changeset/modern-pugs-appear.md | 21 +++++++++++++++++++ docs/tooling/cli/03-commands.md | 11 +++++----- .../build/commands/package/start/command.ts | 1 + .../commands/package/start/startPackage.ts | 3 ++- packages/cli/src/modules/build/index.ts | 4 ++++ 5 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 .changeset/modern-pugs-appear.md diff --git a/.changeset/modern-pugs-appear.md b/.changeset/modern-pugs-appear.md new file mode 100644 index 0000000000..c9b9f56e89 --- /dev/null +++ b/.changeset/modern-pugs-appear.md @@ -0,0 +1,21 @@ +--- +'@backstage/cli': patch +--- + +Added a new `--entry-dir` option to the `package start` command, which allows you to specify a custom entry directory for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). + +**Example usage:** + +Consider the following plugin dev folder structure: + +``` +dev/ + index.tsx + alpha/ + index.ts +``` + +- The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` +- Running `yarn package start --entry-dir dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` + +This enables you to maintain multiple development environments within a single plugin package. diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index d176278655..da8a9650f1 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -177,11 +177,12 @@ Usage: backstage-cli package start [options] Start a package for local development Options: - --config Config files to load instead of app-config.yaml (default: []) - --role Run the command with an explicit package role - --check Enable type checking and linting if available - --inspect Enable debugger in Node.js environments - --inspect-brk Enable debugger in Node.js environments, breaking before code starts + --config Config files to load instead of app-config.yaml (default: []) + --role Run the command with an explicit package role + --check Enable type checking and linting if available + --inspect Enable debugger in Node.js environments + --inspect-brk Enable debugger in Node.js environments, breaking before code starts + --entry-dir Path to the directory containing the entry point file ``` ## package build diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index 05fc668356..4a65953f76 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -23,6 +23,7 @@ import { paths } from '../../../../../lib/paths'; export async function command(opts: OptionValues): Promise { await startPackage({ role: await findRoleFromCommand(opts), + entryDir: opts.entryDir, targetDir: paths.targetDir, configPaths: opts.config as string[], checksEnabled: Boolean(opts.check), diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index 10f705ba4d..bac7bee68f 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -20,6 +20,7 @@ import { startFrontend } from './startFrontend'; export async function startPackage(options: { role: PackageRole; + entryDir: string; targetDir: string; configPaths: string[]; checksEnabled: boolean; @@ -45,8 +46,8 @@ export async function startPackage(options: { case 'frontend-plugin': case 'frontend-plugin-module': return startFrontend({ - entry: 'dev/index', ...options, + entry: `${options.entryDir ?? 'dev'}/index`, }); case 'frontend-dynamic-container' as PackageRole: // experimental return startFrontend({ diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index fd31a6c782..552a35a316 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -137,6 +137,10 @@ export const buildPlugin = createCliPlugin({ '--link ', 'Link an external workspace for module resolution', ) + .option( + '--entry-dir ', + 'Path to the directory containing the entry point file', + ) .action(lazy(() => import('./commands/package/start'), 'command')); await defaultCommand.parseAsync(args, { from: 'user' }); From 8a16781eba4515328e5ec792b48bda4b25fd5408 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 24 Sep 2025 13:58:35 +0200 Subject: [PATCH 122/193] refactor: apply review suggestions Signed-off-by: Camila Belo --- .changeset/modern-pugs-appear.md | 8 +- docs/tooling/cli/03-commands.md | 12 +-- packages/cli/cli-report.md | 1 + .../build/commands/package/start/command.ts | 2 +- .../package/start/startPackage.test.ts | 89 +++++++++++++++++++ .../commands/package/start/startPackage.ts | 20 ++++- packages/cli/src/modules/build/index.ts | 4 +- 7 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/modules/build/commands/package/start/startPackage.test.ts diff --git a/.changeset/modern-pugs-appear.md b/.changeset/modern-pugs-appear.md index c9b9f56e89..8b502fb52d 100644 --- a/.changeset/modern-pugs-appear.md +++ b/.changeset/modern-pugs-appear.md @@ -2,7 +2,7 @@ '@backstage/cli': patch --- -Added a new `--entry-dir` option to the `package start` command, which allows you to specify a custom entry directory for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). +Added a new `--entrypoint` option to the `package start` command, which allows you to specify a custom entry directory/file for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). **Example usage:** @@ -15,7 +15,5 @@ dev/ index.ts ``` -- The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` -- Running `yarn package start --entry-dir dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` - -This enables you to maintain multiple development environments within a single plugin package. +- The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` file; +- Running `yarn package start --entrypoint dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` file. diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index da8a9650f1..ce8f862c98 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -177,12 +177,12 @@ Usage: backstage-cli package start [options] Start a package for local development Options: - --config Config files to load instead of app-config.yaml (default: []) - --role Run the command with an explicit package role - --check Enable type checking and linting if available - --inspect Enable debugger in Node.js environments - --inspect-brk Enable debugger in Node.js environments, breaking before code starts - --entry-dir Path to the directory containing the entry point file + --config Config files to load instead of app-config.yaml (default: []) + --role Run the command with an explicit package role + --check Enable type checking and linting if available + --inspect Enable debugger in Node.js environments + --inspect-brk Enable debugger in Node.js environments, breaking before code starts + --entrypoint Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev" ``` ## package build diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index e2f5bb9cc6..a8df5242ad 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -312,6 +312,7 @@ Usage: program [options] Options: --check --config + --entrypoint --inspect [host] --inspect-brk [host] --link diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index 4a65953f76..b38957f488 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -23,7 +23,7 @@ import { paths } from '../../../../../lib/paths'; export async function command(opts: OptionValues): Promise { await startPackage({ role: await findRoleFromCommand(opts), - entryDir: opts.entryDir, + entrypoint: opts.entrypoint, targetDir: paths.targetDir, configPaths: opts.config as string[], checksEnabled: Boolean(opts.check), diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts new file mode 100644 index 0000000000..b2aa172867 --- /dev/null +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2020 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 { createMockDirectory } from '@backstage/backend-test-utils'; +import { resolveEntryPath } from './startPackage'; + +describe('resolveEntryPath', () => { + const mockDir = createMockDirectory(); + + afterEach(() => { + mockDir.clear(); + }); + + it('should remove file extensions', () => { + mockDir.setContent({ + 'dev/custom.tsx': '// dev app code', + }); + + const result = resolveEntryPath('dev/custom.tsx', mockDir.path); + + expect(result).toBe('dev/custom'); + }); + + it('should remove trailing slashes', () => { + mockDir.setContent({ + 'dev/alpha.ts': '// dev app code', + }); + + const result = resolveEntryPath('dev/alpha/', mockDir.path); + + expect(result).toBe('dev/alpha'); + }); + + it('should handle multiple dots in filename', () => { + mockDir.setContent({ + 'file.backup.old.ts': 'export const data = {};', + }); + + const result = resolveEntryPath('file.backup.old.ts', mockDir.path); + + expect(result).toBe('file.backup.old'); + }); + + it('should handle simple directory names', () => { + mockDir.setContent({ + 'dev/index.ts': '// dev app code', + }); + + const result = resolveEntryPath('dev', mockDir.path); + + expect(result).toBe('dev/index'); + }); + + it('should handle nested directory paths', () => { + mockDir.setContent({ + 'dev/alpha/index.ts': '// dev app code', + }); + + const result = resolveEntryPath('dev/alpha', mockDir.path); + + expect(result).toBe('dev/alpha/index'); + }); + + it('should return the file when there is a directory with the same name', () => { + mockDir.setContent({ + 'dev/alpha.ts': '// dev app code', + 'dev/app-config.yaml': '// dev app config', + 'dev/alpha/index.ts': '// dev app code', + 'dev/alpha/app-config.yaml': '// dev app config', + }); + + const result = resolveEntryPath('dev/alpha', mockDir.path); + + expect(result).toBe('dev/alpha'); + }); +}); diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index bac7bee68f..368a28b5df 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -17,10 +17,26 @@ import { PackageRole } from '@backstage/cli-node'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; +import { statSync, readdirSync } from 'fs'; +import { resolve, join, parse } from 'path'; + +export function resolveEntryPath( + entrypoint: string = 'dev', + targetDir: string, +): string { + const { dir: entryDir, name: entryName } = parse(entrypoint); + const entryPath = join(entryDir, entryName); + const parentPath = resolve(targetDir, entryDir); + const isFile = readdirSync(parentPath).some(base => { + const path = resolve(parentPath, base); + return statSync(path).isFile(); + }); + return isFile ? entryPath : join(entryPath, 'index'); +} export async function startPackage(options: { role: PackageRole; - entryDir: string; + entrypoint?: string; targetDir: string; configPaths: string[]; checksEnabled: boolean; @@ -47,7 +63,7 @@ export async function startPackage(options: { case 'frontend-plugin-module': return startFrontend({ ...options, - entry: `${options.entryDir ?? 'dev'}/index`, + entry: resolveEntryPath(options.entrypoint, options.targetDir), }); case 'frontend-dynamic-container' as PackageRole: // experimental return startFrontend({ diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 552a35a316..88706046ee 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -138,8 +138,8 @@ export const buildPlugin = createCliPlugin({ 'Link an external workspace for module resolution', ) .option( - '--entry-dir ', - 'Path to the directory containing the entry point file', + '--entrypoint ', + 'Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev"', ) .action(lazy(() => import('./commands/package/start'), 'command')); From 941613854c8db61db102afc4a572f84368be897b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Sep 2025 09:39:20 +0200 Subject: [PATCH 123/193] refactor: rephrase the flag description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Camila Belo --- packages/cli/src/modules/build/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 88706046ee..5382adfec9 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -139,7 +139,7 @@ export const buildPlugin = createCliPlugin({ ) .option( '--entrypoint ', - 'Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev"', + 'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"', ) .action(lazy(() => import('./commands/package/start'), 'command')); From d070bda0d4409d143d7bb41eefef43be178905ad Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Sep 2025 10:55:50 +0200 Subject: [PATCH 124/193] refactor: simplify the resolve entry path logic Signed-off-by: Camila Belo --- .../commands/package/start/startPackage.test.ts | 6 +++--- .../build/commands/package/start/startPackage.ts | 16 +++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts index b2aa172867..ca153adb22 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts @@ -46,12 +46,12 @@ describe('resolveEntryPath', () => { it('should handle multiple dots in filename', () => { mockDir.setContent({ - 'file.backup.old.ts': 'export const data = {};', + 'index.alpha.ts': 'export const data = {};', }); - const result = resolveEntryPath('file.backup.old.ts', mockDir.path); + const result = resolveEntryPath('index.alpha.ts', mockDir.path); - expect(result).toBe('file.backup.old'); + expect(result).toBe('index.alpha'); }); it('should handle simple directory names', () => { diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index 368a28b5df..b49fd482e5 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -17,21 +17,19 @@ import { PackageRole } from '@backstage/cli-node'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; -import { statSync, readdirSync } from 'fs'; -import { resolve, join, parse } from 'path'; +import { parse, resolve, join } from 'path'; +import { glob } from 'glob'; export function resolveEntryPath( entrypoint: string = 'dev', targetDir: string, ): string { const { dir: entryDir, name: entryName } = parse(entrypoint); - const entryPath = join(entryDir, entryName); - const parentPath = resolve(targetDir, entryDir); - const isFile = readdirSync(parentPath).some(base => { - const path = resolve(parentPath, base); - return statSync(path).isFile(); - }); - return isFile ? entryPath : join(entryPath, 'index'); + const [entryFile] = glob.sync(`${resolve(targetDir, entryDir, entryName)}.*`); + if (entryFile) { + return join(entryDir, entryName); + } + return join(entryDir, entryName, 'index'); } export async function startPackage(options: { From b78dc1154cc7bf35197b2cf8645f213ffc049411 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Fri, 26 Sep 2025 11:42:09 +0100 Subject: [PATCH 125/193] add page to docs-ui, fix aria labels, compatibility with one password icons, create custom button Signed-off-by: Hermione Bird --- docs-ui/public/theme-backstage.css | 2 +- docs-ui/src/app/page.mdx | 5 + .../src/content/components/password-field.mdx | 64 ++++ .../components/password-field.props.ts | 43 +++ docs-ui/src/snippets/stories-snippets.tsx | 3 + docs-ui/src/utils/changelog.ts | 3 +- docs-ui/src/utils/data.ts | 5 + packages/ui/css/styles.css | 343 +++++++++++------- packages/ui/report.api.md | 12 + .../PasswordField/PasswordField.styles.css | 131 ++----- .../PasswordField/PasswordField.tsx | 43 ++- .../ui/src/components/TextField/TextField.tsx | 14 - packages/ui/src/components/TextField/types.ts | 12 +- packages/ui/src/utils/componentDefinitions.ts | 5 +- 14 files changed, 400 insertions(+), 285 deletions(-) create mode 100644 docs-ui/src/content/components/password-field.mdx create mode 100644 docs-ui/src/content/components/password-field.props.ts diff --git a/docs-ui/public/theme-backstage.css b/docs-ui/public/theme-backstage.css index 98b0f62b47..69cd0dbd31 100644 --- a/docs-ui/public/theme-backstage.css +++ b/docs-ui/public/theme-backstage.css @@ -1,2 +1,2 @@ /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ -@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}}.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--pl-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--pl-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--pl-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--pl-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--pl-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--pr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--pr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--pr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--pr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--pr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--pt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--pt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--pt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--pt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--pt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--pb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--pb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--pb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--pb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--pb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--py-xs);margin-bottom:var(--py-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--py-sm);margin-bottom:var(--py-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--py-md);margin-bottom:var(--py-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--py-lg);margin-bottom:var(--py-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--py-xl);margin-bottom:var(--py-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--px-xs);margin-right:var(--px-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--px-sm);margin-right:var(--px-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--px-md);margin-right:var(--px-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--px-lg);margin-right:var(--px-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--px-xl);margin-right:var(--px-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#ebebeb;--bui-bg-tint:transparent;--bui-bg-tint-hover:#1f549366;--bui-bg-tint-pressed:#1f549399;--bui-bg-tint-disabled:#ebebeb;--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#9c9c9c;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg:#333;--bui-bg-surface-1:#424242;--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#222;--bui-bg-tint:transparent;--bui-bg-tint-hover:#9cc9ff1f;--bui-bg-tint-pressed:#9cc9ff29;--bui-bg-tint-disabled:transparent;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:var(--bui-gray-5);--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757}.bui-AvatarRoot{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:2rem;height:2rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-AvatarRoot[data-size=small]{width:1.5rem;height:1.5rem}.bui-AvatarRoot[data-size=medium]{width:2rem;height:2rem}.bui-AvatarRoot[data-size=large]{width:3rem;height:3rem}.bui-AvatarImage{object-fit:cover;width:100%;height:100%}.bui-AvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-Box{font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary)}.bui-Button{user-select:none;font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-bold);cursor:pointer;border-radius:var(--bui-radius-2);justify-content:center;align-items:center;gap:var(--bui-space-1_5);border:none;flex-shrink:0;padding:0;display:inline-flex;&[data-disabled=true]{cursor:not-allowed}}.bui-Button[data-variant=primary]{background-color:var(--bui-bg-solid);color:var(--bui-fg-solid);&:hover{background-color:var(--bui-bg-solid-hover);transition:background-color .15s}&:active{background-color:var(--bui-bg-solid-pressed)}&:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled=true]{background-color:var(--bui-bg-solid-disabled);color:var(--bui-fg-solid-disabled)}}.bui-Button[data-variant=secondary]{background-color:var(--bui-bg-surface-1);box-shadow:inset 0 0 0 1px var(--bui-border);color:var(--bui-fg-primary);&:hover{box-shadow:inset 0 0 0 1px var(--bui-border-hover);transition:box-shadow .15s}&:active{box-shadow:inset 0 0 0 1px var(--bui-border-pressed)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{box-shadow:inset 0 0 0 1px var(--bui-border-disabled);color:var(--bui-fg-disabled)}}.bui-Button[data-variant=tertiary]{color:var(--bui-fg-primary);background-color:#0000;&:hover{background-color:var(--bui-bg-surface-1);transition:background-color .2s}&:active{background-color:var(--bui-bg-surface-2)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{color:var(--bui-fg-disabled);background-color:#0000}}.bui-Button[data-size=medium]{font-size:var(--bui-font-size-4);padding:0 var(--bui-space-3);height:2.5rem}.bui-Button[data-size=small]{font-size:var(--bui-font-size-3);padding:0 var(--bui-space-2);height:2rem}.bui-Button[data-size=small] svg{width:1rem;height:1rem}.bui-Button[data-size=medium] svg{width:1.25rem;height:1.25rem}.bui-ButtonIcon{justify-content:center;align-items:center}.bui-ButtonIcon[data-size=small]{width:2rem;padding:0}.bui-ButtonIcon[data-size=medium]{width:2.5rem;padding:0}.bui-Card{gap:var(--bui-space-3);background-color:var(--bui-bg-surface-1);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-3);color:var(--bui-fg-primary);border:1px solid var(--bui-border);flex-direction:column;width:100%;min-height:0;display:flex;overflow:hidden}.bui-CardBody{flex:1;min-height:0;overflow:auto}.bui-CardHeader,.bui-CardFooter{padding-inline:var(--bui-space-3)}.bui-CheckboxRoot{width:1rem;height:1rem;box-shadow:inset 0 0 0 1px var(--bui-border);cursor:pointer;background-color:var(--bui-bg-surface-1);border:none;border-radius:2px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background-color .2s ease-in-out;display:flex}.bui-CheckboxRoot:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px;transition:none}.bui-CheckboxRoot[data-checked]{background-color:var(--bui-bg-solid);box-shadow:none;color:var(--bui-fg-solid)}.bui-CheckboxLabel{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);user-select:none;flex-direction:row;display:flex;&:hover{& .bui-CheckboxRoot:not([data-checked]){box-shadow:inset 0 0 0 1px var(--bui-border-hover)}}}.bui-CheckboxIndicator{color:var(--bui-fg-solid);justify-content:center;align-items:center;display:flex}.bui-CollapsiblePanel{height:var(--collapsible-panel-height);transition:all .15s ease-out;display:flex;overflow:hidden;&[data-starting-style],&[data-ending-style]{height:0}}.bui-Container{max-width:120rem;padding-inline:var(--bui-space-4);margin-inline:auto;transition:padding .2s ease-in-out}@media (width>=640px){.bui-Container{padding-inline:var(--bui-space-5)}}.bui-FieldError{color:var(--bui-fg-danger);font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);margin-top:var(--bui-space-2);display:inline-block}.bui-FieldLabelWrapper{margin-bottom:var(--bui-space-3);gap:var(--bui-space-1);flex-direction:column;display:flex}.bui-FieldLabel{color:var(--bui-fg-primary);cursor:pointer;font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);margin-right:auto}.bui-FieldSecondaryLabel{color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular);margin-left:var(--bui-space-1)}.bui-FieldDescription{font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);margin:0}.bui-Flex{min-width:0;display:flex}.bui-Grid{display:grid}.bui-HeaderToolbar{margin-bottom:var(--bui-space-6);&:before{content:"";background-color:var(--bui-bg);z-index:0;height:16px;position:absolute;top:0;left:0;right:0}&[data-has-tabs=true]{margin-bottom:0}}.bui-HeaderToolbarWrapper{z-index:1;background-color:var(--bui-bg-surface-1);padding-inline:var(--bui-space-5);border-bottom:1px solid var(--bui-border);color:var(--bui-fg-primary);flex-direction:row;justify-content:space-between;align-items:center;height:52px;display:flex;position:relative}.bui-HeaderToolbarContent{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-HeaderToolbarName{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);flex-direction:row;flex-shrink:0;display:flex}.bui-HeaderToolbarIcon{width:16px;height:16px;color:var(--bui-fg-primary);& svg{width:100%;height:100%}}.bui-HeaderToolbarControls{right:var(--bui-space-5);align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.bui-HeaderTabsWrapper{margin-bottom:var(--bui-space-4);padding-inline:var(--bui-space-3);border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1)}.bui-HeaderPage{gap:var(--bui-space-1);margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6);flex-direction:column;display:flex}.bui-HeaderPageContent{flex-direction:row;justify-content:space-between;display:flex}.bui-HeaderPageTabsWrapper{margin-left:-8px}.bui-HeaderPageControls,.bui-HeaderPageBreadcrumbs{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-Icon{width:1rem;height:1rem}.bui-Link{font-family:var(--bui-font-regular);cursor:pointer;margin:0;padding:0;text-decoration-line:none;display:inline-block;&:hover{text-underline-offset:calc(.025em + 2px);text-decoration-line:underline;text-decoration-style:solid;text-decoration-thickness:min(2px,max(1px,.05em));text-decoration-color:color-mix(in srgb,currentColor 30%,transparent)}}.bui-MenuPopover{border:1px solid var(--bui-border);border-radius:var(--bui-radius-2);background:var(--bui-bg-surface-1);color:var(--bui-fg-primary);outline:none;flex-direction:column;min-height:0;transition:transform .2s,opacity .2s;display:flex;overflow:hidden;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}&[data-placement=top]{--origin:translateY(8px)}&[data-placement=bottom]{--origin:translateY(-8px)}&[data-placement=right]{--origin:translateX(-8px)}&[data-placement=left]{--origin:translateX(8px)}}.bui-MenuContent{max-height:inherit;box-sizing:border-box;padding:var(--bui-space-1);outline:none;min-width:150px;overflow:auto}.bui-MenuPopover .bui-ScrollAreaRoot{flex-direction:column;flex:1;height:100%;min-height:0;display:flex}.bui-MenuPopover .bui-ScrollAreaScrollbar{margin-inline:var(--bui-space-1_5)}.bui-MenuItem{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&[data-focused],&[data-open]{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-color=danger]{color:var(--bui-fg-danger)}&[data-color=danger][data-focused]{background:var(--bui-bg-danger);color:var(--bui-fg-danger)}&[data-has-submenu]{&>.bui-MenuItemArrow{display:block}}}.bui-MenuItemListBox{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&:hover{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-selected] .bui-MenuItemListBoxCheck{&>svg{opacity:1;color:var(--bui-fg-primary)}}}.bui-MenuItemListBoxCheck{justify-content:center;align-items:center;width:1rem;height:1rem;display:flex;&>svg{opacity:0;width:1rem;height:1rem}}.bui-MenuItemContent{align-items:center;gap:var(--bui-space-2);display:flex;&>svg{width:1rem;height:1rem}}.bui-MenuItemArrow{width:1rem;height:1rem;display:none;&>svg{width:1rem;height:1rem}}.bui-MenuSection{&:first-child .bui-MenuSectionHeader{padding-top:0}}.bui-MenuSectionHeader{height:2rem;padding-top:var(--bui-space-3);padding-left:var(--bui-space-2);color:var(--bui-fg-primary);font-size:var(--bui-font-size-1);letter-spacing:.05rem;text-transform:uppercase;align-items:center;font-weight:700;display:flex}.bui-MenuSeparator{background:var(--bui-border);height:1px;margin-inline:var(--bui-space-1_5);margin-block:var(--bui-space-1)}.bui-MenuSearchField{font-family:var(--bui-font-regular);flex-shrink:0;width:100%;position:relative;&[data-empty]{& .bui-MenuSearchFieldClear{display:none}}}.bui-MenuSearchFieldInput{padding:0 var(--bui-space-3);border:none;border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:2rem;cursor:inherit;outline:none;align-items:center;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}}.bui-MenuSearchFieldClear{right:var(--bui-space-2);cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;&>svg{width:1rem;height:1rem}}.bui-MenuEmptyState{padding:var(--bui-space-1);color:var(--bui-fg-secondary);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular)}.bui-Popover{background-color:var(--bui-bg-surface-1);border:1px solid var(--bui-border);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-1);margin-right:12px;overflow:scroll;box-shadow:0 4px 12px #0000001a}.bui-RadioGroup{color:var(--bui-fg-primary);flex-direction:column;display:flex}.bui-RadioGroup[data-orientation=horizontal] .bui-RadioGroupContent{gap:var(--bui-space-4);flex-direction:row}.bui-RadioGroupContent{gap:var(--bui-space-2);flex-direction:column;display:flex}.bui-Radio{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-2);color:var(--bui-fg-primary);forced-color-adjust:none;display:flex;position:relative;&:before{content:"";box-sizing:border-box;border:.125rem solid var(--bui-border);background:var(--bui-gray-1);border-radius:var(--bui-radius-full);width:1rem;height:1rem;transition:all .2s;display:block}&[data-pressed]:before{border-color:var(--bui-border)}&[data-selected]{&:before{border-color:var(--bui-bg-solid);border-width:.25rem}&[data-pressed]:before{border-color:var(--bui-bg-solid)}}&[data-focus-visible]:before{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}&[data-invalid]:before,&[data-invalid][data-selected]:before{border-color:var(--bui-border-danger)}&[data-disabled][data-invalid]{color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}}.bui-Table{caption-side:bottom;border-collapse:collapse;width:100%}.bui-TableHeader{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out}.bui-TableHead{text-align:left;padding:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary)}.bui-TableHeadSortButton{cursor:pointer;user-select:none;align-items:center;gap:var(--bui-space-1);display:inline-flex;&:hover svg{opacity:.5}& svg{opacity:0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}&[data-sort-order=asc] svg{opacity:1;transform:rotate(0)}&[data-sort-order=desc] svg{opacity:1;transform:rotate(180deg)}}.bui-TableBody{color:var(--bui-fg-primary)}.bui-TableRow{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out;&[data-react-aria-pressable=true]{cursor:pointer}}.bui-TableBody .bui-TableRow:hover{background-color:var(--bui-gray-2)}.bui-TableCell{padding:var(--bui-space-3);font-size:var(--bui-font-size-3);padding:var(--bui-space-3);font-size:var(--bui-font-size-3)}.bui-TableCellContentWrapper{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:inline-flex}.bui-TableCellIcon,.bui-TableCellIcon svg{color:var(--bui-fg-primary);align-items:center;display:inline-flex}.bui-TableCellContent{gap:var(--bui-space-0_5);flex-direction:column;display:flex}.bui-TableCellProfile{gap:var(--bui-space-2);flex-direction:row;align-items:center;display:flex}.bui-TableCellProfileAvatar{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-TableCellProfileAvatarImage{object-fit:cover;width:100%;height:100%}.bui-TableCellProfileAvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-DataTablePagination{padding-top:var(--bui-space-5);justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--left{justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--right{justify-content:space-between;align-items:center;gap:var(--bui-space-2);display:flex}.bui-DataTablePagination--select{min-width:10.5rem}.bui-Tabs{--active-tab-left:0px;--active-tab-right:0px;--active-tab-top:0px;--active-tab-bottom:0px;--active-tab-width:0px;--active-tab-height:0px;--active-transition-duration:0s;--hovered-tab-left:0px;--hovered-tab-right:0px;--hovered-tab-top:0px;--hovered-tab-bottom:0px;--hovered-tab-width:0px;--hovered-tab-height:0px;--hovered-tab-opacity:0;--hovered-transition-duration:0s}.bui-TabList{flex-direction:row;display:flex}.bui-TabListWrapper{position:relative}.bui-Tab{font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-secondary);cursor:pointer;z-index:2;height:36px;padding-inline:var(--bui-space-2);justify-content:center;align-items:center;display:flex;position:relative;&[data-selected=true]{color:var(--bui-fg-primary)}}.bui-TabActive{content:"";left:calc(var(--active-tab-left) + var(--bui-space-2));width:calc(var(--active-tab-width) - var(--bui-space-4));background-color:var(--bui-fg-primary);height:1px;transition:left var(--active-transition-duration)ease-out,opacity .15s ease-out,width var(--active-transition-duration)ease-out;opacity:1;border-radius:4px;position:absolute;bottom:-1px}.bui-TabHovered{content:"";left:var(--hovered-tab-left);top:calc(var(--hovered-tab-top) + 4px);width:var(--hovered-tab-width);height:calc(var(--hovered-tab-height) - 8px);background-color:var(--bui-gray-2);opacity:var(--hovered-tab-opacity);transition:left var(--hovered-transition-duration)ease-out,top var(--hovered-transition-duration)ease-out,width var(--hovered-transition-duration)ease-out,height var(--hovered-transition-duration)ease-out,opacity .15s ease-out;border-radius:4px;position:absolute}.bui-TabPanel{padding-inline:var(--bui-space-2);padding-top:var(--bui-space-4)}.bui-TagList{gap:var(--bui-space-2);flex-wrap:wrap;display:flex}.bui-Tag{color:var(--bui-fg-primary);background-color:var(--bui-gray-2);border-radius:var(--bui-radius-2);font-weight:var(--bui-font-weight-regular);justify-content:center;align-items:center;gap:var(--bui-space-1);box-sizing:border-box;transition-property:background-color,box-shadow,color;transition-duration:.2s;transition-timing-function:ease-in-out;display:flex}.bui-Tag[data-size=small]{height:26px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-1)}.bui-Tag[data-size=medium]{height:32px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-2)}.bui-Tag[data-hovered]{background-color:var(--bui-gray-3);cursor:pointer}.bui-Tag[data-focus-visible]{outline:2px solid var(--bui-ring);outline-offset:1px}.bui-Tag[data-selected]{box-shadow:inset 0 0 0 1px var(--bui-gray-8)}.bui-Tag[data-disabled]{color:var(--bui-fg-disabled);cursor:not-allowed}.bui-TagRemoveButton{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;width:1rem;height:1rem;margin:0;padding:0}.bui-TagIcon{justify-content:center;align-items:center;transition:color .2s ease-in-out;display:flex;& svg{width:1rem;height:1rem}}.bui-Text{font-family:var(--bui-font-regular);margin:0;padding:0}.bui-Text[data-variant=title-large]{font-size:var(--bui-font-size-8);line-height:140%}.bui-Text[data-variant=title-medium]{font-size:var(--bui-font-size-7);line-height:140%}.bui-Text[data-variant=title-small]{font-size:var(--bui-font-size-6);line-height:140%}.bui-Text[data-variant=title-x-small]{font-size:var(--bui-font-size-5);line-height:140%}.bui-Text[data-variant=body-large]{font-size:var(--bui-font-size-4);line-height:140%}.bui-Text[data-variant=body-medium]{font-size:var(--bui-font-size-3);line-height:140%}.bui-Text[data-variant=body-small]{font-size:var(--bui-font-size-2);line-height:140%}.bui-Text[data-variant=body-x-small]{font-size:var(--bui-font-size-1);line-height:140%}.bui-Text[data-weight=regular]{font-weight:var(--bui-font-weight-regular)}.bui-Text[data-weight=bold]{font-weight:var(--bui-font-weight-bold)}.bui-Text[data-color=primary]{color:var(--bui-fg-primary)}.bui-Text[data-color=secondary]{color:var(--bui-fg-secondary)}.bui-Text[data-color=danger]{color:var(--bui-fg-danger)}.bui-Text[data-color=warning]{color:var(--bui-fg-warning)}.bui-Text[data-color=success]{color:var(--bui-fg-success)}.bui-Text[data-truncate]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.bui-Text[data-as=span],.bui-Text[data-as=label],.bui-Text[data-as=strong],.bui-Text[data-as=em],.bui-Text[data-as=small]{display:inline-block}.bui-TextField{font-family:var(--bui-font-regular);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputWrapper{position:relative;&[data-size=small] .bui-Input{height:2rem}&[data-size=medium] .bui-Input{height:2.5rem}&[data-size=small] .bui-Input[data-icon]{padding-left:var(--bui-space-8)}&[data-size=medium] .bui-Input[data-icon]{padding-left:var(--bui-space-9)}}.bui-InputIcon{left:var(--bui-space-3);margin-right:var(--bui-space-1);color:var(--bui-fg-primary);pointer-events:none;flex-shrink:0;transition:left .2s ease-in-out;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-Input{padding:0 var(--bui-space-3);border-radius:var(--bui-radius-2);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:100%;cursor:inherit;align-items:center;transition:border-color .2s ease-in-out,outline-color .2s ease-in-out;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}&::placeholder{color:var(--bui-fg-secondary)}&[data-focused]{outline-color:var(--bui-border-pressed);outline-width:0}&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-disabled]{opacity:.5;cursor:not-allowed;border:1px solid var(--bui-border-disabled)}}.bui-SearchField{&[data-empty]{& .bui-InputClear{display:none}}&[data-start-collapsed=true]{padding:0;transition:width .3s ease-in-out;&[data-collapsed=false]{cursor:pointer;&[data-size=medium]{width:2.5rem;height:2.5rem}&[data-size=small]{width:2rem;height:2rem}&[data-size=medium] .bui-Input{padding-left:0;&::placeholder{opacity:0}}&[data-size=small] .bui-Input{padding-left:0;&::placeholder{opacity:0}}&[data-size=small] .bui-InputIcon{left:var(--bui-space-2)}&[data-size=medium] .bui-InputIcon{left:10px}}}}.bui-InputClear{cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;right:0}.bui-InputClear:hover{color:var(--bui-fg-primary)}.bui-InputClear[data-size=small]{width:2rem;height:2rem}.bui-InputClear[data-size=medium]{width:2.5rem;height:2.5rem}.bui-InputClear svg{width:1rem;height:1rem}.bui-Skeleton{animation:var(--bui-animate-pulse);background-color:var(--bui-bg-surface-2);border-radius:var(--bui-radius-2)}.bui-Skeleton[data-rounded=true]{border-radius:var(--bui-radius-full)}.bui-Tooltip{background:var(--bui-bg-surface-1);border:1px solid var(--bui-gray-3);forced-color-adjust:none;padding:var(--bui-space-2)var(--bui-space-3);max-width:240px;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);color:var(--bui-fg-primary);border-radius:4px;outline:none;transition:transform .2s,opacity .2s;transform:translate(0,0);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}--tooltip-offset:var(--bui-space-3)&[data-placement=top]{margin-bottom:var(--tooltip-offset);--origin:translateY(4px)}&[data-placement=right]{margin-left:var(--tooltip-offset);--origin:translateX(-4px)}&[data-placement=bottom]{margin-top:var(--tooltip-offset);--origin:translateY(-4px)}&[data-placement=left]{margin-right:var(--tooltip-offset);--origin:translateX(4px)}}.bui-TooltipArrow{& svg{display:block;& path:first-child{fill:var(--bui-bg-surface-1)}& path:nth-child(2){fill:var(--bui-gray-3)}--tooltip-arrow-overlap:-2px}&[data-placement=top] svg{margin-top:var(--tooltip-arrow-overlap)}&[data-placement=bottom] svg{margin-bottom:var(--tooltip-arrow-overlap);transform:rotate(180deg)}&[data-placement=right] svg{margin-right:var(--tooltip-arrow-overlap);transform:rotate(90deg)}&[data-placement=left] svg{margin-left:var(--tooltip-arrow-overlap);transform:rotate(-90deg)}}[data-theme=dark]{& .bui-Tooltip{background:var(--bui-bg-surface-2);box-shadow:none;border:1px solid var(--bui-gray-4)}& .bui-TooltipArrow{& svg path:first-child{fill:var(--bui-bg-surface-2)}& svg path:nth-child(2){fill:var(--bui-gray-4)}}}.bui-ScrollAreaRoot{box-sizing:border-box;width:100%}.bui-ScrollAreaViewport{overscroll-behavior:contain;height:100%}.bui-ScrollAreaContent{padding-block:.75rem;flex-direction:column;gap:1rem;padding-left:1rem;padding-right:1.5rem;display:flex}.bui-ScrollAreaScrollbar{background-color:var(--bui-scrollbar);opacity:0;border-radius:.375rem;justify-content:center;width:.25rem;margin:.5rem;transition:opacity .15s .3s;display:flex;&[data-hovering],&[data-scrolling]{opacity:1;transition-duration:75ms;transition-delay:0s}&:before{content:"";width:1.25rem;height:100%;position:absolute}}.bui-ScrollAreaThumb{border-radius:inherit;background-color:var(--bui-scrollbar-thumb);width:100%}.bui-Select[data-invalid]{& .bui-SelectTrigger{border-color:var(--bui-fg-danger)}}.bui-SelectTrigger{box-sizing:border-box;border-radius:var(--bui-radius-3);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);cursor:pointer;justify-content:space-between;align-items:center;gap:var(--bui-space-2);width:100%;display:flex;& svg{color:var(--bui-fg-secondary);flex-shrink:0}&[data-size=small]{height:2rem;padding-inline:var(--bui-space-3)}&[data-size=medium]{height:3rem;padding-inline:var(--bui-space-4)}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}&::placeholder{color:var(--bui-fg-secondary)}&:hover{border-color:var(--bui-border-hover);transition:border-color .2s ease-in-out,outline-color .2s ease-in-out}&:focus-visible{border-color:var(--bui-border-pressed);outline:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-invalid]:hover,&[data-invalid]:focus-visible{border-width:2px}&[disabled]{cursor:not-allowed;border-color:var(--bui-border-disabled);color:var(--bui-fg-disabled)}&[disabled] .bui-SelectValue{color:var(--bui-fg-disabled)}&[data-popup-open] .bui-SelectIcon{transform:rotate(180deg)}}.bui-SelectValue{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);text-align:left;overflow:hidden;& .bui-SelectItemIndicator{display:none}&[disabled]{color:var(--bui-fg-disabled)}}.bui-SelectItem{width:var(--anchor-width);padding-block:var(--bui-space-2);padding-left:var(--bui-space-3);padding-right:var(--bui-space-4);color:var(--bui-fg-primary);border-radius:var(--bui-radius-3);cursor:pointer;user-select:none;font-size:var(--bui-font-size-3);align-items:center;gap:var(--bui-space-1);outline:none;grid-template-columns:1rem 1fr;grid-template-areas:"icon text";display:grid;position:relative;&[data-focused]{z-index:0;color:var(--bui-fg-primary);position:relative}&[data-focused]:before{content:"";z-index:-1;background-color:var(--bui-bg-tint-hover);border-radius:.25rem;position:absolute;inset-block:0;inset-inline:.25rem}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled)}&[data-selected] .bui-SelectItemIndicator{opacity:1}}.bui-SelectItemIndicator{opacity:0;grid-area:icon;justify-content:center;align-items:center;transition:opacity .2s ease-in-out;display:flex}.bui-SelectItemLabel{flex:1;grid-area:text}.bui-Switch{align-items:center;gap:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary);cursor:pointer;display:flex;position:relative;&[data-pressed] .bui-SwitchIndicator{&:before{background:var(--bui-fg-solid)}}&[data-selected]{& .bui-SwitchIndicator{background:var(--bui-bg-solid);&:before{background:var(--bui-fg-solid);transform:translate(100%)}}&[data-pressed]{& .indicator{background:var(--bui-gray-3)}}}&[data-focus-visible] .bui-SwitchIndicator{outline-offset:2px;outline:2px solid}}.bui-SwitchIndicator{background:var(--bui-gray-3);border:2px;border-radius:1.143rem;width:2rem;height:1.143rem;transition:all .2s;&:before{content:"";background:var(--bui-fg-solid);border-radius:16px;width:.857rem;height:.857rem;margin:.143rem;transition:all .2s;display:block}} \ No newline at end of file +@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}}.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--pl-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--pl-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--pl-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--pl-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--pl-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--pr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--pr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--pr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--pr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--pr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--pt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--pt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--pt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--pt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--pt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--pb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--pb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--pb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--pb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--pb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--py-xs);margin-bottom:var(--py-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--py-sm);margin-bottom:var(--py-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--py-md);margin-bottom:var(--py-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--py-lg);margin-bottom:var(--py-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--py-xl);margin-bottom:var(--py-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--px-xs);margin-right:var(--px-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--px-sm);margin-right:var(--px-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--px-md);margin-right:var(--px-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--px-lg);margin-right:var(--px-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--px-xl);margin-right:var(--px-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-baseline{align-items:baseline}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#ebebeb;--bui-bg-tint:transparent;--bui-bg-tint-hover:#1f549366;--bui-bg-tint-pressed:#1f549399;--bui-bg-tint-disabled:#ebebeb;--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#9c9c9c;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg:#333;--bui-bg-surface-1:#424242;--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#222;--bui-bg-tint:transparent;--bui-bg-tint-hover:#9cc9ff1f;--bui-bg-tint-pressed:#9cc9ff29;--bui-bg-tint-disabled:transparent;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:var(--bui-gray-5);--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757}.bui-AvatarRoot{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:2rem;height:2rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-AvatarRoot[data-size=small]{width:1.5rem;height:1.5rem}.bui-AvatarRoot[data-size=medium]{width:2rem;height:2rem}.bui-AvatarRoot[data-size=large]{width:3rem;height:3rem}.bui-AvatarImage{object-fit:cover;width:100%;height:100%}.bui-AvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-Box{font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary)}.bui-Button{user-select:none;font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-bold);cursor:pointer;border-radius:var(--bui-radius-2);justify-content:center;align-items:center;gap:var(--bui-space-1_5);border:none;flex-shrink:0;padding:0;display:inline-flex;&[data-disabled=true]{cursor:not-allowed}}.bui-Button[data-variant=primary]{background-color:var(--bui-bg-solid);color:var(--bui-fg-solid);&:hover{background-color:var(--bui-bg-solid-hover);transition:background-color .15s}&:active{background-color:var(--bui-bg-solid-pressed)}&:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled=true]{background-color:var(--bui-bg-solid-disabled);color:var(--bui-fg-solid-disabled)}}.bui-Button[data-variant=secondary]{background-color:var(--bui-bg-surface-1);box-shadow:inset 0 0 0 1px var(--bui-border);color:var(--bui-fg-primary);&:hover{box-shadow:inset 0 0 0 1px var(--bui-border-hover);transition:box-shadow .15s}&:active{box-shadow:inset 0 0 0 1px var(--bui-border-pressed)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{box-shadow:inset 0 0 0 1px var(--bui-border-disabled);color:var(--bui-fg-disabled)}}.bui-Button[data-variant=tertiary]{color:var(--bui-fg-primary);background-color:#0000;&:hover{background-color:var(--bui-bg-surface-1);transition:background-color .2s}&:active{background-color:var(--bui-bg-surface-2)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{color:var(--bui-fg-disabled);background-color:#0000}}.bui-Button[data-size=medium]{font-size:var(--bui-font-size-4);padding:0 var(--bui-space-3);height:2.5rem}.bui-Button[data-size=small]{font-size:var(--bui-font-size-3);padding:0 var(--bui-space-2);height:2rem}.bui-Button[data-size=small] svg{width:1rem;height:1rem}.bui-Button[data-size=medium] svg{width:1.25rem;height:1.25rem}.bui-ButtonIcon{justify-content:center;align-items:center}.bui-ButtonIcon[data-size=small]{width:2rem;padding:0}.bui-ButtonIcon[data-size=medium]{width:2.5rem;padding:0}.bui-Card{gap:var(--bui-space-3);background-color:var(--bui-bg-surface-1);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-3);color:var(--bui-fg-primary);border:1px solid var(--bui-border);flex-direction:column;width:100%;min-height:0;display:flex;overflow:hidden}.bui-CardBody{flex:1;min-height:0;overflow:auto}.bui-CardHeader,.bui-CardFooter{padding-inline:var(--bui-space-3)}.bui-CheckboxRoot{width:1rem;height:1rem;box-shadow:inset 0 0 0 1px var(--bui-border);cursor:pointer;background-color:var(--bui-bg-surface-1);border:none;border-radius:2px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background-color .2s ease-in-out;display:flex}.bui-CheckboxRoot:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px;transition:none}.bui-CheckboxRoot[data-checked]{background-color:var(--bui-bg-solid);box-shadow:none;color:var(--bui-fg-solid)}.bui-CheckboxLabel{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);user-select:none;flex-direction:row;display:flex;&:hover{& .bui-CheckboxRoot:not([data-checked]){box-shadow:inset 0 0 0 1px var(--bui-border-hover)}}}.bui-CheckboxIndicator{color:var(--bui-fg-solid);justify-content:center;align-items:center;display:flex}.bui-CollapsiblePanel{height:var(--collapsible-panel-height);transition:all .15s ease-out;display:flex;overflow:hidden;&[data-starting-style],&[data-ending-style]{height:0}}.bui-Container{max-width:120rem;padding-inline:var(--bui-space-4);margin-inline:auto;transition:padding .2s ease-in-out}@media (width>=640px){.bui-Container{padding-inline:var(--bui-space-5)}}.bui-FieldError{color:var(--bui-fg-danger);font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);margin-top:var(--bui-space-2);display:inline-block}.bui-FieldLabelWrapper{margin-bottom:var(--bui-space-3);gap:var(--bui-space-1);flex-direction:column;display:flex}.bui-FieldLabel{color:var(--bui-fg-primary);cursor:pointer;font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);margin-right:auto}.bui-FieldSecondaryLabel{color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular);margin-left:var(--bui-space-1)}.bui-FieldDescription{font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);margin:0}.bui-Flex{min-width:0;display:flex}.bui-Grid{display:grid}.bui-HeaderToolbar{margin-bottom:var(--bui-space-6);&:before{content:"";background-color:var(--bui-bg);z-index:0;height:16px;position:absolute;top:0;left:0;right:0}&[data-has-tabs=true]{margin-bottom:0}}.bui-HeaderToolbarWrapper{z-index:1;background-color:var(--bui-bg-surface-1);padding-inline:var(--bui-space-5);border-bottom:1px solid var(--bui-border);color:var(--bui-fg-primary);flex-direction:row;justify-content:space-between;align-items:center;height:52px;display:flex;position:relative}.bui-HeaderToolbarContent{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-HeaderToolbarName{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);flex-direction:row;flex-shrink:0;display:flex}.bui-HeaderToolbarIcon{width:16px;height:16px;color:var(--bui-fg-primary);& svg{width:100%;height:100%}}.bui-HeaderToolbarControls{right:var(--bui-space-5);align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.bui-HeaderTabsWrapper{margin-bottom:var(--bui-space-4);padding-inline:var(--bui-space-3);border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1)}.bui-HeaderPage{gap:var(--bui-space-1);margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6);flex-direction:column;display:flex}.bui-HeaderPageContent{flex-direction:row;justify-content:space-between;display:flex}.bui-HeaderPageTabsWrapper{margin-left:-8px}.bui-HeaderPageControls,.bui-HeaderPageBreadcrumbs{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-Icon{width:1rem;height:1rem}.bui-Link{font-family:var(--bui-font-regular);cursor:pointer;margin:0;padding:0;text-decoration-line:none;display:inline-block;&:hover{text-underline-offset:calc(.025em + 2px);text-decoration-line:underline;text-decoration-style:solid;text-decoration-thickness:min(2px,max(1px,.05em));text-decoration-color:color-mix(in srgb,currentColor 30%,transparent)}}.bui-MenuPopover{border:1px solid var(--bui-border);border-radius:var(--bui-radius-2);background:var(--bui-bg-surface-1);color:var(--bui-fg-primary);outline:none;flex-direction:column;min-height:0;transition:transform .2s,opacity .2s;display:flex;overflow:hidden;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}&[data-placement=top]{--origin:translateY(8px)}&[data-placement=bottom]{--origin:translateY(-8px)}&[data-placement=right]{--origin:translateX(-8px)}&[data-placement=left]{--origin:translateX(8px)}}.bui-MenuContent{max-height:inherit;box-sizing:border-box;padding:var(--bui-space-1);outline:none;min-width:150px;overflow:auto}.bui-MenuPopover .bui-ScrollAreaRoot{flex-direction:column;flex:1;height:100%;min-height:0;display:flex}.bui-MenuPopover .bui-ScrollAreaScrollbar{margin-inline:var(--bui-space-1_5)}.bui-MenuItem{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&[data-focused],&[data-open]{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-color=danger]{color:var(--bui-fg-danger)}&[data-color=danger][data-focused]{background:var(--bui-bg-danger);color:var(--bui-fg-danger)}&[data-has-submenu]{&>.bui-MenuItemArrow{display:block}}}.bui-MenuItemListBox{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&:hover{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-selected] .bui-MenuItemListBoxCheck{&>svg{opacity:1;color:var(--bui-fg-primary)}}}.bui-MenuItemListBoxCheck{justify-content:center;align-items:center;width:1rem;height:1rem;display:flex;&>svg{opacity:0;width:1rem;height:1rem}}.bui-MenuItemContent{align-items:center;gap:var(--bui-space-2);display:flex;&>svg{width:1rem;height:1rem}}.bui-MenuItemArrow{width:1rem;height:1rem;display:none;&>svg{width:1rem;height:1rem}}.bui-MenuSection{&:first-child .bui-MenuSectionHeader{padding-top:0}}.bui-MenuSectionHeader{height:2rem;padding-top:var(--bui-space-3);padding-left:var(--bui-space-2);color:var(--bui-fg-primary);font-size:var(--bui-font-size-1);letter-spacing:.05rem;text-transform:uppercase;align-items:center;font-weight:700;display:flex}.bui-MenuSeparator{background:var(--bui-border);height:1px;margin-inline:var(--bui-space-1_5);margin-block:var(--bui-space-1)}.bui-MenuSearchField{font-family:var(--bui-font-regular);flex-shrink:0;width:100%;position:relative;&[data-empty]{& .bui-MenuSearchFieldClear{display:none}}}.bui-MenuSearchFieldInput{padding:0 var(--bui-space-3);border:none;border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:2rem;cursor:inherit;outline:none;align-items:center;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}}.bui-MenuSearchFieldClear{right:var(--bui-space-2);cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;&>svg{width:1rem;height:1rem}}.bui-MenuEmptyState{padding:var(--bui-space-1);color:var(--bui-fg-secondary);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular)}.bui-Popover{background-color:var(--bui-bg-surface-1);border:1px solid var(--bui-border);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-1);margin-right:12px;overflow:scroll;box-shadow:0 4px 12px #0000001a}.bui-RadioGroup{color:var(--bui-fg-primary);flex-direction:column;display:flex}.bui-RadioGroup[data-orientation=horizontal] .bui-RadioGroupContent{gap:var(--bui-space-4);flex-direction:row}.bui-RadioGroupContent{gap:var(--bui-space-2);flex-direction:column;display:flex}.bui-Radio{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-2);color:var(--bui-fg-primary);forced-color-adjust:none;display:flex;position:relative;&:before{content:"";box-sizing:border-box;border:.125rem solid var(--bui-border);background:var(--bui-gray-1);border-radius:var(--bui-radius-full);width:1rem;height:1rem;transition:all .2s;display:block}&[data-pressed]:before{border-color:var(--bui-border)}&[data-selected]{&:before{border-color:var(--bui-bg-solid);border-width:.25rem}&[data-pressed]:before{border-color:var(--bui-bg-solid)}}&[data-focus-visible]:before{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}&[data-invalid]:before,&[data-invalid][data-selected]:before{border-color:var(--bui-border-danger)}&[data-disabled][data-invalid]{color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}}.bui-Table{caption-side:bottom;border-collapse:collapse;width:100%}.bui-TableHeader{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out}.bui-TableHead{text-align:left;padding:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary)}.bui-TableHeadSortButton{cursor:pointer;user-select:none;align-items:center;gap:var(--bui-space-1);display:inline-flex;&:hover svg{opacity:.5}& svg{opacity:0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}&[data-sort-order=asc] svg{opacity:1;transform:rotate(0)}&[data-sort-order=desc] svg{opacity:1;transform:rotate(180deg)}}.bui-TableBody{color:var(--bui-fg-primary)}.bui-TableRow{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out;&[data-react-aria-pressable=true]{cursor:pointer}}.bui-TableBody .bui-TableRow:hover{background-color:var(--bui-gray-2)}.bui-TableCell{padding:var(--bui-space-3);font-size:var(--bui-font-size-3);padding:var(--bui-space-3);font-size:var(--bui-font-size-3)}.bui-TableCellContentWrapper{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:inline-flex}.bui-TableCellIcon,.bui-TableCellIcon svg{color:var(--bui-fg-primary);align-items:center;display:inline-flex}.bui-TableCellContent{gap:var(--bui-space-0_5);flex-direction:column;display:flex}.bui-TableCellProfile{gap:var(--bui-space-2);flex-direction:row;align-items:center;display:flex}.bui-TableCellProfileAvatar{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-TableCellProfileAvatarImage{object-fit:cover;width:100%;height:100%}.bui-TableCellProfileAvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-DataTablePagination{padding-top:var(--bui-space-5);justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--left{justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--right{justify-content:space-between;align-items:center;gap:var(--bui-space-2);display:flex}.bui-DataTablePagination--select{min-width:10.5rem}.bui-Tabs{--active-tab-left:0px;--active-tab-right:0px;--active-tab-top:0px;--active-tab-bottom:0px;--active-tab-width:0px;--active-tab-height:0px;--active-transition-duration:0s;--hovered-tab-left:0px;--hovered-tab-right:0px;--hovered-tab-top:0px;--hovered-tab-bottom:0px;--hovered-tab-width:0px;--hovered-tab-height:0px;--hovered-tab-opacity:0;--hovered-transition-duration:0s}.bui-TabList{flex-direction:row;display:flex}.bui-TabListWrapper{position:relative}.bui-Tab{font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-secondary);cursor:pointer;z-index:2;height:36px;padding-inline:var(--bui-space-2);justify-content:center;align-items:center;display:flex;position:relative;&[data-selected=true]{color:var(--bui-fg-primary)}}.bui-TabActive{content:"";left:calc(var(--active-tab-left) + var(--bui-space-2));width:calc(var(--active-tab-width) - var(--bui-space-4));background-color:var(--bui-fg-primary);height:1px;transition:left var(--active-transition-duration)ease-out,opacity .15s ease-out,width var(--active-transition-duration)ease-out;opacity:1;border-radius:4px;position:absolute;bottom:-1px}.bui-TabHovered{content:"";left:var(--hovered-tab-left);top:calc(var(--hovered-tab-top) + 4px);width:var(--hovered-tab-width);height:calc(var(--hovered-tab-height) - 8px);background-color:var(--bui-gray-2);opacity:var(--hovered-tab-opacity);transition:left var(--hovered-transition-duration)ease-out,top var(--hovered-transition-duration)ease-out,width var(--hovered-transition-duration)ease-out,height var(--hovered-transition-duration)ease-out,opacity .15s ease-out;border-radius:4px;position:absolute}.bui-TabPanel{padding-inline:var(--bui-space-2);padding-top:var(--bui-space-4)}.bui-TagList{gap:var(--bui-space-2);flex-wrap:wrap;display:flex}.bui-Tag{color:var(--bui-fg-primary);background-color:var(--bui-gray-2);border-radius:var(--bui-radius-2);font-weight:var(--bui-font-weight-regular);justify-content:center;align-items:center;gap:var(--bui-space-1);box-sizing:border-box;transition-property:background-color,box-shadow,color;transition-duration:.2s;transition-timing-function:ease-in-out;display:flex}.bui-Tag[data-size=small]{height:26px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-1)}.bui-Tag[data-size=medium]{height:32px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-2)}.bui-Tag[data-hovered]{background-color:var(--bui-gray-3);cursor:pointer}.bui-Tag[data-focus-visible]{outline:2px solid var(--bui-ring);outline-offset:1px}.bui-Tag[data-selected]{box-shadow:inset 0 0 0 1px var(--bui-gray-8)}.bui-Tag[data-disabled]{color:var(--bui-fg-disabled);cursor:not-allowed}.bui-TagRemoveButton{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;width:1rem;height:1rem;margin:0;padding:0}.bui-TagIcon{justify-content:center;align-items:center;transition:color .2s ease-in-out;display:flex;& svg{width:1rem;height:1rem}}.bui-Text{font-family:var(--bui-font-regular);margin:0;padding:0}.bui-Text[data-variant=title-large]{font-size:var(--bui-font-size-8);line-height:140%}.bui-Text[data-variant=title-medium]{font-size:var(--bui-font-size-7);line-height:140%}.bui-Text[data-variant=title-small]{font-size:var(--bui-font-size-6);line-height:140%}.bui-Text[data-variant=title-x-small]{font-size:var(--bui-font-size-5);line-height:140%}.bui-Text[data-variant=body-large]{font-size:var(--bui-font-size-4);line-height:140%}.bui-Text[data-variant=body-medium]{font-size:var(--bui-font-size-3);line-height:140%}.bui-Text[data-variant=body-small]{font-size:var(--bui-font-size-2);line-height:140%}.bui-Text[data-variant=body-x-small]{font-size:var(--bui-font-size-1);line-height:140%}.bui-Text[data-weight=regular]{font-weight:var(--bui-font-weight-regular)}.bui-Text[data-weight=bold]{font-weight:var(--bui-font-weight-bold)}.bui-Text[data-color=primary]{color:var(--bui-fg-primary)}.bui-Text[data-color=secondary]{color:var(--bui-fg-secondary)}.bui-Text[data-color=danger]{color:var(--bui-fg-danger)}.bui-Text[data-color=warning]{color:var(--bui-fg-warning)}.bui-Text[data-color=success]{color:var(--bui-fg-success)}.bui-Text[data-truncate]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.bui-Text[data-as=span],.bui-Text[data-as=label],.bui-Text[data-as=strong],.bui-Text[data-as=em],.bui-Text[data-as=small]{display:inline-block}.bui-TextField{font-family:var(--bui-font-regular);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-PasswordField{font-family:var(--bui-font-regular);--bui-passwordmanager-icon-width:var(--bui-space-1);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputWrapper{position:relative;&[data-size=small] .bui-Input{height:2rem}&[data-size=medium] .bui-Input{height:2.5rem}&[data-size=small] .bui-Input[data-icon]{padding-left:var(--bui-space-8)}&[data-size=medium] .bui-Input[data-icon]{padding-left:var(--bui-space-9)}}.bui-InputAction{right:var(--bui-space-1);color:var(--bui-fg-primary);flex-direction:row;flex-shrink:0;transition:right .2s ease-in-out;display:flex;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-InputVisibility{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;display:flex;&[data-size=small]{width:2rem;height:2rem}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium]{width:2.5rem;height:2.5rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-PasswordField .bui-InputWrapper[data-size=small] .bui-Input{padding-right:calc(2rem + var(--bui-passwordmanager-icon-width))}.bui-PasswordField .bui-InputWrapper[data-size=medium] .bui-Input{padding-right:calc(2.5rem + var(--bui-passwordmanager-icon-width))}.bui-InputIcon{left:var(--bui-space-3);margin-right:var(--bui-space-1);color:var(--bui-fg-primary);pointer-events:none;flex-shrink:0;transition:left .2s ease-in-out;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-Input{padding:0 var(--bui-space-3);border-radius:var(--bui-radius-2);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:100%;cursor:inherit;align-items:center;transition:border-color .2s ease-in-out,outline-color .2s ease-in-out;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}&::placeholder{color:var(--bui-fg-secondary)}&[data-focused]{outline-color:var(--bui-border-pressed);outline-width:0}&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-disabled]{opacity:.5;cursor:not-allowed;border:1px solid var(--bui-border-disabled)}}.bui-SearchField{flex:1 0;&[data-empty]{& .bui-InputClear{display:none}}&[data-start-collapsed=true]{flex:0 auto;padding:0;transition:flex-basis .3s ease-in-out;&[data-collapsed=true]{flex-basis:200px}&[data-collapsed=false]{cursor:pointer;&[data-size=medium]{flex-basis:2.5rem;height:2.5rem}&[data-size=small]{flex-basis:2rem;height:2rem}&[data-size=medium] .bui-Input{&::placeholder{opacity:0}}&[data-size=small] .bui-Input{&::placeholder{opacity:0}}& .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:0}}}}}.bui-SearchField .bui-Input{transition:padding .3s ease-in-out,border-color .2s ease-in-out,outline-color .2s ease-in-out;&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}}.bui-SearchField .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:var(--bui-space-6)}}.bui-SearchField .bui-InputIcon{justify-content:center;display:flex;left:0;&[data-size=small]{width:var(--bui-space-8)}&[data-size=medium]{width:var(--bui-space-10)}}.bui-InputClear{cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;right:0}.bui-InputClear:hover{color:var(--bui-fg-primary)}.bui-InputClear[data-size=small]{width:2rem;height:2rem}.bui-InputClear[data-size=medium]{width:2.5rem;height:2.5rem}.bui-InputClear svg{width:1rem;height:1rem}.bui-Skeleton{animation:var(--bui-animate-pulse);background-color:var(--bui-bg-surface-2);border-radius:var(--bui-radius-2)}.bui-Skeleton[data-rounded=true]{border-radius:var(--bui-radius-full)}.bui-Tooltip{background:var(--bui-bg-surface-1);border:1px solid var(--bui-gray-3);forced-color-adjust:none;padding:var(--bui-space-2)var(--bui-space-3);max-width:240px;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);color:var(--bui-fg-primary);border-radius:4px;outline:none;transition:transform .2s,opacity .2s;transform:translate(0,0);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}--tooltip-offset:var(--bui-space-3)&[data-placement=top]{margin-bottom:var(--tooltip-offset);--origin:translateY(4px)}&[data-placement=right]{margin-left:var(--tooltip-offset);--origin:translateX(-4px)}&[data-placement=bottom]{margin-top:var(--tooltip-offset);--origin:translateY(-4px)}&[data-placement=left]{margin-right:var(--tooltip-offset);--origin:translateX(4px)}}.bui-TooltipArrow{& svg{display:block;& path:first-child{fill:var(--bui-bg-surface-1)}& path:nth-child(2){fill:var(--bui-gray-3)}--tooltip-arrow-overlap:-2px}&[data-placement=top] svg{margin-top:var(--tooltip-arrow-overlap)}&[data-placement=bottom] svg{margin-bottom:var(--tooltip-arrow-overlap);transform:rotate(180deg)}&[data-placement=right] svg{margin-right:var(--tooltip-arrow-overlap);transform:rotate(90deg)}&[data-placement=left] svg{margin-left:var(--tooltip-arrow-overlap);transform:rotate(-90deg)}}[data-theme=dark]{& .bui-Tooltip{background:var(--bui-bg-surface-2);box-shadow:none;border:1px solid var(--bui-gray-4)}& .bui-TooltipArrow{& svg path:first-child{fill:var(--bui-bg-surface-2)}& svg path:nth-child(2){fill:var(--bui-gray-4)}}}.bui-ScrollAreaRoot{box-sizing:border-box;width:100%}.bui-ScrollAreaViewport{overscroll-behavior:contain;height:100%}.bui-ScrollAreaContent{padding-block:.75rem;flex-direction:column;gap:1rem;padding-left:1rem;padding-right:1.5rem;display:flex}.bui-ScrollAreaScrollbar{background-color:var(--bui-scrollbar);opacity:0;border-radius:.375rem;justify-content:center;width:.25rem;margin:.5rem;transition:opacity .15s .3s;display:flex;&[data-hovering],&[data-scrolling]{opacity:1;transition-duration:75ms;transition-delay:0s}&:before{content:"";width:1.25rem;height:100%;position:absolute}}.bui-ScrollAreaThumb{border-radius:inherit;background-color:var(--bui-scrollbar-thumb);width:100%}.bui-Select[data-invalid]{& .bui-SelectTrigger{border-color:var(--bui-fg-danger)}}.bui-SelectTrigger{box-sizing:border-box;border-radius:var(--bui-radius-3);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);cursor:pointer;justify-content:space-between;align-items:center;gap:var(--bui-space-2);width:100%;display:flex;& svg{color:var(--bui-fg-secondary);flex-shrink:0}&[data-size=small]{height:2rem;padding-inline:var(--bui-space-3)}&[data-size=medium]{height:3rem;padding-inline:var(--bui-space-4)}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}&::placeholder{color:var(--bui-fg-secondary)}&:hover{border-color:var(--bui-border-hover);transition:border-color .2s ease-in-out,outline-color .2s ease-in-out}&:focus-visible{border-color:var(--bui-border-pressed);outline:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-invalid]:hover,&[data-invalid]:focus-visible{border-width:2px}&[disabled]{cursor:not-allowed;border-color:var(--bui-border-disabled);color:var(--bui-fg-disabled)}&[disabled] .bui-SelectValue{color:var(--bui-fg-disabled)}&[data-popup-open] .bui-SelectIcon{transform:rotate(180deg)}}.bui-SelectValue{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);text-align:left;overflow:hidden;& .bui-SelectItemIndicator{display:none}&[disabled]{color:var(--bui-fg-disabled)}}.bui-SelectItem{width:var(--anchor-width);padding-block:var(--bui-space-2);padding-left:var(--bui-space-3);padding-right:var(--bui-space-4);color:var(--bui-fg-primary);border-radius:var(--bui-radius-3);cursor:pointer;user-select:none;font-size:var(--bui-font-size-3);align-items:center;gap:var(--bui-space-1);outline:none;grid-template-columns:1rem 1fr;grid-template-areas:"icon text";display:grid;position:relative;&[data-focused]{z-index:0;color:var(--bui-fg-primary);position:relative}&[data-focused]:before{content:"";z-index:-1;background-color:var(--bui-bg-tint-hover);border-radius:.25rem;position:absolute;inset-block:0;inset-inline:.25rem}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled)}&[data-selected] .bui-SelectItemIndicator{opacity:1}}.bui-SelectItemIndicator{opacity:0;grid-area:icon;justify-content:center;align-items:center;transition:opacity .2s ease-in-out;display:flex}.bui-SelectItemLabel{flex:1;grid-area:text}.bui-Switch{align-items:center;gap:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary);cursor:pointer;display:flex;position:relative;&[data-pressed] .bui-SwitchIndicator{&:before{background:var(--bui-fg-solid)}}&[data-selected]{& .bui-SwitchIndicator{background:var(--bui-bg-solid);&:before{background:var(--bui-fg-solid);transform:translate(100%)}}&[data-pressed]{& .indicator{background:var(--bui-gray-3)}}}&[data-focus-visible] .bui-SwitchIndicator{outline-offset:2px;outline:2px solid}}.bui-SwitchIndicator{background:var(--bui-gray-3);border:2px;border-radius:1.143rem;width:2rem;height:1.143rem;transition:all .2s;&:before{content:"";background:var(--bui-fg-solid);border-radius:16px;width:.857rem;height:.857rem;margin:.143rem;transition:all .2s;display:block}} \ No newline at end of file diff --git a/docs-ui/src/app/page.mdx b/docs-ui/src/app/page.mdx index f31cf4524e..ffd3bc9dcb 100644 --- a/docs-ui/src/app/page.mdx +++ b/docs-ui/src/app/page.mdx @@ -85,6 +85,11 @@ building it incrementally with not conflict with the existing theming system. description="A search field component to help you search for items." href="/components/search-field" /> + + +} + code={passwordFieldDefaultSnippet} +/> + +## Usage + + + +## API reference + + + +## Examples + +### Sizes + +We support two different sizes: `small`, `medium`. + +} + code={passwordFieldSizesSnippet} +/> + +### With description + +Here's a simple PasswordField with a description. + +} + code={passwordFieldDescriptionSnippet} +/> + + + + diff --git a/docs-ui/src/content/components/password-field.props.ts b/docs-ui/src/content/components/password-field.props.ts new file mode 100644 index 0000000000..493405b5fd --- /dev/null +++ b/docs-ui/src/content/components/password-field.props.ts @@ -0,0 +1,43 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const inputPropDefs: Record = { + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'small', + responsive: true, + }, + label: { + type: 'string', + }, + icon: { + type: 'enum', + values: ['ReactNode'], + }, + description: { + type: 'string', + }, + name: { + type: 'string', + required: true, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui'; + +`; + +export const passwordFieldDefaultSnippet = ``; + +export const passwordFieldSizesSnippet = ` + } /> + } /> +`; + +export const passwordFieldDescriptionSnippet = ``; diff --git a/docs-ui/src/snippets/stories-snippets.tsx b/docs-ui/src/snippets/stories-snippets.tsx index 5aa5b8c7d9..5bba3384b5 100644 --- a/docs-ui/src/snippets/stories-snippets.tsx +++ b/docs-ui/src/snippets/stories-snippets.tsx @@ -28,6 +28,7 @@ import * as HeaderStories from '../../../packages/ui/src/components/Header/Heade import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories'; import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories'; import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories'; +import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories'; // Helper function to create snippet components // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -53,6 +54,8 @@ export const ContainerSnippet = createSnippetComponent(ContainerStories); export const GridSnippet = createSnippetComponent(GridStories); export const IconSnippet = createSnippetComponent(IconStories); export const TextFieldSnippet = createSnippetComponent(TextFieldStories); +export const PasswordFieldSnippet = + createSnippetComponent(PasswordFieldStories); export const TextSnippet = createSnippetComponent(TextStories); export const FlexSnippet = createSnippetComponent(FlexStories); export const SelectSnippet = createSnippetComponent(SelectStories); diff --git a/docs-ui/src/utils/changelog.ts b/docs-ui/src/utils/changelog.ts index a66997cf26..ef2e6c9fb9 100644 --- a/docs-ui/src/utils/changelog.ts +++ b/docs-ui/src/utils/changelog.ts @@ -25,7 +25,8 @@ export type Component = | 'radio-group' | 'card' | 'skeleton' - | 'header'; + | 'header' + | 'passwordfield'; export type Version = `${number}.${number}.${number}`; diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index 5e7305da28..c1a69e459b 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -126,6 +126,11 @@ export const components: Page[] = [ slug: 'menu', status: 'alpha', }, + { + title: 'PasswordField', + slug: 'password-field', + status: 'alpha', + }, { title: 'RadioGroup', slug: 'radio-group', diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index 2816ed7e8f..2d3397f4a1 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -1,6 +1,9 @@ /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ @layer base { - *, :before, :after { + + *, + :before, + :after { box-sizing: border-box; } @@ -15,11 +18,15 @@ margin: 0; } - b, strong { + b, + strong { font-weight: bolder; } - code, kbd, samp, pre { + code, + kbd, + samp, + pre { font-family: ui-monospace, SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; font-size: 1em; } @@ -28,7 +35,8 @@ font-size: 80%; } - sub, sup { + sub, + sup { vertical-align: baseline; font-size: 75%; line-height: 0; @@ -47,14 +55,21 @@ border-color: currentColor; } - button, input, optgroup, select, textarea { + button, + input, + optgroup, + select, + textarea { margin: 0; font-family: inherit; font-size: 100%; line-height: 1.15; } - button, [type="button"], [type="reset"], [type="submit"] { + button, + [type="button"], + [type="reset"], + [type="submit"] { -webkit-appearance: button; } @@ -66,7 +81,8 @@ vertical-align: baseline; } - ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { height: auto; } @@ -157,7 +173,7 @@ padding: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-p { padding: var(--p-xs); } @@ -227,7 +243,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-p { padding: var(--p-sm); } @@ -297,7 +313,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-p { padding: var(--p-md); } @@ -367,7 +383,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-p { padding: var(--p-lg); } @@ -437,7 +453,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-p { padding: var(--p-xl); } @@ -575,7 +591,7 @@ padding-left: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-pl { padding-left: var(--pl-xs); } @@ -645,7 +661,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-pl { padding-left: var(--pl-sm); } @@ -715,7 +731,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-pl { padding-left: var(--pl-md); } @@ -785,7 +801,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-pl { padding-left: var(--pl-lg); } @@ -855,7 +871,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-pl { padding-left: var(--pl-xl); } @@ -993,7 +1009,7 @@ padding-right: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-pr { padding-right: var(--pr-xs); } @@ -1063,7 +1079,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-pr { padding-right: var(--pr-sm); } @@ -1133,7 +1149,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-pr { padding-right: var(--pr-md); } @@ -1203,7 +1219,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-pr { padding-right: var(--pr-lg); } @@ -1273,7 +1289,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-pr { padding-right: var(--pr-xl); } @@ -1411,7 +1427,7 @@ padding-top: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-pt { padding-top: var(--pt-xs); } @@ -1481,7 +1497,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-pt { padding-top: var(--pt-sm); } @@ -1551,7 +1567,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-pt { padding-top: var(--pt-md); } @@ -1621,7 +1637,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-pt { padding-top: var(--pt-lg); } @@ -1691,7 +1707,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-pt { padding-top: var(--pt-xl); } @@ -1829,7 +1845,7 @@ padding-bottom: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-pb { padding-bottom: var(--pb-xs); } @@ -1899,7 +1915,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-pb { padding-bottom: var(--pb-sm); } @@ -1969,7 +1985,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-pb { padding-bottom: var(--pb-md); } @@ -2039,7 +2055,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-pb { padding-bottom: var(--pb-lg); } @@ -2109,7 +2125,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-pb { padding-bottom: var(--pb-xl); } @@ -2264,7 +2280,7 @@ padding-bottom: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-py { padding-top: var(--py-xs); padding-bottom: var(--py-xs); @@ -2351,7 +2367,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-py { padding-top: var(--py-sm); padding-bottom: var(--py-sm); @@ -2438,7 +2454,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-py { padding-top: var(--py-md); padding-bottom: var(--py-md); @@ -2525,7 +2541,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-py { padding-top: var(--py-lg); padding-bottom: var(--py-lg); @@ -2612,7 +2628,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-py { padding-top: var(--py-xl); padding-bottom: var(--py-xl); @@ -2784,7 +2800,7 @@ padding-right: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-px { padding-left: var(--px-xs); padding-right: var(--px-xs); @@ -2871,7 +2887,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-px { padding-left: var(--px-sm); padding-right: var(--px-sm); @@ -2958,7 +2974,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-px { padding-left: var(--px-md); padding-right: var(--px-md); @@ -3045,7 +3061,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-px { padding-left: var(--px-lg); padding-right: var(--px-lg); @@ -3132,7 +3148,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-px { padding-left: var(--px-xl); padding-right: var(--px-xl); @@ -3287,7 +3303,7 @@ margin: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-m { margin: var(--p-xs); } @@ -3357,7 +3373,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-m { margin: var(--p-sm); } @@ -3427,7 +3443,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-m { margin: var(--p-md); } @@ -3497,7 +3513,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-m { margin: var(--p-lg); } @@ -3567,7 +3583,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-m { margin: var(--p-xl); } @@ -3705,7 +3721,7 @@ margin-left: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-ml { margin-left: var(--pl-xs); } @@ -3775,7 +3791,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-ml { margin-left: var(--pl-sm); } @@ -3845,7 +3861,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-ml { margin-left: var(--pl-md); } @@ -3915,7 +3931,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-ml { margin-left: var(--pl-lg); } @@ -3985,7 +4001,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-ml { margin-left: var(--pl-xl); } @@ -4123,7 +4139,7 @@ margin-right: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-mr { margin-right: var(--pr-xs); } @@ -4193,7 +4209,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-mr { margin-right: var(--pr-sm); } @@ -4263,7 +4279,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-mr { margin-right: var(--pr-md); } @@ -4333,7 +4349,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-mr { margin-right: var(--pr-lg); } @@ -4403,7 +4419,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-mr { margin-right: var(--pr-xl); } @@ -4541,7 +4557,7 @@ margin-top: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-mt { margin-top: var(--pt-xs); } @@ -4611,7 +4627,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-mt { margin-top: var(--pt-sm); } @@ -4681,7 +4697,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-mt { margin-top: var(--pt-md); } @@ -4751,7 +4767,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-mt { margin-top: var(--pt-lg); } @@ -4821,7 +4837,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-mt { margin-top: var(--pt-xl); } @@ -4959,7 +4975,7 @@ margin-bottom: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-mb { margin-bottom: var(--pb-xs); } @@ -5029,7 +5045,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-mb { margin-bottom: var(--pb-sm); } @@ -5099,7 +5115,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-mb { margin-bottom: var(--pb-md); } @@ -5169,7 +5185,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-mb { margin-bottom: var(--pb-lg); } @@ -5239,7 +5255,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-mb { margin-bottom: var(--pb-xl); } @@ -5394,7 +5410,7 @@ margin-bottom: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-my { margin-top: var(--py-xs); margin-bottom: var(--py-xs); @@ -5481,7 +5497,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-my { margin-top: var(--py-sm); margin-bottom: var(--py-sm); @@ -5568,7 +5584,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-my { margin-top: var(--py-md); margin-bottom: var(--py-md); @@ -5655,7 +5671,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-my { margin-top: var(--py-lg); margin-bottom: var(--py-lg); @@ -5742,7 +5758,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-my { margin-top: var(--py-xl); margin-bottom: var(--py-xl); @@ -5914,7 +5930,7 @@ margin-right: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-mx { margin-left: var(--px-xs); margin-right: var(--px-xs); @@ -6001,7 +6017,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-mx { margin-left: var(--px-sm); margin-right: var(--px-sm); @@ -6088,7 +6104,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-mx { margin-left: var(--px-md); margin-right: var(--px-md); @@ -6175,7 +6191,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-mx { margin-left: var(--px-lg); margin-right: var(--px-lg); @@ -6262,7 +6278,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-mx { margin-left: var(--px-xl); margin-right: var(--px-xl); @@ -6365,7 +6381,7 @@ display: block; } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-display-none { display: none; } @@ -6383,7 +6399,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-display-none { display: none; } @@ -6401,7 +6417,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-display-none { display: none; } @@ -6419,7 +6435,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-display-none { display: none; } @@ -6437,7 +6453,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-display-none { display: none; } @@ -6467,7 +6483,7 @@ max-width: var(--max-width); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-w { width: var(--width); } @@ -6481,7 +6497,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-w { width: var(--width); } @@ -6495,7 +6511,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-w { width: var(--width); } @@ -6509,7 +6525,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-w { width: var(--width); } @@ -6523,7 +6539,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-w { width: var(--width); } @@ -6549,7 +6565,7 @@ max-height: var(--max-height); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-h { height: var(--height); } @@ -6563,7 +6579,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-h { height: var(--height); } @@ -6577,7 +6593,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-h { height: var(--height); } @@ -6591,7 +6607,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-h { height: var(--height); } @@ -6605,7 +6621,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-h { height: var(--height); } @@ -6639,7 +6655,7 @@ position: static; } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-position-absolute { position: absolute; } @@ -6661,7 +6677,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-position-absolute { position: absolute; } @@ -6683,7 +6699,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-position-absolute { position: absolute; } @@ -6705,7 +6721,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-position-absolute { position: absolute; } @@ -6727,7 +6743,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-position-absolute { position: absolute; } @@ -7017,7 +7033,7 @@ grid-row: span auto / span auto; } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -7287,7 +7303,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -7557,7 +7573,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -7827,7 +7843,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -8097,7 +8113,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -8435,7 +8451,7 @@ gap: var(--bui-space-14); } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-gap { gap: var(--gap-xs); } @@ -8505,7 +8521,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-gap { gap: var(--gap-sm); } @@ -8575,7 +8591,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-gap { gap: var(--gap-md); } @@ -8645,7 +8661,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-gap { gap: var(--gap-lg); } @@ -8715,7 +8731,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-gap { gap: var(--gap-xl); } @@ -8837,7 +8853,7 @@ flex-direction: column-reverse; } -@media (width >= 640px) { +@media (width >=640px) { .xs\:bui-align-start { align-items: start; } @@ -8887,7 +8903,7 @@ } } -@media (width >= 768px) { +@media (width >=768px) { .sm\:bui-align-start { align-items: start; } @@ -8937,7 +8953,7 @@ } } -@media (width >= 1024px) { +@media (width >=1024px) { .md\:bui-align-start { align-items: start; } @@ -8987,7 +9003,7 @@ } } -@media (width >= 1280px) { +@media (width >=1280px) { .lg\:bui-align-start { align-items: start; } @@ -9037,7 +9053,7 @@ } } -@media (width >= 1536px) { +@media (width >=1536px) { .xl\:bui-align-start { align-items: start; } @@ -9440,7 +9456,8 @@ overflow: auto; } -.bui-CardHeader, .bui-CardFooter { +.bui-CardHeader, +.bui-CardFooter { padding-inline: var(--bui-space-3); } @@ -9503,7 +9520,8 @@ display: flex; overflow: hidden; - &[data-starting-style], &[data-ending-style] { + &[data-starting-style], + &[data-ending-style] { height: 0; } } @@ -9515,7 +9533,7 @@ transition: padding .2s ease-in-out; } -@media (width >= 640px) { +@media (width >=640px) { .bui-Container { padding-inline: var(--bui-space-5); } @@ -9663,7 +9681,8 @@ margin-left: -8px; } -.bui-HeaderPageControls, .bui-HeaderPageBreadcrumbs { +.bui-HeaderPageControls, +.bui-HeaderPageBreadcrumbs { align-items: center; gap: var(--bui-space-2); flex-direction: row; @@ -9704,7 +9723,8 @@ display: flex; overflow: hidden; - &[data-entering], &[data-exiting] { + &[data-entering], + &[data-exiting] { transform: var(--origin); opacity: 0; } @@ -9760,7 +9780,8 @@ outline: none; display: flex; - &[data-focused], &[data-open] { + &[data-focused], + &[data-open] { background: var(--bui-bg-surface-2); color: var(--bui-fg-primary); } @@ -9775,7 +9796,7 @@ } &[data-has-submenu] { - & > .bui-MenuItemArrow { + &>.bui-MenuItemArrow { display: block; } } @@ -9800,7 +9821,7 @@ } &[data-selected] .bui-MenuItemListBoxCheck { - & > svg { + &>svg { opacity: 1; color: var(--bui-fg-primary); } @@ -9814,7 +9835,7 @@ height: 1rem; display: flex; - & > svg { + &>svg { opacity: 0; width: 1rem; height: 1rem; @@ -9826,7 +9847,7 @@ gap: var(--bui-space-2); display: flex; - & > svg { + &>svg { width: 1rem; height: 1rem; } @@ -9837,7 +9858,7 @@ height: 1rem; display: none; - & > svg { + &>svg { width: 1rem; height: 1rem; } @@ -9898,7 +9919,8 @@ align-items: center; display: flex; - &::-webkit-search-cancel-button, &::-webkit-search-decoration { + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { -webkit-appearance: none; } } @@ -9919,7 +9941,7 @@ top: 0; bottom: 0; - & > svg { + &>svg { width: 1rem; height: 1rem; } @@ -10015,7 +10037,8 @@ } } - &[data-invalid]:before, &[data-invalid][data-selected]:before { + &[data-invalid]:before, + &[data-invalid][data-selected]:before { border-color: var(--bui-border-danger); } @@ -10109,7 +10132,8 @@ display: inline-flex; } -.bui-TableCellIcon, .bui-TableCellIcon svg { +.bui-TableCellIcon, +.bui-TableCellIcon svg { color: var(--bui-fg-primary); align-items: center; display: inline-flex; @@ -10235,8 +10259,8 @@ .bui-TabActive { content: ""; - left: calc(var(--active-tab-left) + var(--bui-space-2)); - width: calc(var(--active-tab-width) - var(--bui-space-4)); + left: calc(var(--active-tab-left) + var(--bui-space-2)); + width: calc(var(--active-tab-width) - var(--bui-space-4)); background-color: var(--bui-fg-primary); height: 1px; transition: left var(--active-transition-duration) ease-out, opacity .15s ease-out, width var(--active-transition-duration) ease-out; @@ -10249,9 +10273,9 @@ .bui-TabHovered { content: ""; left: var(--hovered-tab-left); - top: calc(var(--hovered-tab-top) + 4px); + top: calc(var(--hovered-tab-top) + 4px); width: var(--hovered-tab-width); - height: calc(var(--hovered-tab-height) - 8px); + height: calc(var(--hovered-tab-height) - 8px); background-color: var(--bui-gray-2); opacity: var(--hovered-tab-opacity); transition: left var(--hovered-transition-duration) ease-out, top var(--hovered-transition-duration) ease-out, width var(--hovered-transition-duration) ease-out, height var(--hovered-transition-duration) ease-out, opacity .15s ease-out; @@ -10419,7 +10443,11 @@ overflow: hidden; } -.bui-Text[data-as="span"], .bui-Text[data-as="label"], .bui-Text[data-as="strong"], .bui-Text[data-as="em"], .bui-Text[data-as="small"] { +.bui-Text[data-as="span"], +.bui-Text[data-as="label"], +.bui-Text[data-as="strong"], +.bui-Text[data-as="em"], +.bui-Text[data-as="small"] { display: inline-block; } @@ -10437,6 +10465,8 @@ flex-shrink: 0; width: 100%; display: flex; + /* Reserve space for browser/password manager icon (e.g. 1Password) */ + --bui-passwordmanager-icon-width: var(--bui-space-1); } .bui-InputWrapper { @@ -10484,6 +10514,35 @@ } } +.bui-InputVisibility { + background-color: transparent; + cursor: pointer; + border: none; + padding: 0; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--bui-fg-primary); + + /* Size: small */ + &[data-size="small"] { width: 2rem; height: 2rem; } + &[data-size="small"] svg { width: 1rem; height: 1rem; } + + /* Size: medium */ + &[data-size="medium"] { width: 2.5rem; height: 2.5rem; } + &[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } +} + +/* Ensure input has enough right padding for our toggle + PM icon */ +.bui-PasswordField .bui-InputWrapper[data-size='small'] .bui-Input { + padding-right: calc(2rem + var(--bui-passwordmanager-icon-width)); +} + +.bui-PasswordField .bui-InputWrapper[data-size='medium'] .bui-Input { + padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width)); +} + .bui-InputIcon { left: var(--bui-space-3); margin-right: var(--bui-space-1); @@ -10495,12 +10554,14 @@ top: 50%; transform: translateY(-50%); - &[data-size="small"], &[data-size="small"] svg { + &[data-size="small"], + &[data-size="small"] svg { width: 1rem; height: 1rem; } - &[data-size="medium"], &[data-size="medium"] svg { + &[data-size="medium"], + &[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } @@ -10522,7 +10583,8 @@ transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; display: flex; - &::-webkit-search-cancel-button, &::-webkit-search-decoration { + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { -webkit-appearance: none; } @@ -10701,7 +10763,8 @@ transform: translate3d(0, 0, 0); box-shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a; - &[data-entering], &[data-exiting] { + &[data-entering], + &[data-exiting] { transform: var(--origin); opacity: 0; } @@ -10811,7 +10874,8 @@ transition: opacity .15s .3s; display: flex; - &[data-hovering], &[data-scrolling] { + &[data-hovering], + &[data-scrolling] { opacity: 1; transition-duration: 75ms; transition-delay: 0s; @@ -10892,7 +10956,8 @@ border-color: var(--bui-fg-danger); } - &[data-invalid]:hover, &[data-invalid]:focus-visible { + &[data-invalid]:hover, + &[data-invalid]:focus-visible { border-width: 2px; } @@ -11045,4 +11110,4 @@ transition: all .2s; display: block; } -} +} \ No newline at end of file diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 5d04bf8626..00dd6d4d42 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -643,6 +643,17 @@ export const componentDefinitions: { readonly inputWrapper: 'bui-InputWrapper'; readonly input: 'bui-Input'; readonly inputIcon: 'bui-InputIcon'; + readonly inputAction: 'bui-InputAction'; + }; + readonly dataAttributes: { + readonly invalid: readonly [true, false]; + readonly disabled: readonly [true, false]; + }; + }; + readonly PasswordField: { + readonly classNames: { + readonly root: 'bui-PasswordField'; + readonly inputVisibility: 'bui-InputVisibility'; }; readonly dataAttributes: { readonly invalid: readonly [true, false]; @@ -1760,6 +1771,7 @@ export interface TextFieldProps icon?: ReactNode; placeholder?: string; size?: 'small' | 'medium' | Partial>; + type?: 'text' | 'email' | 'tel' | 'url'; } // @public (undocumented) diff --git a/packages/ui/src/components/PasswordField/PasswordField.styles.css b/packages/ui/src/components/PasswordField/PasswordField.styles.css index e4d0d2f792..371d769ea6 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.styles.css +++ b/packages/ui/src/components/PasswordField/PasswordField.styles.css @@ -20,122 +20,51 @@ font-family: var(--bui-font-regular); width: 100%; flex-shrink: 0; + /* Reserve space for browser/password manager icon (e.g. 1Password) */ + --bui-passwordmanager-icon-width: var(--bui-space-1); } -.bui-InputWrapper { - position: relative; - - &[data-size='small'] .bui-Input { - height: 2rem; - } - - &[data-size='medium'] .bui-Input { - height: 2.5rem; - } - - &[data-size='small'] .bui-Input[data-icon] { - padding-left: var(--bui-space-8); - } - - &[data-size='medium'] .bui-Input[data-icon] { - padding-left: var(--bui-space-9); - } -} - -.bui-InputIcon { +.bui-InputVisibility { position: absolute; - left: var(--bui-space-3); - top: 50%; - transform: translateY(-50%); - margin-right: var(--bui-space-1); - color: var(--bui-fg-primary); - flex-shrink: 0; - pointer-events: none; - /* To animate the icon when the input is collapsed */ - transition: left 0.2s ease-in-out; - - &[data-size='small'], - &[data-size='small'] svg { - width: 1rem; - height: 1rem; - } - - &[data-size='medium'], - &[data-size='medium'] svg { - width: 1.25rem; - height: 1.25rem; - } -} - -.bui-Input { + right: var(--bui-passwordmanager-icon-width); + top: 0; + bottom: 0; + background-color: transparent; + cursor: pointer; + border: none; + padding: 0; + margin: 0; display: flex; align-items: center; - padding: 0 var(--bui-space-3); - border-radius: var(--bui-radius-2); - border: 1px solid var(--bui-border); - background-color: var(--bui-bg-surface-1); - font-size: var(--bui-font-size-3); - font-family: var(--bui-font-regular); - font-weight: var(--bui-font-weight-regular); + justify-content: center; color: var(--bui-fg-primary); - transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; - width: 100%; - height: 100%; - cursor: inherit; - &::-webkit-search-cancel-button, - &::-webkit-search-decoration { - -webkit-appearance: none; + /* Size: small */ + &[data-size='small'] { + width: 2rem; + height: 2rem; } - - &::placeholder { - color: var(--bui-fg-secondary); - } - - &[data-focused] { - outline-color: var(--bui-border-pressed); - outline-width: 0px; - } - - &[data-hovered] { - border-color: var(--bui-border-hover); - } - - &[data-focused] { - border-color: var(--bui-border-pressed); - outline-width: 0px; - } - - &[data-invalid] { - border-color: var(--bui-fg-danger); - } - - &[data-disabled] { - opacity: 0.5; - cursor: not-allowed; - border: 1px solid var(--bui-border-disabled); - } -} - -.bui-InputAction { - position: absolute; - right: var(--bui-space-1); - top: 50%; - transform: translateY(-50%); - color: var(--bui-fg-primary); - flex-shrink: 0; - /* To animate the icon when the input is collapsed */ - transition: right 0.2s ease-in-out; - - &[data-size='small'], &[data-size='small'] svg { width: 1rem; height: 1rem; } - &[data-size='medium'], + /* Size: medium */ + &[data-size='medium'] { + width: 2.5rem; + height: 2.5rem; + } &[data-size='medium'] svg { width: 1.25rem; height: 1.25rem; } } + +/* Ensure input has enough right padding for our toggle + PM icon */ +.bui-PasswordField .bui-InputWrapper[data-size='small'] .bui-Input { + padding-right: calc(2rem + var(--bui-passwordmanager-icon-width)); +} + +.bui-PasswordField .bui-InputWrapper[data-size='medium'] .bui-Input { + padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width)); +} diff --git a/packages/ui/src/components/PasswordField/PasswordField.tsx b/packages/ui/src/components/PasswordField/PasswordField.tsx index 46b907c010..fc5dae1088 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.tsx +++ b/packages/ui/src/components/PasswordField/PasswordField.tsx @@ -15,15 +15,18 @@ */ import { forwardRef, useEffect, useState } from 'react'; -import { Input, TextField as AriaTextField } from 'react-aria-components'; +import { + Input, + TextField as AriaTextField, + Button as RAButton, +} from 'react-aria-components'; import clsx from 'clsx'; import { FieldLabel } from '../FieldLabel'; import { FieldError } from '../FieldError'; import type { PasswordFieldProps } from './types'; import { useStyles } from '../../hooks/useStyles'; -import { Icon } from '../Icon'; -import { ButtonIcon } from '../ButtonIcon'; +import { RiEyeLine, RiEyeOffLine } from '@remixicon/react'; /** @public */ export const PasswordField = forwardRef( @@ -50,9 +53,14 @@ export const PasswordField = forwardRef( } }, [label, ariaLabel, ariaLabelledBy]); - const { classNames, dataAttributes } = useStyles('PasswordField', { - size, - }); + const { classNames: passwordFieldClassNames, dataAttributes } = useStyles( + 'PasswordField', + { + size, + }, + ); + + const { classNames: textFieldClassNames } = useStyles('TextField', {}); // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. const secondaryLabelText = @@ -63,10 +71,11 @@ export const PasswordField = forwardRef( return ( @@ -76,29 +85,33 @@ export const PasswordField = forwardRef( description={description} />
{icon && ( )} -
- + setIsVisible(v => !v)} - icon={} - /> + className={passwordFieldClassNames.inputVisibility} + > + {isVisible ? : } +
( } }, [label, ariaLabel, ariaLabelledBy]); - useEffect(() => { - const inputType = (props as { type?: string }).type; - if (inputType === 'search') { - console.warn( - 'TextField type="search" is deprecated. Use SearchField instead.', - ); - } - if (inputType === 'password') { - console.warn( - 'TextField type="password" is deprecated. Use PasswordField instead.', - ); - } - }, [props]); - const { classNames, dataAttributes } = useStyles('TextField', { size, }); diff --git a/packages/ui/src/components/TextField/types.ts b/packages/ui/src/components/TextField/types.ts index 0c34634e2d..b971ef8d26 100644 --- a/packages/ui/src/components/TextField/types.ts +++ b/packages/ui/src/components/TextField/types.ts @@ -27,18 +27,10 @@ export interface TextFieldProps * The HTML input type for the text field * * @remarks - * The values 'search' and 'password' are deprecated. Use `SearchField` for + * Use `SearchField` for * search inputs and `PasswordField` for password inputs. */ - type?: - | 'text' - | 'email' - | 'tel' - | 'url' - /** @deprecated Use `SearchField` instead */ - | 'search' - /** @deprecated Use `PasswordField` instead */ - | 'password'; + type?: 'text' | 'email' | 'tel' | 'url'; /** * An icon to render before the input */ diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index f67ee781dc..ff91831cc4 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -296,10 +296,7 @@ export const componentDefinitions = { PasswordField: { classNames: { root: 'bui-PasswordField', - inputWrapper: 'bui-InputWrapper', - input: 'bui-Input', - inputIcon: 'bui-InputIcon', - inputAction: 'bui-InputAction', + inputVisibility: 'bui-InputVisibility', }, dataAttributes: { invalid: [true, false] as const, From ba406d91f3250125cdc1c65efe85da79d8569e4d Mon Sep 17 00:00:00 2001 From: birdhb Date: Fri, 26 Sep 2025 11:43:13 +0100 Subject: [PATCH 126/193] Update eighty-cows-care.md Signed-off-by: birdhb --- .changeset/eighty-cows-care.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/eighty-cows-care.md b/.changeset/eighty-cows-care.md index 8fd5c0adb0..e03b364449 100644 --- a/.changeset/eighty-cows-care.md +++ b/.changeset/eighty-cows-care.md @@ -1,5 +1,5 @@ --- -'@backstage/ui': patch +'@backstage/ui': minor --- -Added PasswordField to BUI with default show/hide visibility on text input +Added PasswordField to BUI with default show/hide visibility on input From 48df0d0e32607c01c4437afce54076e8155fd02b Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 26 Sep 2025 12:50:15 +0200 Subject: [PATCH 127/193] chore: fixing loading state Signed-off-by: benjdlambert --- .../src/hooks/useEventStream.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-react/src/hooks/useEventStream.ts b/plugins/scaffolder-react/src/hooks/useEventStream.ts index 7c5ce09ab7..01906d3e32 100644 --- a/plugins/scaffolder-react/src/hooks/useEventStream.ts +++ b/plugins/scaffolder-react/src/hooks/useEventStream.ts @@ -19,12 +19,12 @@ import { useEffect } from 'react'; import { useApi } from '@backstage/core-plugin-api'; import { Subscription } from '@backstage/types'; import { - LogEvent, - scaffolderApiRef, - ScaffolderTask, - ScaffolderTaskOutput, ScaffolderTaskStatus, -} from '../api'; + ScaffolderTaskOutput, + ScaffolderTask, + LogEvent, +} from '@backstage/plugin-scaffolder-common'; +import { scaffolderApiRef } from '../api'; /** * The status of the step being processed @@ -85,7 +85,6 @@ function reducer(draft: TaskStream, action: ReducerAction) { current[next.id] = []; return current; }, {} as { [stepId in string]: string[] }); - draft.loading = false; draft.error = undefined; draft.completed = false; draft.task = action.data; @@ -96,6 +95,12 @@ function reducer(draft: TaskStream, action: ReducerAction) { const entries = action.data; const logLines = []; + // only set loading as false once we have logs, + // otherwise things flicker from pending to loaded. + if (draft.loading && entries.length > 0) { + draft.loading = false; + } + for (const entry of entries) { const logLine = `${entry.createdAt} ${entry.body.message}`; logLines.push(logLine); From e61f89e611b3d5a6efd46ebb7d2e49e431dc5ee7 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 26 Sep 2025 12:51:09 +0200 Subject: [PATCH 128/193] chore: changeset Signed-off-by: benjdlambert --- .changeset/curvy-bobcats-melt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curvy-bobcats-melt.md diff --git a/.changeset/curvy-bobcats-melt.md b/.changeset/curvy-bobcats-melt.md new file mode 100644 index 0000000000..a9a5d58e5c --- /dev/null +++ b/.changeset/curvy-bobcats-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Don't change loading to false until we've actually got some log state From 68c0f72084ead2fa884653747b4067f379d079f7 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 26 Sep 2025 13:01:29 +0200 Subject: [PATCH 129/193] chore: fix api-reports Signed-off-by: benjdlambert --- plugins/scaffolder-react/report.api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index 4b81a18eb6..daec52a7e3 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -354,7 +354,7 @@ export type ScaffolderScaffoldResponse = ScaffolderScaffoldResponse_2; // @public export type ScaffolderStep = { id: string; - status: ScaffolderTaskStatus; + status: ScaffolderTaskStatus_2; endedAt?: string; startedAt?: string; }; @@ -398,11 +398,11 @@ export type TaskStream = { [stepId in string]: string[]; }; completed: boolean; - task?: ScaffolderTask; + task?: ScaffolderTask_2; steps: { [stepId in string]: ScaffolderStep; }; - output?: ScaffolderTaskOutput; + output?: ScaffolderTaskOutput_2; }; // @public @deprecated From dd28877f94f1a9793350cde35d98cf193d5ae7fa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 26 Sep 2025 13:03:16 +0200 Subject: [PATCH 130/193] workflows: tweak techdocs workflow Signed-off-by: Patrik Oldsberg --- .github/workflows/verify_e2e-techdocs.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 080d14a743..64ce9e7b12 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -41,11 +41,10 @@ jobs: with: python-version: '3.9' - - name: install dependencies - run: yarn install --immutable - - - name: generate types - run: yarn tsc + - name: yarn install + uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: build techdocs-cli working-directory: packages/techdocs-cli From c953ee97882ebc9aceebbb86ce9454592b9afe13 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Fri, 26 Sep 2025 13:55:45 +0100 Subject: [PATCH 131/193] remove formatting on style css Signed-off-by: Hermione Bird --- packages/ui/css/styles.css | 312 +++++++++++++++++-------------------- 1 file changed, 139 insertions(+), 173 deletions(-) diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index 2d3397f4a1..0554276dc2 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -1,9 +1,6 @@ /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ @layer base { - - *, - :before, - :after { + *, :before, :after { box-sizing: border-box; } @@ -18,15 +15,11 @@ margin: 0; } - b, - strong { + b, strong { font-weight: bolder; } - code, - kbd, - samp, - pre { + code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; font-size: 1em; } @@ -35,8 +28,7 @@ font-size: 80%; } - sub, - sup { + sub, sup { vertical-align: baseline; font-size: 75%; line-height: 0; @@ -55,21 +47,14 @@ border-color: currentColor; } - button, - input, - optgroup, - select, - textarea { + button, input, optgroup, select, textarea { margin: 0; font-family: inherit; font-size: 100%; line-height: 1.15; } - button, - [type="button"], - [type="reset"], - [type="submit"] { + button, [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } @@ -81,8 +66,7 @@ vertical-align: baseline; } - ::-webkit-inner-spin-button, - ::-webkit-outer-spin-button { + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } @@ -173,7 +157,7 @@ padding: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-p { padding: var(--p-xs); } @@ -243,7 +227,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-p { padding: var(--p-sm); } @@ -313,7 +297,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-p { padding: var(--p-md); } @@ -383,7 +367,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-p { padding: var(--p-lg); } @@ -453,7 +437,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-p { padding: var(--p-xl); } @@ -591,7 +575,7 @@ padding-left: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-pl { padding-left: var(--pl-xs); } @@ -661,7 +645,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-pl { padding-left: var(--pl-sm); } @@ -731,7 +715,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-pl { padding-left: var(--pl-md); } @@ -801,7 +785,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-pl { padding-left: var(--pl-lg); } @@ -871,7 +855,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-pl { padding-left: var(--pl-xl); } @@ -1009,7 +993,7 @@ padding-right: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-pr { padding-right: var(--pr-xs); } @@ -1079,7 +1063,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-pr { padding-right: var(--pr-sm); } @@ -1149,7 +1133,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-pr { padding-right: var(--pr-md); } @@ -1219,7 +1203,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-pr { padding-right: var(--pr-lg); } @@ -1289,7 +1273,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-pr { padding-right: var(--pr-xl); } @@ -1427,7 +1411,7 @@ padding-top: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-pt { padding-top: var(--pt-xs); } @@ -1497,7 +1481,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-pt { padding-top: var(--pt-sm); } @@ -1567,7 +1551,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-pt { padding-top: var(--pt-md); } @@ -1637,7 +1621,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-pt { padding-top: var(--pt-lg); } @@ -1707,7 +1691,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-pt { padding-top: var(--pt-xl); } @@ -1845,7 +1829,7 @@ padding-bottom: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-pb { padding-bottom: var(--pb-xs); } @@ -1915,7 +1899,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-pb { padding-bottom: var(--pb-sm); } @@ -1985,7 +1969,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-pb { padding-bottom: var(--pb-md); } @@ -2055,7 +2039,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-pb { padding-bottom: var(--pb-lg); } @@ -2125,7 +2109,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-pb { padding-bottom: var(--pb-xl); } @@ -2280,7 +2264,7 @@ padding-bottom: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-py { padding-top: var(--py-xs); padding-bottom: var(--py-xs); @@ -2367,7 +2351,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-py { padding-top: var(--py-sm); padding-bottom: var(--py-sm); @@ -2454,7 +2438,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-py { padding-top: var(--py-md); padding-bottom: var(--py-md); @@ -2541,7 +2525,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-py { padding-top: var(--py-lg); padding-bottom: var(--py-lg); @@ -2628,7 +2612,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-py { padding-top: var(--py-xl); padding-bottom: var(--py-xl); @@ -2800,7 +2784,7 @@ padding-right: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-px { padding-left: var(--px-xs); padding-right: var(--px-xs); @@ -2887,7 +2871,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-px { padding-left: var(--px-sm); padding-right: var(--px-sm); @@ -2974,7 +2958,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-px { padding-left: var(--px-md); padding-right: var(--px-md); @@ -3061,7 +3045,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-px { padding-left: var(--px-lg); padding-right: var(--px-lg); @@ -3148,7 +3132,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-px { padding-left: var(--px-xl); padding-right: var(--px-xl); @@ -3303,7 +3287,7 @@ margin: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-m { margin: var(--p-xs); } @@ -3373,7 +3357,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-m { margin: var(--p-sm); } @@ -3443,7 +3427,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-m { margin: var(--p-md); } @@ -3513,7 +3497,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-m { margin: var(--p-lg); } @@ -3583,7 +3567,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-m { margin: var(--p-xl); } @@ -3721,7 +3705,7 @@ margin-left: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-ml { margin-left: var(--pl-xs); } @@ -3791,7 +3775,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-ml { margin-left: var(--pl-sm); } @@ -3861,7 +3845,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-ml { margin-left: var(--pl-md); } @@ -3931,7 +3915,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-ml { margin-left: var(--pl-lg); } @@ -4001,7 +3985,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-ml { margin-left: var(--pl-xl); } @@ -4139,7 +4123,7 @@ margin-right: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-mr { margin-right: var(--pr-xs); } @@ -4209,7 +4193,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-mr { margin-right: var(--pr-sm); } @@ -4279,7 +4263,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-mr { margin-right: var(--pr-md); } @@ -4349,7 +4333,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-mr { margin-right: var(--pr-lg); } @@ -4419,7 +4403,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-mr { margin-right: var(--pr-xl); } @@ -4557,7 +4541,7 @@ margin-top: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-mt { margin-top: var(--pt-xs); } @@ -4627,7 +4611,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-mt { margin-top: var(--pt-sm); } @@ -4697,7 +4681,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-mt { margin-top: var(--pt-md); } @@ -4767,7 +4751,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-mt { margin-top: var(--pt-lg); } @@ -4837,7 +4821,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-mt { margin-top: var(--pt-xl); } @@ -4975,7 +4959,7 @@ margin-bottom: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-mb { margin-bottom: var(--pb-xs); } @@ -5045,7 +5029,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-mb { margin-bottom: var(--pb-sm); } @@ -5115,7 +5099,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-mb { margin-bottom: var(--pb-md); } @@ -5185,7 +5169,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-mb { margin-bottom: var(--pb-lg); } @@ -5255,7 +5239,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-mb { margin-bottom: var(--pb-xl); } @@ -5410,7 +5394,7 @@ margin-bottom: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-my { margin-top: var(--py-xs); margin-bottom: var(--py-xs); @@ -5497,7 +5481,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-my { margin-top: var(--py-sm); margin-bottom: var(--py-sm); @@ -5584,7 +5568,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-my { margin-top: var(--py-md); margin-bottom: var(--py-md); @@ -5671,7 +5655,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-my { margin-top: var(--py-lg); margin-bottom: var(--py-lg); @@ -5758,7 +5742,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-my { margin-top: var(--py-xl); margin-bottom: var(--py-xl); @@ -5930,7 +5914,7 @@ margin-right: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-mx { margin-left: var(--px-xs); margin-right: var(--px-xs); @@ -6017,7 +6001,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-mx { margin-left: var(--px-sm); margin-right: var(--px-sm); @@ -6104,7 +6088,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-mx { margin-left: var(--px-md); margin-right: var(--px-md); @@ -6191,7 +6175,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-mx { margin-left: var(--px-lg); margin-right: var(--px-lg); @@ -6278,7 +6262,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-mx { margin-left: var(--px-xl); margin-right: var(--px-xl); @@ -6381,7 +6365,7 @@ display: block; } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-display-none { display: none; } @@ -6399,7 +6383,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-display-none { display: none; } @@ -6417,7 +6401,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-display-none { display: none; } @@ -6435,7 +6419,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-display-none { display: none; } @@ -6453,7 +6437,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-display-none { display: none; } @@ -6483,7 +6467,7 @@ max-width: var(--max-width); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-w { width: var(--width); } @@ -6497,7 +6481,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-w { width: var(--width); } @@ -6511,7 +6495,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-w { width: var(--width); } @@ -6525,7 +6509,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-w { width: var(--width); } @@ -6539,7 +6523,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-w { width: var(--width); } @@ -6565,7 +6549,7 @@ max-height: var(--max-height); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-h { height: var(--height); } @@ -6579,7 +6563,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-h { height: var(--height); } @@ -6593,7 +6577,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-h { height: var(--height); } @@ -6607,7 +6591,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-h { height: var(--height); } @@ -6621,7 +6605,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-h { height: var(--height); } @@ -6655,7 +6639,7 @@ position: static; } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-position-absolute { position: absolute; } @@ -6677,7 +6661,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-position-absolute { position: absolute; } @@ -6699,7 +6683,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-position-absolute { position: absolute; } @@ -6721,7 +6705,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-position-absolute { position: absolute; } @@ -6743,7 +6727,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-position-absolute { position: absolute; } @@ -7033,7 +7017,7 @@ grid-row: span auto / span auto; } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -7303,7 +7287,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -7573,7 +7557,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -7843,7 +7827,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -8113,7 +8097,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -8451,7 +8435,7 @@ gap: var(--bui-space-14); } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-gap { gap: var(--gap-xs); } @@ -8521,7 +8505,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-gap { gap: var(--gap-sm); } @@ -8591,7 +8575,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-gap { gap: var(--gap-md); } @@ -8661,7 +8645,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-gap { gap: var(--gap-lg); } @@ -8731,7 +8715,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-gap { gap: var(--gap-xl); } @@ -8853,7 +8837,7 @@ flex-direction: column-reverse; } -@media (width >=640px) { +@media (width >= 640px) { .xs\:bui-align-start { align-items: start; } @@ -8903,7 +8887,7 @@ } } -@media (width >=768px) { +@media (width >= 768px) { .sm\:bui-align-start { align-items: start; } @@ -8953,7 +8937,7 @@ } } -@media (width >=1024px) { +@media (width >= 1024px) { .md\:bui-align-start { align-items: start; } @@ -9003,7 +8987,7 @@ } } -@media (width >=1280px) { +@media (width >= 1280px) { .lg\:bui-align-start { align-items: start; } @@ -9053,7 +9037,7 @@ } } -@media (width >=1536px) { +@media (width >= 1536px) { .xl\:bui-align-start { align-items: start; } @@ -9456,8 +9440,7 @@ overflow: auto; } -.bui-CardHeader, -.bui-CardFooter { +.bui-CardHeader, .bui-CardFooter { padding-inline: var(--bui-space-3); } @@ -9520,8 +9503,7 @@ display: flex; overflow: hidden; - &[data-starting-style], - &[data-ending-style] { + &[data-starting-style], &[data-ending-style] { height: 0; } } @@ -9533,7 +9515,7 @@ transition: padding .2s ease-in-out; } -@media (width >=640px) { +@media (width >= 640px) { .bui-Container { padding-inline: var(--bui-space-5); } @@ -9681,8 +9663,7 @@ margin-left: -8px; } -.bui-HeaderPageControls, -.bui-HeaderPageBreadcrumbs { +.bui-HeaderPageControls, .bui-HeaderPageBreadcrumbs { align-items: center; gap: var(--bui-space-2); flex-direction: row; @@ -9723,8 +9704,7 @@ display: flex; overflow: hidden; - &[data-entering], - &[data-exiting] { + &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } @@ -9780,8 +9760,7 @@ outline: none; display: flex; - &[data-focused], - &[data-open] { + &[data-focused], &[data-open] { background: var(--bui-bg-surface-2); color: var(--bui-fg-primary); } @@ -9796,7 +9775,7 @@ } &[data-has-submenu] { - &>.bui-MenuItemArrow { + & > .bui-MenuItemArrow { display: block; } } @@ -9821,7 +9800,7 @@ } &[data-selected] .bui-MenuItemListBoxCheck { - &>svg { + & > svg { opacity: 1; color: var(--bui-fg-primary); } @@ -9835,7 +9814,7 @@ height: 1rem; display: flex; - &>svg { + & > svg { opacity: 0; width: 1rem; height: 1rem; @@ -9847,7 +9826,7 @@ gap: var(--bui-space-2); display: flex; - &>svg { + & > svg { width: 1rem; height: 1rem; } @@ -9858,7 +9837,7 @@ height: 1rem; display: none; - &>svg { + & > svg { width: 1rem; height: 1rem; } @@ -9919,8 +9898,7 @@ align-items: center; display: flex; - &::-webkit-search-cancel-button, - &::-webkit-search-decoration { + &::-webkit-search-cancel-button, &::-webkit-search-decoration { -webkit-appearance: none; } } @@ -9941,7 +9919,7 @@ top: 0; bottom: 0; - &>svg { + & > svg { width: 1rem; height: 1rem; } @@ -10037,8 +10015,7 @@ } } - &[data-invalid]:before, - &[data-invalid][data-selected]:before { + &[data-invalid]:before, &[data-invalid][data-selected]:before { border-color: var(--bui-border-danger); } @@ -10132,8 +10109,7 @@ display: inline-flex; } -.bui-TableCellIcon, -.bui-TableCellIcon svg { +.bui-TableCellIcon, .bui-TableCellIcon svg { color: var(--bui-fg-primary); align-items: center; display: inline-flex; @@ -10259,8 +10235,8 @@ .bui-TabActive { content: ""; - left: calc(var(--active-tab-left) + var(--bui-space-2)); - width: calc(var(--active-tab-width) - var(--bui-space-4)); + left: calc(var(--active-tab-left) + var(--bui-space-2)); + width: calc(var(--active-tab-width) - var(--bui-space-4)); background-color: var(--bui-fg-primary); height: 1px; transition: left var(--active-transition-duration) ease-out, opacity .15s ease-out, width var(--active-transition-duration) ease-out; @@ -10273,9 +10249,9 @@ .bui-TabHovered { content: ""; left: var(--hovered-tab-left); - top: calc(var(--hovered-tab-top) + 4px); + top: calc(var(--hovered-tab-top) + 4px); width: var(--hovered-tab-width); - height: calc(var(--hovered-tab-height) - 8px); + height: calc(var(--hovered-tab-height) - 8px); background-color: var(--bui-gray-2); opacity: var(--hovered-tab-opacity); transition: left var(--hovered-transition-duration) ease-out, top var(--hovered-transition-duration) ease-out, width var(--hovered-transition-duration) ease-out, height var(--hovered-transition-duration) ease-out, opacity .15s ease-out; @@ -10443,11 +10419,7 @@ overflow: hidden; } -.bui-Text[data-as="span"], -.bui-Text[data-as="label"], -.bui-Text[data-as="strong"], -.bui-Text[data-as="em"], -.bui-Text[data-as="small"] { +.bui-Text[data-as="span"], .bui-Text[data-as="label"], .bui-Text[data-as="strong"], .bui-Text[data-as="em"], .bui-Text[data-as="small"] { display: inline-block; } @@ -10554,14 +10526,12 @@ top: 50%; transform: translateY(-50%); - &[data-size="small"], - &[data-size="small"] svg { + &[data-size="small"], &[data-size="small"] svg { width: 1rem; height: 1rem; } - &[data-size="medium"], - &[data-size="medium"] svg { + &[data-size="medium"], &[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } @@ -10583,8 +10553,7 @@ transition: border-color .2s ease-in-out, outline-color .2s ease-in-out; display: flex; - &::-webkit-search-cancel-button, - &::-webkit-search-decoration { + &::-webkit-search-cancel-button, &::-webkit-search-decoration { -webkit-appearance: none; } @@ -10763,8 +10732,7 @@ transform: translate3d(0, 0, 0); box-shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a; - &[data-entering], - &[data-exiting] { + &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } @@ -10874,8 +10842,7 @@ transition: opacity .15s .3s; display: flex; - &[data-hovering], - &[data-scrolling] { + &[data-hovering], &[data-scrolling] { opacity: 1; transition-duration: 75ms; transition-delay: 0s; @@ -10956,8 +10923,7 @@ border-color: var(--bui-fg-danger); } - &[data-invalid]:hover, - &[data-invalid]:focus-visible { + &[data-invalid]:hover, &[data-invalid]:focus-visible { border-width: 2px; } @@ -11110,4 +11076,4 @@ transition: all .2s; display: block; } -} \ No newline at end of file +} From b5ee96aaaec19213bdd2ad9875ed07a3f4e2be50 Mon Sep 17 00:00:00 2001 From: birdhb Date: Fri, 26 Sep 2025 14:30:13 +0100 Subject: [PATCH 132/193] Update eighty-cows-care.md Signed-off-by: birdhb --- .changeset/eighty-cows-care.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eighty-cows-care.md b/.changeset/eighty-cows-care.md index e03b364449..73ec239b36 100644 --- a/.changeset/eighty-cows-care.md +++ b/.changeset/eighty-cows-care.md @@ -2,4 +2,4 @@ '@backstage/ui': minor --- -Added PasswordField to BUI with default show/hide visibility on input +**Breaking** We are adding a new component `PasswordField`. We will be removing support for types `password` and `search` on `TextField`. From 683574f888860c1fff2ea693ee797d60f680295d Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Fri, 26 Sep 2025 15:28:01 +0100 Subject: [PATCH 133/193] format componentDefinitions Signed-off-by: Hermione Bird --- packages/ui/report.api.md | 16 ++++++---------- packages/ui/src/utils/componentDefinitions.ts | 16 ++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 00dd6d4d42..c3aca2718d 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -525,6 +525,12 @@ export const componentDefinitions: { readonly emptyState: 'bui-MenuEmptyState'; }; }; + readonly PasswordField: { + readonly classNames: { + readonly root: 'bui-PasswordField'; + readonly inputVisibility: 'bui-InputVisibility'; + }; + }; readonly Popover: { readonly classNames: { readonly root: 'bui-Popover'; @@ -650,16 +656,6 @@ export const componentDefinitions: { readonly disabled: readonly [true, false]; }; }; - readonly PasswordField: { - readonly classNames: { - readonly root: 'bui-PasswordField'; - readonly inputVisibility: 'bui-InputVisibility'; - }; - readonly dataAttributes: { - readonly invalid: readonly [true, false]; - readonly disabled: readonly [true, false]; - }; - }; readonly Tooltip: { readonly classNames: { readonly tooltip: 'bui-Tooltip'; diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index ff91831cc4..931bfacca7 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -174,6 +174,12 @@ export const componentDefinitions = { emptyState: 'bui-MenuEmptyState', }, }, + PasswordField: { + classNames: { + root: 'bui-PasswordField', + inputVisibility: 'bui-InputVisibility', + }, + }, Popover: { classNames: { root: 'bui-Popover', @@ -293,16 +299,6 @@ export const componentDefinitions = { disabled: [true, false] as const, }, }, - PasswordField: { - classNames: { - root: 'bui-PasswordField', - inputVisibility: 'bui-InputVisibility', - }, - dataAttributes: { - invalid: [true, false] as const, - disabled: [true, false] as const, - }, - }, Tooltip: { classNames: { tooltip: 'bui-Tooltip', From 9912c21afa29287a2f2507b389efcaff1db388d2 Mon Sep 17 00:00:00 2001 From: Hermione Bird Date: Fri, 26 Sep 2025 15:40:19 +0100 Subject: [PATCH 134/193] fix ci install build bui Signed-off-by: Hermione Bird --- docs-ui/public/theme-backstage.css | 2 +- packages/ui/css/styles.css | 117 +++++++++++++---------------- 2 files changed, 54 insertions(+), 65 deletions(-) diff --git a/docs-ui/public/theme-backstage.css b/docs-ui/public/theme-backstage.css index 69cd0dbd31..9d9c282f5c 100644 --- a/docs-ui/public/theme-backstage.css +++ b/docs-ui/public/theme-backstage.css @@ -1,2 +1,2 @@ /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ -@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}}.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--pl-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--pl-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--pl-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--pl-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--pl-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--pr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--pr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--pr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--pr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--pr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--pt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--pt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--pt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--pt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--pt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--pb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--pb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--pb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--pb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--pb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--py-xs);margin-bottom:var(--py-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--py-sm);margin-bottom:var(--py-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--py-md);margin-bottom:var(--py-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--py-lg);margin-bottom:var(--py-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--py-xl);margin-bottom:var(--py-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--px-xs);margin-right:var(--px-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--px-sm);margin-right:var(--px-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--px-md);margin-right:var(--px-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--px-lg);margin-right:var(--px-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--px-xl);margin-right:var(--px-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-baseline{align-items:baseline}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#ebebeb;--bui-bg-tint:transparent;--bui-bg-tint-hover:#1f549366;--bui-bg-tint-pressed:#1f549399;--bui-bg-tint-disabled:#ebebeb;--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#9c9c9c;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg:#333;--bui-bg-surface-1:#424242;--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#222;--bui-bg-tint:transparent;--bui-bg-tint-hover:#9cc9ff1f;--bui-bg-tint-pressed:#9cc9ff29;--bui-bg-tint-disabled:transparent;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:var(--bui-gray-5);--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757}.bui-AvatarRoot{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:2rem;height:2rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-AvatarRoot[data-size=small]{width:1.5rem;height:1.5rem}.bui-AvatarRoot[data-size=medium]{width:2rem;height:2rem}.bui-AvatarRoot[data-size=large]{width:3rem;height:3rem}.bui-AvatarImage{object-fit:cover;width:100%;height:100%}.bui-AvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-Box{font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary)}.bui-Button{user-select:none;font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-bold);cursor:pointer;border-radius:var(--bui-radius-2);justify-content:center;align-items:center;gap:var(--bui-space-1_5);border:none;flex-shrink:0;padding:0;display:inline-flex;&[data-disabled=true]{cursor:not-allowed}}.bui-Button[data-variant=primary]{background-color:var(--bui-bg-solid);color:var(--bui-fg-solid);&:hover{background-color:var(--bui-bg-solid-hover);transition:background-color .15s}&:active{background-color:var(--bui-bg-solid-pressed)}&:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled=true]{background-color:var(--bui-bg-solid-disabled);color:var(--bui-fg-solid-disabled)}}.bui-Button[data-variant=secondary]{background-color:var(--bui-bg-surface-1);box-shadow:inset 0 0 0 1px var(--bui-border);color:var(--bui-fg-primary);&:hover{box-shadow:inset 0 0 0 1px var(--bui-border-hover);transition:box-shadow .15s}&:active{box-shadow:inset 0 0 0 1px var(--bui-border-pressed)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{box-shadow:inset 0 0 0 1px var(--bui-border-disabled);color:var(--bui-fg-disabled)}}.bui-Button[data-variant=tertiary]{color:var(--bui-fg-primary);background-color:#0000;&:hover{background-color:var(--bui-bg-surface-1);transition:background-color .2s}&:active{background-color:var(--bui-bg-surface-2)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{color:var(--bui-fg-disabled);background-color:#0000}}.bui-Button[data-size=medium]{font-size:var(--bui-font-size-4);padding:0 var(--bui-space-3);height:2.5rem}.bui-Button[data-size=small]{font-size:var(--bui-font-size-3);padding:0 var(--bui-space-2);height:2rem}.bui-Button[data-size=small] svg{width:1rem;height:1rem}.bui-Button[data-size=medium] svg{width:1.25rem;height:1.25rem}.bui-ButtonIcon{justify-content:center;align-items:center}.bui-ButtonIcon[data-size=small]{width:2rem;padding:0}.bui-ButtonIcon[data-size=medium]{width:2.5rem;padding:0}.bui-Card{gap:var(--bui-space-3);background-color:var(--bui-bg-surface-1);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-3);color:var(--bui-fg-primary);border:1px solid var(--bui-border);flex-direction:column;width:100%;min-height:0;display:flex;overflow:hidden}.bui-CardBody{flex:1;min-height:0;overflow:auto}.bui-CardHeader,.bui-CardFooter{padding-inline:var(--bui-space-3)}.bui-CheckboxRoot{width:1rem;height:1rem;box-shadow:inset 0 0 0 1px var(--bui-border);cursor:pointer;background-color:var(--bui-bg-surface-1);border:none;border-radius:2px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background-color .2s ease-in-out;display:flex}.bui-CheckboxRoot:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px;transition:none}.bui-CheckboxRoot[data-checked]{background-color:var(--bui-bg-solid);box-shadow:none;color:var(--bui-fg-solid)}.bui-CheckboxLabel{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);user-select:none;flex-direction:row;display:flex;&:hover{& .bui-CheckboxRoot:not([data-checked]){box-shadow:inset 0 0 0 1px var(--bui-border-hover)}}}.bui-CheckboxIndicator{color:var(--bui-fg-solid);justify-content:center;align-items:center;display:flex}.bui-CollapsiblePanel{height:var(--collapsible-panel-height);transition:all .15s ease-out;display:flex;overflow:hidden;&[data-starting-style],&[data-ending-style]{height:0}}.bui-Container{max-width:120rem;padding-inline:var(--bui-space-4);margin-inline:auto;transition:padding .2s ease-in-out}@media (width>=640px){.bui-Container{padding-inline:var(--bui-space-5)}}.bui-FieldError{color:var(--bui-fg-danger);font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);margin-top:var(--bui-space-2);display:inline-block}.bui-FieldLabelWrapper{margin-bottom:var(--bui-space-3);gap:var(--bui-space-1);flex-direction:column;display:flex}.bui-FieldLabel{color:var(--bui-fg-primary);cursor:pointer;font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);margin-right:auto}.bui-FieldSecondaryLabel{color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular);margin-left:var(--bui-space-1)}.bui-FieldDescription{font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);margin:0}.bui-Flex{min-width:0;display:flex}.bui-Grid{display:grid}.bui-HeaderToolbar{margin-bottom:var(--bui-space-6);&:before{content:"";background-color:var(--bui-bg);z-index:0;height:16px;position:absolute;top:0;left:0;right:0}&[data-has-tabs=true]{margin-bottom:0}}.bui-HeaderToolbarWrapper{z-index:1;background-color:var(--bui-bg-surface-1);padding-inline:var(--bui-space-5);border-bottom:1px solid var(--bui-border);color:var(--bui-fg-primary);flex-direction:row;justify-content:space-between;align-items:center;height:52px;display:flex;position:relative}.bui-HeaderToolbarContent{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-HeaderToolbarName{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);flex-direction:row;flex-shrink:0;display:flex}.bui-HeaderToolbarIcon{width:16px;height:16px;color:var(--bui-fg-primary);& svg{width:100%;height:100%}}.bui-HeaderToolbarControls{right:var(--bui-space-5);align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.bui-HeaderTabsWrapper{margin-bottom:var(--bui-space-4);padding-inline:var(--bui-space-3);border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1)}.bui-HeaderPage{gap:var(--bui-space-1);margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6);flex-direction:column;display:flex}.bui-HeaderPageContent{flex-direction:row;justify-content:space-between;display:flex}.bui-HeaderPageTabsWrapper{margin-left:-8px}.bui-HeaderPageControls,.bui-HeaderPageBreadcrumbs{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-Icon{width:1rem;height:1rem}.bui-Link{font-family:var(--bui-font-regular);cursor:pointer;margin:0;padding:0;text-decoration-line:none;display:inline-block;&:hover{text-underline-offset:calc(.025em + 2px);text-decoration-line:underline;text-decoration-style:solid;text-decoration-thickness:min(2px,max(1px,.05em));text-decoration-color:color-mix(in srgb,currentColor 30%,transparent)}}.bui-MenuPopover{border:1px solid var(--bui-border);border-radius:var(--bui-radius-2);background:var(--bui-bg-surface-1);color:var(--bui-fg-primary);outline:none;flex-direction:column;min-height:0;transition:transform .2s,opacity .2s;display:flex;overflow:hidden;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}&[data-placement=top]{--origin:translateY(8px)}&[data-placement=bottom]{--origin:translateY(-8px)}&[data-placement=right]{--origin:translateX(-8px)}&[data-placement=left]{--origin:translateX(8px)}}.bui-MenuContent{max-height:inherit;box-sizing:border-box;padding:var(--bui-space-1);outline:none;min-width:150px;overflow:auto}.bui-MenuPopover .bui-ScrollAreaRoot{flex-direction:column;flex:1;height:100%;min-height:0;display:flex}.bui-MenuPopover .bui-ScrollAreaScrollbar{margin-inline:var(--bui-space-1_5)}.bui-MenuItem{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&[data-focused],&[data-open]{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-color=danger]{color:var(--bui-fg-danger)}&[data-color=danger][data-focused]{background:var(--bui-bg-danger);color:var(--bui-fg-danger)}&[data-has-submenu]{&>.bui-MenuItemArrow{display:block}}}.bui-MenuItemListBox{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&:hover{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-selected] .bui-MenuItemListBoxCheck{&>svg{opacity:1;color:var(--bui-fg-primary)}}}.bui-MenuItemListBoxCheck{justify-content:center;align-items:center;width:1rem;height:1rem;display:flex;&>svg{opacity:0;width:1rem;height:1rem}}.bui-MenuItemContent{align-items:center;gap:var(--bui-space-2);display:flex;&>svg{width:1rem;height:1rem}}.bui-MenuItemArrow{width:1rem;height:1rem;display:none;&>svg{width:1rem;height:1rem}}.bui-MenuSection{&:first-child .bui-MenuSectionHeader{padding-top:0}}.bui-MenuSectionHeader{height:2rem;padding-top:var(--bui-space-3);padding-left:var(--bui-space-2);color:var(--bui-fg-primary);font-size:var(--bui-font-size-1);letter-spacing:.05rem;text-transform:uppercase;align-items:center;font-weight:700;display:flex}.bui-MenuSeparator{background:var(--bui-border);height:1px;margin-inline:var(--bui-space-1_5);margin-block:var(--bui-space-1)}.bui-MenuSearchField{font-family:var(--bui-font-regular);flex-shrink:0;width:100%;position:relative;&[data-empty]{& .bui-MenuSearchFieldClear{display:none}}}.bui-MenuSearchFieldInput{padding:0 var(--bui-space-3);border:none;border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:2rem;cursor:inherit;outline:none;align-items:center;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}}.bui-MenuSearchFieldClear{right:var(--bui-space-2);cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;&>svg{width:1rem;height:1rem}}.bui-MenuEmptyState{padding:var(--bui-space-1);color:var(--bui-fg-secondary);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular)}.bui-Popover{background-color:var(--bui-bg-surface-1);border:1px solid var(--bui-border);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-1);margin-right:12px;overflow:scroll;box-shadow:0 4px 12px #0000001a}.bui-RadioGroup{color:var(--bui-fg-primary);flex-direction:column;display:flex}.bui-RadioGroup[data-orientation=horizontal] .bui-RadioGroupContent{gap:var(--bui-space-4);flex-direction:row}.bui-RadioGroupContent{gap:var(--bui-space-2);flex-direction:column;display:flex}.bui-Radio{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-2);color:var(--bui-fg-primary);forced-color-adjust:none;display:flex;position:relative;&:before{content:"";box-sizing:border-box;border:.125rem solid var(--bui-border);background:var(--bui-gray-1);border-radius:var(--bui-radius-full);width:1rem;height:1rem;transition:all .2s;display:block}&[data-pressed]:before{border-color:var(--bui-border)}&[data-selected]{&:before{border-color:var(--bui-bg-solid);border-width:.25rem}&[data-pressed]:before{border-color:var(--bui-bg-solid)}}&[data-focus-visible]:before{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}&[data-invalid]:before,&[data-invalid][data-selected]:before{border-color:var(--bui-border-danger)}&[data-disabled][data-invalid]{color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}}.bui-Table{caption-side:bottom;border-collapse:collapse;width:100%}.bui-TableHeader{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out}.bui-TableHead{text-align:left;padding:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary)}.bui-TableHeadSortButton{cursor:pointer;user-select:none;align-items:center;gap:var(--bui-space-1);display:inline-flex;&:hover svg{opacity:.5}& svg{opacity:0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}&[data-sort-order=asc] svg{opacity:1;transform:rotate(0)}&[data-sort-order=desc] svg{opacity:1;transform:rotate(180deg)}}.bui-TableBody{color:var(--bui-fg-primary)}.bui-TableRow{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out;&[data-react-aria-pressable=true]{cursor:pointer}}.bui-TableBody .bui-TableRow:hover{background-color:var(--bui-gray-2)}.bui-TableCell{padding:var(--bui-space-3);font-size:var(--bui-font-size-3);padding:var(--bui-space-3);font-size:var(--bui-font-size-3)}.bui-TableCellContentWrapper{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:inline-flex}.bui-TableCellIcon,.bui-TableCellIcon svg{color:var(--bui-fg-primary);align-items:center;display:inline-flex}.bui-TableCellContent{gap:var(--bui-space-0_5);flex-direction:column;display:flex}.bui-TableCellProfile{gap:var(--bui-space-2);flex-direction:row;align-items:center;display:flex}.bui-TableCellProfileAvatar{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-TableCellProfileAvatarImage{object-fit:cover;width:100%;height:100%}.bui-TableCellProfileAvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-DataTablePagination{padding-top:var(--bui-space-5);justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--left{justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--right{justify-content:space-between;align-items:center;gap:var(--bui-space-2);display:flex}.bui-DataTablePagination--select{min-width:10.5rem}.bui-Tabs{--active-tab-left:0px;--active-tab-right:0px;--active-tab-top:0px;--active-tab-bottom:0px;--active-tab-width:0px;--active-tab-height:0px;--active-transition-duration:0s;--hovered-tab-left:0px;--hovered-tab-right:0px;--hovered-tab-top:0px;--hovered-tab-bottom:0px;--hovered-tab-width:0px;--hovered-tab-height:0px;--hovered-tab-opacity:0;--hovered-transition-duration:0s}.bui-TabList{flex-direction:row;display:flex}.bui-TabListWrapper{position:relative}.bui-Tab{font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-secondary);cursor:pointer;z-index:2;height:36px;padding-inline:var(--bui-space-2);justify-content:center;align-items:center;display:flex;position:relative;&[data-selected=true]{color:var(--bui-fg-primary)}}.bui-TabActive{content:"";left:calc(var(--active-tab-left) + var(--bui-space-2));width:calc(var(--active-tab-width) - var(--bui-space-4));background-color:var(--bui-fg-primary);height:1px;transition:left var(--active-transition-duration)ease-out,opacity .15s ease-out,width var(--active-transition-duration)ease-out;opacity:1;border-radius:4px;position:absolute;bottom:-1px}.bui-TabHovered{content:"";left:var(--hovered-tab-left);top:calc(var(--hovered-tab-top) + 4px);width:var(--hovered-tab-width);height:calc(var(--hovered-tab-height) - 8px);background-color:var(--bui-gray-2);opacity:var(--hovered-tab-opacity);transition:left var(--hovered-transition-duration)ease-out,top var(--hovered-transition-duration)ease-out,width var(--hovered-transition-duration)ease-out,height var(--hovered-transition-duration)ease-out,opacity .15s ease-out;border-radius:4px;position:absolute}.bui-TabPanel{padding-inline:var(--bui-space-2);padding-top:var(--bui-space-4)}.bui-TagList{gap:var(--bui-space-2);flex-wrap:wrap;display:flex}.bui-Tag{color:var(--bui-fg-primary);background-color:var(--bui-gray-2);border-radius:var(--bui-radius-2);font-weight:var(--bui-font-weight-regular);justify-content:center;align-items:center;gap:var(--bui-space-1);box-sizing:border-box;transition-property:background-color,box-shadow,color;transition-duration:.2s;transition-timing-function:ease-in-out;display:flex}.bui-Tag[data-size=small]{height:26px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-1)}.bui-Tag[data-size=medium]{height:32px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-2)}.bui-Tag[data-hovered]{background-color:var(--bui-gray-3);cursor:pointer}.bui-Tag[data-focus-visible]{outline:2px solid var(--bui-ring);outline-offset:1px}.bui-Tag[data-selected]{box-shadow:inset 0 0 0 1px var(--bui-gray-8)}.bui-Tag[data-disabled]{color:var(--bui-fg-disabled);cursor:not-allowed}.bui-TagRemoveButton{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;width:1rem;height:1rem;margin:0;padding:0}.bui-TagIcon{justify-content:center;align-items:center;transition:color .2s ease-in-out;display:flex;& svg{width:1rem;height:1rem}}.bui-Text{font-family:var(--bui-font-regular);margin:0;padding:0}.bui-Text[data-variant=title-large]{font-size:var(--bui-font-size-8);line-height:140%}.bui-Text[data-variant=title-medium]{font-size:var(--bui-font-size-7);line-height:140%}.bui-Text[data-variant=title-small]{font-size:var(--bui-font-size-6);line-height:140%}.bui-Text[data-variant=title-x-small]{font-size:var(--bui-font-size-5);line-height:140%}.bui-Text[data-variant=body-large]{font-size:var(--bui-font-size-4);line-height:140%}.bui-Text[data-variant=body-medium]{font-size:var(--bui-font-size-3);line-height:140%}.bui-Text[data-variant=body-small]{font-size:var(--bui-font-size-2);line-height:140%}.bui-Text[data-variant=body-x-small]{font-size:var(--bui-font-size-1);line-height:140%}.bui-Text[data-weight=regular]{font-weight:var(--bui-font-weight-regular)}.bui-Text[data-weight=bold]{font-weight:var(--bui-font-weight-bold)}.bui-Text[data-color=primary]{color:var(--bui-fg-primary)}.bui-Text[data-color=secondary]{color:var(--bui-fg-secondary)}.bui-Text[data-color=danger]{color:var(--bui-fg-danger)}.bui-Text[data-color=warning]{color:var(--bui-fg-warning)}.bui-Text[data-color=success]{color:var(--bui-fg-success)}.bui-Text[data-truncate]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.bui-Text[data-as=span],.bui-Text[data-as=label],.bui-Text[data-as=strong],.bui-Text[data-as=em],.bui-Text[data-as=small]{display:inline-block}.bui-TextField{font-family:var(--bui-font-regular);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-PasswordField{font-family:var(--bui-font-regular);--bui-passwordmanager-icon-width:var(--bui-space-1);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputWrapper{position:relative;&[data-size=small] .bui-Input{height:2rem}&[data-size=medium] .bui-Input{height:2.5rem}&[data-size=small] .bui-Input[data-icon]{padding-left:var(--bui-space-8)}&[data-size=medium] .bui-Input[data-icon]{padding-left:var(--bui-space-9)}}.bui-InputAction{right:var(--bui-space-1);color:var(--bui-fg-primary);flex-direction:row;flex-shrink:0;transition:right .2s ease-in-out;display:flex;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-InputVisibility{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;display:flex;&[data-size=small]{width:2rem;height:2rem}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium]{width:2.5rem;height:2.5rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-PasswordField .bui-InputWrapper[data-size=small] .bui-Input{padding-right:calc(2rem + var(--bui-passwordmanager-icon-width))}.bui-PasswordField .bui-InputWrapper[data-size=medium] .bui-Input{padding-right:calc(2.5rem + var(--bui-passwordmanager-icon-width))}.bui-InputIcon{left:var(--bui-space-3);margin-right:var(--bui-space-1);color:var(--bui-fg-primary);pointer-events:none;flex-shrink:0;transition:left .2s ease-in-out;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-Input{padding:0 var(--bui-space-3);border-radius:var(--bui-radius-2);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:100%;cursor:inherit;align-items:center;transition:border-color .2s ease-in-out,outline-color .2s ease-in-out;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}&::placeholder{color:var(--bui-fg-secondary)}&[data-focused]{outline-color:var(--bui-border-pressed);outline-width:0}&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-disabled]{opacity:.5;cursor:not-allowed;border:1px solid var(--bui-border-disabled)}}.bui-SearchField{flex:1 0;&[data-empty]{& .bui-InputClear{display:none}}&[data-start-collapsed=true]{flex:0 auto;padding:0;transition:flex-basis .3s ease-in-out;&[data-collapsed=true]{flex-basis:200px}&[data-collapsed=false]{cursor:pointer;&[data-size=medium]{flex-basis:2.5rem;height:2.5rem}&[data-size=small]{flex-basis:2rem;height:2rem}&[data-size=medium] .bui-Input{&::placeholder{opacity:0}}&[data-size=small] .bui-Input{&::placeholder{opacity:0}}& .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:0}}}}}.bui-SearchField .bui-Input{transition:padding .3s ease-in-out,border-color .2s ease-in-out,outline-color .2s ease-in-out;&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}}.bui-SearchField .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:var(--bui-space-6)}}.bui-SearchField .bui-InputIcon{justify-content:center;display:flex;left:0;&[data-size=small]{width:var(--bui-space-8)}&[data-size=medium]{width:var(--bui-space-10)}}.bui-InputClear{cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;right:0}.bui-InputClear:hover{color:var(--bui-fg-primary)}.bui-InputClear[data-size=small]{width:2rem;height:2rem}.bui-InputClear[data-size=medium]{width:2.5rem;height:2.5rem}.bui-InputClear svg{width:1rem;height:1rem}.bui-Skeleton{animation:var(--bui-animate-pulse);background-color:var(--bui-bg-surface-2);border-radius:var(--bui-radius-2)}.bui-Skeleton[data-rounded=true]{border-radius:var(--bui-radius-full)}.bui-Tooltip{background:var(--bui-bg-surface-1);border:1px solid var(--bui-gray-3);forced-color-adjust:none;padding:var(--bui-space-2)var(--bui-space-3);max-width:240px;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);color:var(--bui-fg-primary);border-radius:4px;outline:none;transition:transform .2s,opacity .2s;transform:translate(0,0);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}--tooltip-offset:var(--bui-space-3)&[data-placement=top]{margin-bottom:var(--tooltip-offset);--origin:translateY(4px)}&[data-placement=right]{margin-left:var(--tooltip-offset);--origin:translateX(-4px)}&[data-placement=bottom]{margin-top:var(--tooltip-offset);--origin:translateY(-4px)}&[data-placement=left]{margin-right:var(--tooltip-offset);--origin:translateX(4px)}}.bui-TooltipArrow{& svg{display:block;& path:first-child{fill:var(--bui-bg-surface-1)}& path:nth-child(2){fill:var(--bui-gray-3)}--tooltip-arrow-overlap:-2px}&[data-placement=top] svg{margin-top:var(--tooltip-arrow-overlap)}&[data-placement=bottom] svg{margin-bottom:var(--tooltip-arrow-overlap);transform:rotate(180deg)}&[data-placement=right] svg{margin-right:var(--tooltip-arrow-overlap);transform:rotate(90deg)}&[data-placement=left] svg{margin-left:var(--tooltip-arrow-overlap);transform:rotate(-90deg)}}[data-theme=dark]{& .bui-Tooltip{background:var(--bui-bg-surface-2);box-shadow:none;border:1px solid var(--bui-gray-4)}& .bui-TooltipArrow{& svg path:first-child{fill:var(--bui-bg-surface-2)}& svg path:nth-child(2){fill:var(--bui-gray-4)}}}.bui-ScrollAreaRoot{box-sizing:border-box;width:100%}.bui-ScrollAreaViewport{overscroll-behavior:contain;height:100%}.bui-ScrollAreaContent{padding-block:.75rem;flex-direction:column;gap:1rem;padding-left:1rem;padding-right:1.5rem;display:flex}.bui-ScrollAreaScrollbar{background-color:var(--bui-scrollbar);opacity:0;border-radius:.375rem;justify-content:center;width:.25rem;margin:.5rem;transition:opacity .15s .3s;display:flex;&[data-hovering],&[data-scrolling]{opacity:1;transition-duration:75ms;transition-delay:0s}&:before{content:"";width:1.25rem;height:100%;position:absolute}}.bui-ScrollAreaThumb{border-radius:inherit;background-color:var(--bui-scrollbar-thumb);width:100%}.bui-Select[data-invalid]{& .bui-SelectTrigger{border-color:var(--bui-fg-danger)}}.bui-SelectTrigger{box-sizing:border-box;border-radius:var(--bui-radius-3);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);cursor:pointer;justify-content:space-between;align-items:center;gap:var(--bui-space-2);width:100%;display:flex;& svg{color:var(--bui-fg-secondary);flex-shrink:0}&[data-size=small]{height:2rem;padding-inline:var(--bui-space-3)}&[data-size=medium]{height:3rem;padding-inline:var(--bui-space-4)}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}&::placeholder{color:var(--bui-fg-secondary)}&:hover{border-color:var(--bui-border-hover);transition:border-color .2s ease-in-out,outline-color .2s ease-in-out}&:focus-visible{border-color:var(--bui-border-pressed);outline:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-invalid]:hover,&[data-invalid]:focus-visible{border-width:2px}&[disabled]{cursor:not-allowed;border-color:var(--bui-border-disabled);color:var(--bui-fg-disabled)}&[disabled] .bui-SelectValue{color:var(--bui-fg-disabled)}&[data-popup-open] .bui-SelectIcon{transform:rotate(180deg)}}.bui-SelectValue{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);text-align:left;overflow:hidden;& .bui-SelectItemIndicator{display:none}&[disabled]{color:var(--bui-fg-disabled)}}.bui-SelectItem{width:var(--anchor-width);padding-block:var(--bui-space-2);padding-left:var(--bui-space-3);padding-right:var(--bui-space-4);color:var(--bui-fg-primary);border-radius:var(--bui-radius-3);cursor:pointer;user-select:none;font-size:var(--bui-font-size-3);align-items:center;gap:var(--bui-space-1);outline:none;grid-template-columns:1rem 1fr;grid-template-areas:"icon text";display:grid;position:relative;&[data-focused]{z-index:0;color:var(--bui-fg-primary);position:relative}&[data-focused]:before{content:"";z-index:-1;background-color:var(--bui-bg-tint-hover);border-radius:.25rem;position:absolute;inset-block:0;inset-inline:.25rem}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled)}&[data-selected] .bui-SelectItemIndicator{opacity:1}}.bui-SelectItemIndicator{opacity:0;grid-area:icon;justify-content:center;align-items:center;transition:opacity .2s ease-in-out;display:flex}.bui-SelectItemLabel{flex:1;grid-area:text}.bui-Switch{align-items:center;gap:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary);cursor:pointer;display:flex;position:relative;&[data-pressed] .bui-SwitchIndicator{&:before{background:var(--bui-fg-solid)}}&[data-selected]{& .bui-SwitchIndicator{background:var(--bui-bg-solid);&:before{background:var(--bui-fg-solid);transform:translate(100%)}}&[data-pressed]{& .indicator{background:var(--bui-gray-3)}}}&[data-focus-visible] .bui-SwitchIndicator{outline-offset:2px;outline:2px solid}}.bui-SwitchIndicator{background:var(--bui-gray-3);border:2px;border-radius:1.143rem;width:2rem;height:1.143rem;transition:all .2s;&:before{content:"";background:var(--bui-fg-solid);border-radius:16px;width:.857rem;height:.857rem;margin:.143rem;transition:all .2s;display:block}} \ No newline at end of file +@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}}.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--pl-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--pl-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--pl-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--pl-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--pl-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--pr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--pr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--pr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--pr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--pr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--pt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--pt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--pt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--pt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--pt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--pb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--pb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--pb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--pb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--pb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--py-xs);margin-bottom:var(--py-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--py-sm);margin-bottom:var(--py-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--py-md);margin-bottom:var(--py-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--py-lg);margin-bottom:var(--py-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--py-xl);margin-bottom:var(--py-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--px-xs);margin-right:var(--px-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--px-sm);margin-right:var(--px-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--px-md);margin-right:var(--px-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--px-lg);margin-right:var(--px-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--px-xl);margin-right:var(--px-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-baseline{align-items:baseline}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#ebebeb;--bui-bg-tint:transparent;--bui-bg-tint-hover:#1f549366;--bui-bg-tint-pressed:#1f549399;--bui-bg-tint-disabled:#ebebeb;--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#9c9c9c;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg:#333;--bui-bg-surface-1:#424242;--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#222;--bui-bg-tint:transparent;--bui-bg-tint-hover:#9cc9ff1f;--bui-bg-tint-pressed:#9cc9ff29;--bui-bg-tint-disabled:transparent;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:var(--bui-gray-5);--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757}.bui-AvatarRoot{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:2rem;height:2rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-AvatarRoot[data-size=small]{width:1.5rem;height:1.5rem}.bui-AvatarRoot[data-size=medium]{width:2rem;height:2rem}.bui-AvatarRoot[data-size=large]{width:3rem;height:3rem}.bui-AvatarImage{object-fit:cover;width:100%;height:100%}.bui-AvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-Box{font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary)}.bui-Button{user-select:none;font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-bold);cursor:pointer;border-radius:var(--bui-radius-2);justify-content:center;align-items:center;gap:var(--bui-space-1_5);border:none;flex-shrink:0;padding:0;display:inline-flex;&[data-disabled=true]{cursor:not-allowed}}.bui-Button[data-variant=primary]{background-color:var(--bui-bg-solid);color:var(--bui-fg-solid);&:hover{background-color:var(--bui-bg-solid-hover);transition:background-color .15s}&:active{background-color:var(--bui-bg-solid-pressed)}&:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled=true]{background-color:var(--bui-bg-solid-disabled);color:var(--bui-fg-solid-disabled)}}.bui-Button[data-variant=secondary]{background-color:var(--bui-bg-surface-1);box-shadow:inset 0 0 0 1px var(--bui-border);color:var(--bui-fg-primary);&:hover{box-shadow:inset 0 0 0 1px var(--bui-border-hover);transition:box-shadow .15s}&:active{box-shadow:inset 0 0 0 1px var(--bui-border-pressed)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{box-shadow:inset 0 0 0 1px var(--bui-border-disabled);color:var(--bui-fg-disabled)}}.bui-Button[data-variant=tertiary]{color:var(--bui-fg-primary);background-color:#0000;&:hover{background-color:var(--bui-bg-surface-1);transition:background-color .2s}&:active{background-color:var(--bui-bg-surface-2)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{color:var(--bui-fg-disabled);background-color:#0000}}.bui-Button[data-size=medium]{font-size:var(--bui-font-size-4);padding:0 var(--bui-space-3);height:2.5rem}.bui-Button[data-size=small]{font-size:var(--bui-font-size-3);padding:0 var(--bui-space-2);height:2rem}.bui-Button[data-size=small] svg{width:1rem;height:1rem}.bui-Button[data-size=medium] svg{width:1.25rem;height:1.25rem}.bui-ButtonIcon{justify-content:center;align-items:center}.bui-ButtonIcon[data-size=small]{width:2rem;padding:0}.bui-ButtonIcon[data-size=medium]{width:2.5rem;padding:0}.bui-Card{gap:var(--bui-space-3);background-color:var(--bui-bg-surface-1);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-3);color:var(--bui-fg-primary);border:1px solid var(--bui-border);flex-direction:column;width:100%;min-height:0;display:flex;overflow:hidden}.bui-CardBody{flex:1;min-height:0;overflow:auto}.bui-CardHeader,.bui-CardFooter{padding-inline:var(--bui-space-3)}.bui-CheckboxRoot{width:1rem;height:1rem;box-shadow:inset 0 0 0 1px var(--bui-border);cursor:pointer;background-color:var(--bui-bg-surface-1);border:none;border-radius:2px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background-color .2s ease-in-out;display:flex}.bui-CheckboxRoot:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px;transition:none}.bui-CheckboxRoot[data-checked]{background-color:var(--bui-bg-solid);box-shadow:none;color:var(--bui-fg-solid)}.bui-CheckboxLabel{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);user-select:none;flex-direction:row;display:flex;&:hover{& .bui-CheckboxRoot:not([data-checked]){box-shadow:inset 0 0 0 1px var(--bui-border-hover)}}}.bui-CheckboxIndicator{color:var(--bui-fg-solid);justify-content:center;align-items:center;display:flex}.bui-CollapsiblePanel{height:var(--collapsible-panel-height);transition:all .15s ease-out;display:flex;overflow:hidden;&[data-starting-style],&[data-ending-style]{height:0}}.bui-Container{max-width:120rem;padding-inline:var(--bui-space-4);margin-inline:auto;transition:padding .2s ease-in-out}@media (width>=640px){.bui-Container{padding-inline:var(--bui-space-5)}}.bui-FieldError{color:var(--bui-fg-danger);font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);margin-top:var(--bui-space-2);display:inline-block}.bui-FieldLabelWrapper{margin-bottom:var(--bui-space-3);gap:var(--bui-space-1);flex-direction:column;display:flex}.bui-FieldLabel{color:var(--bui-fg-primary);cursor:pointer;font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);margin-right:auto}.bui-FieldSecondaryLabel{color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular);margin-left:var(--bui-space-1)}.bui-FieldDescription{font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);margin:0}.bui-Flex{min-width:0;display:flex}.bui-Grid{display:grid}.bui-HeaderToolbar{margin-bottom:var(--bui-space-6);&:before{content:"";background-color:var(--bui-bg);z-index:0;height:16px;position:absolute;top:0;left:0;right:0}&[data-has-tabs=true]{margin-bottom:0}}.bui-HeaderToolbarWrapper{z-index:1;background-color:var(--bui-bg-surface-1);padding-inline:var(--bui-space-5);border-bottom:1px solid var(--bui-border);color:var(--bui-fg-primary);flex-direction:row;justify-content:space-between;align-items:center;height:52px;display:flex;position:relative}.bui-HeaderToolbarContent{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-HeaderToolbarName{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);flex-direction:row;flex-shrink:0;display:flex}.bui-HeaderToolbarIcon{width:16px;height:16px;color:var(--bui-fg-primary);& svg{width:100%;height:100%}}.bui-HeaderToolbarControls{right:var(--bui-space-5);align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.bui-HeaderTabsWrapper{margin-bottom:var(--bui-space-4);padding-inline:var(--bui-space-3);border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1)}.bui-HeaderPage{gap:var(--bui-space-1);margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6);flex-direction:column;display:flex}.bui-HeaderPageContent{flex-direction:row;justify-content:space-between;display:flex}.bui-HeaderPageTabsWrapper{margin-left:-8px}.bui-HeaderPageControls,.bui-HeaderPageBreadcrumbs{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-Icon{width:1rem;height:1rem}.bui-Link{font-family:var(--bui-font-regular);cursor:pointer;margin:0;padding:0;text-decoration-line:none;display:inline-block;&:hover{text-underline-offset:calc(.025em + 2px);text-decoration-line:underline;text-decoration-style:solid;text-decoration-thickness:min(2px,max(1px,.05em));text-decoration-color:color-mix(in srgb,currentColor 30%,transparent)}}.bui-MenuPopover{border:1px solid var(--bui-border);border-radius:var(--bui-radius-2);background:var(--bui-bg-surface-1);color:var(--bui-fg-primary);outline:none;flex-direction:column;min-height:0;transition:transform .2s,opacity .2s;display:flex;overflow:hidden;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}&[data-placement=top]{--origin:translateY(8px)}&[data-placement=bottom]{--origin:translateY(-8px)}&[data-placement=right]{--origin:translateX(-8px)}&[data-placement=left]{--origin:translateX(8px)}}.bui-MenuContent{max-height:inherit;box-sizing:border-box;padding:var(--bui-space-1);outline:none;min-width:150px;overflow:auto}.bui-MenuPopover .bui-ScrollAreaRoot{flex-direction:column;flex:1;height:100%;min-height:0;display:flex}.bui-MenuPopover .bui-ScrollAreaScrollbar{margin-inline:var(--bui-space-1_5)}.bui-MenuItem{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&[data-focused],&[data-open]{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-color=danger]{color:var(--bui-fg-danger)}&[data-color=danger][data-focused]{background:var(--bui-bg-danger);color:var(--bui-fg-danger)}&[data-has-submenu]{&>.bui-MenuItemArrow{display:block}}}.bui-MenuItemListBox{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&:hover{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-selected] .bui-MenuItemListBoxCheck{&>svg{opacity:1;color:var(--bui-fg-primary)}}}.bui-MenuItemListBoxCheck{justify-content:center;align-items:center;width:1rem;height:1rem;display:flex;&>svg{opacity:0;width:1rem;height:1rem}}.bui-MenuItemContent{align-items:center;gap:var(--bui-space-2);display:flex;&>svg{width:1rem;height:1rem}}.bui-MenuItemArrow{width:1rem;height:1rem;display:none;&>svg{width:1rem;height:1rem}}.bui-MenuSection{&:first-child .bui-MenuSectionHeader{padding-top:0}}.bui-MenuSectionHeader{height:2rem;padding-top:var(--bui-space-3);padding-left:var(--bui-space-2);color:var(--bui-fg-primary);font-size:var(--bui-font-size-1);letter-spacing:.05rem;text-transform:uppercase;align-items:center;font-weight:700;display:flex}.bui-MenuSeparator{background:var(--bui-border);height:1px;margin-inline:var(--bui-space-1_5);margin-block:var(--bui-space-1)}.bui-MenuSearchField{font-family:var(--bui-font-regular);flex-shrink:0;width:100%;position:relative;&[data-empty]{& .bui-MenuSearchFieldClear{display:none}}}.bui-MenuSearchFieldInput{padding:0 var(--bui-space-3);border:none;border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:2rem;cursor:inherit;outline:none;align-items:center;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}}.bui-MenuSearchFieldClear{right:var(--bui-space-2);cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;&>svg{width:1rem;height:1rem}}.bui-MenuEmptyState{padding:var(--bui-space-1);color:var(--bui-fg-secondary);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular)}.bui-Popover{background-color:var(--bui-bg-surface-1);border:1px solid var(--bui-border);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-1);margin-right:12px;overflow:scroll;box-shadow:0 4px 12px #0000001a}.bui-RadioGroup{color:var(--bui-fg-primary);flex-direction:column;display:flex}.bui-RadioGroup[data-orientation=horizontal] .bui-RadioGroupContent{gap:var(--bui-space-4);flex-direction:row}.bui-RadioGroupContent{gap:var(--bui-space-2);flex-direction:column;display:flex}.bui-Radio{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-2);color:var(--bui-fg-primary);forced-color-adjust:none;display:flex;position:relative;&:before{content:"";box-sizing:border-box;border:.125rem solid var(--bui-border);background:var(--bui-gray-1);border-radius:var(--bui-radius-full);width:1rem;height:1rem;transition:all .2s;display:block}&[data-pressed]:before{border-color:var(--bui-border)}&[data-selected]{&:before{border-color:var(--bui-bg-solid);border-width:.25rem}&[data-pressed]:before{border-color:var(--bui-bg-solid)}}&[data-focus-visible]:before{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}&[data-invalid]:before,&[data-invalid][data-selected]:before{border-color:var(--bui-border-danger)}&[data-disabled][data-invalid]{color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}}.bui-Table{caption-side:bottom;border-collapse:collapse;width:100%}.bui-TableHeader{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out}.bui-TableHead{text-align:left;padding:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary)}.bui-TableHeadSortButton{cursor:pointer;user-select:none;align-items:center;gap:var(--bui-space-1);display:inline-flex;&:hover svg{opacity:.5}& svg{opacity:0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}&[data-sort-order=asc] svg{opacity:1;transform:rotate(0)}&[data-sort-order=desc] svg{opacity:1;transform:rotate(180deg)}}.bui-TableBody{color:var(--bui-fg-primary)}.bui-TableRow{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out;&[data-react-aria-pressable=true]{cursor:pointer}}.bui-TableBody .bui-TableRow:hover{background-color:var(--bui-gray-2)}.bui-TableCell{padding:var(--bui-space-3);font-size:var(--bui-font-size-3);padding:var(--bui-space-3);font-size:var(--bui-font-size-3)}.bui-TableCellContentWrapper{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:inline-flex}.bui-TableCellIcon,.bui-TableCellIcon svg{color:var(--bui-fg-primary);align-items:center;display:inline-flex}.bui-TableCellContent{gap:var(--bui-space-0_5);flex-direction:column;display:flex}.bui-TableCellProfile{gap:var(--bui-space-2);flex-direction:row;align-items:center;display:flex}.bui-TableCellProfileAvatar{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-TableCellProfileAvatarImage{object-fit:cover;width:100%;height:100%}.bui-TableCellProfileAvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-DataTablePagination{padding-top:var(--bui-space-5);justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--left{justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--right{justify-content:space-between;align-items:center;gap:var(--bui-space-2);display:flex}.bui-DataTablePagination--select{min-width:10.5rem}.bui-Tabs{--active-tab-left:0px;--active-tab-right:0px;--active-tab-top:0px;--active-tab-bottom:0px;--active-tab-width:0px;--active-tab-height:0px;--active-transition-duration:0s;--hovered-tab-left:0px;--hovered-tab-right:0px;--hovered-tab-top:0px;--hovered-tab-bottom:0px;--hovered-tab-width:0px;--hovered-tab-height:0px;--hovered-tab-opacity:0;--hovered-transition-duration:0s}.bui-TabList{flex-direction:row;display:flex}.bui-TabListWrapper{position:relative}.bui-Tab{font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-secondary);cursor:pointer;z-index:2;height:36px;padding-inline:var(--bui-space-2);justify-content:center;align-items:center;display:flex;position:relative;&[data-selected=true]{color:var(--bui-fg-primary)}}.bui-TabActive{content:"";left:calc(var(--active-tab-left) + var(--bui-space-2));width:calc(var(--active-tab-width) - var(--bui-space-4));background-color:var(--bui-fg-primary);height:1px;transition:left var(--active-transition-duration)ease-out,opacity .15s ease-out,width var(--active-transition-duration)ease-out;opacity:1;border-radius:4px;position:absolute;bottom:-1px}.bui-TabHovered{content:"";left:var(--hovered-tab-left);top:calc(var(--hovered-tab-top) + 4px);width:var(--hovered-tab-width);height:calc(var(--hovered-tab-height) - 8px);background-color:var(--bui-gray-2);opacity:var(--hovered-tab-opacity);transition:left var(--hovered-transition-duration)ease-out,top var(--hovered-transition-duration)ease-out,width var(--hovered-transition-duration)ease-out,height var(--hovered-transition-duration)ease-out,opacity .15s ease-out;border-radius:4px;position:absolute}.bui-TabPanel{padding-inline:var(--bui-space-2);padding-top:var(--bui-space-4)}.bui-TagList{gap:var(--bui-space-2);flex-wrap:wrap;display:flex}.bui-Tag{color:var(--bui-fg-primary);background-color:var(--bui-gray-2);border-radius:var(--bui-radius-2);font-weight:var(--bui-font-weight-regular);justify-content:center;align-items:center;gap:var(--bui-space-1);box-sizing:border-box;transition-property:background-color,box-shadow,color;transition-duration:.2s;transition-timing-function:ease-in-out;display:flex}.bui-Tag[data-size=small]{height:26px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-1)}.bui-Tag[data-size=medium]{height:32px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-2)}.bui-Tag[data-hovered]{background-color:var(--bui-gray-3);cursor:pointer}.bui-Tag[data-focus-visible]{outline:2px solid var(--bui-ring);outline-offset:1px}.bui-Tag[data-selected]{box-shadow:inset 0 0 0 1px var(--bui-gray-8)}.bui-Tag[data-disabled]{color:var(--bui-fg-disabled);cursor:not-allowed}.bui-TagRemoveButton{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;width:1rem;height:1rem;margin:0;padding:0}.bui-TagIcon{justify-content:center;align-items:center;transition:color .2s ease-in-out;display:flex;& svg{width:1rem;height:1rem}}.bui-Text{font-family:var(--bui-font-regular);margin:0;padding:0}.bui-Text[data-variant=title-large]{font-size:var(--bui-font-size-8);line-height:140%}.bui-Text[data-variant=title-medium]{font-size:var(--bui-font-size-7);line-height:140%}.bui-Text[data-variant=title-small]{font-size:var(--bui-font-size-6);line-height:140%}.bui-Text[data-variant=title-x-small]{font-size:var(--bui-font-size-5);line-height:140%}.bui-Text[data-variant=body-large]{font-size:var(--bui-font-size-4);line-height:140%}.bui-Text[data-variant=body-medium]{font-size:var(--bui-font-size-3);line-height:140%}.bui-Text[data-variant=body-small]{font-size:var(--bui-font-size-2);line-height:140%}.bui-Text[data-variant=body-x-small]{font-size:var(--bui-font-size-1);line-height:140%}.bui-Text[data-weight=regular]{font-weight:var(--bui-font-weight-regular)}.bui-Text[data-weight=bold]{font-weight:var(--bui-font-weight-bold)}.bui-Text[data-color=primary]{color:var(--bui-fg-primary)}.bui-Text[data-color=secondary]{color:var(--bui-fg-secondary)}.bui-Text[data-color=danger]{color:var(--bui-fg-danger)}.bui-Text[data-color=warning]{color:var(--bui-fg-warning)}.bui-Text[data-color=success]{color:var(--bui-fg-success)}.bui-Text[data-truncate]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.bui-Text[data-as=span],.bui-Text[data-as=label],.bui-Text[data-as=strong],.bui-Text[data-as=em],.bui-Text[data-as=small]{display:inline-block}.bui-TextField{font-family:var(--bui-font-regular);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputWrapper{position:relative;&[data-size=small] .bui-Input{height:2rem}&[data-size=medium] .bui-Input{height:2.5rem}&[data-size=small] .bui-Input[data-icon]{padding-left:var(--bui-space-8)}&[data-size=medium] .bui-Input[data-icon]{padding-left:var(--bui-space-9)}}.bui-InputIcon{left:var(--bui-space-3);margin-right:var(--bui-space-1);color:var(--bui-fg-primary);pointer-events:none;flex-shrink:0;transition:left .2s ease-in-out;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-Input{padding:0 var(--bui-space-3);border-radius:var(--bui-radius-2);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:100%;cursor:inherit;align-items:center;transition:border-color .2s ease-in-out,outline-color .2s ease-in-out;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}&::placeholder{color:var(--bui-fg-secondary)}&[data-focused]{outline-color:var(--bui-border-pressed);outline-width:0}&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-disabled]{opacity:.5;cursor:not-allowed;border:1px solid var(--bui-border-disabled)}}.bui-PasswordField{font-family:var(--bui-font-regular);--bui-passwordmanager-icon-width:var(--bui-space-1);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputVisibility{right:var(--bui-passwordmanager-icon-width);cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;display:flex;position:absolute;top:0;bottom:0;&[data-size=small]{width:2rem;height:2rem}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium]{width:2.5rem;height:2.5rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-PasswordField .bui-InputWrapper[data-size=small] .bui-Input{padding-right:calc(2rem + var(--bui-passwordmanager-icon-width))}.bui-PasswordField .bui-InputWrapper[data-size=medium] .bui-Input{padding-right:calc(2.5rem + var(--bui-passwordmanager-icon-width))}.bui-SearchField{flex:1 0;&[data-empty]{& .bui-InputClear{display:none}}&[data-start-collapsed=true]{flex:0 auto;padding:0;transition:flex-basis .3s ease-in-out;&[data-collapsed=true]{flex-basis:200px}&[data-collapsed=false]{cursor:pointer;&[data-size=medium]{flex-basis:2.5rem;height:2.5rem}&[data-size=small]{flex-basis:2rem;height:2rem}&[data-size=medium] .bui-Input{&::placeholder{opacity:0}}&[data-size=small] .bui-Input{&::placeholder{opacity:0}}& .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:0}}}}}.bui-SearchField .bui-Input{transition:padding .3s ease-in-out,border-color .2s ease-in-out,outline-color .2s ease-in-out;&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}}.bui-SearchField .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:var(--bui-space-6)}}.bui-SearchField .bui-InputIcon{justify-content:center;display:flex;left:0;&[data-size=small]{width:var(--bui-space-8)}&[data-size=medium]{width:var(--bui-space-10)}}.bui-InputClear{cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;right:0}.bui-InputClear:hover{color:var(--bui-fg-primary)}.bui-InputClear[data-size=small]{width:2rem;height:2rem}.bui-InputClear[data-size=medium]{width:2.5rem;height:2.5rem}.bui-InputClear svg{width:1rem;height:1rem}.bui-Skeleton{animation:var(--bui-animate-pulse);background-color:var(--bui-bg-surface-2);border-radius:var(--bui-radius-2)}.bui-Skeleton[data-rounded=true]{border-radius:var(--bui-radius-full)}.bui-Tooltip{background:var(--bui-bg-surface-1);border:1px solid var(--bui-gray-3);forced-color-adjust:none;padding:var(--bui-space-2)var(--bui-space-3);max-width:240px;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);color:var(--bui-fg-primary);border-radius:4px;outline:none;transition:transform .2s,opacity .2s;transform:translate(0,0);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}--tooltip-offset:var(--bui-space-3)&[data-placement=top]{margin-bottom:var(--tooltip-offset);--origin:translateY(4px)}&[data-placement=right]{margin-left:var(--tooltip-offset);--origin:translateX(-4px)}&[data-placement=bottom]{margin-top:var(--tooltip-offset);--origin:translateY(-4px)}&[data-placement=left]{margin-right:var(--tooltip-offset);--origin:translateX(4px)}}.bui-TooltipArrow{& svg{display:block;& path:first-child{fill:var(--bui-bg-surface-1)}& path:nth-child(2){fill:var(--bui-gray-3)}--tooltip-arrow-overlap:-2px}&[data-placement=top] svg{margin-top:var(--tooltip-arrow-overlap)}&[data-placement=bottom] svg{margin-bottom:var(--tooltip-arrow-overlap);transform:rotate(180deg)}&[data-placement=right] svg{margin-right:var(--tooltip-arrow-overlap);transform:rotate(90deg)}&[data-placement=left] svg{margin-left:var(--tooltip-arrow-overlap);transform:rotate(-90deg)}}[data-theme=dark]{& .bui-Tooltip{background:var(--bui-bg-surface-2);box-shadow:none;border:1px solid var(--bui-gray-4)}& .bui-TooltipArrow{& svg path:first-child{fill:var(--bui-bg-surface-2)}& svg path:nth-child(2){fill:var(--bui-gray-4)}}}.bui-ScrollAreaRoot{box-sizing:border-box;width:100%}.bui-ScrollAreaViewport{overscroll-behavior:contain;height:100%}.bui-ScrollAreaContent{padding-block:.75rem;flex-direction:column;gap:1rem;padding-left:1rem;padding-right:1.5rem;display:flex}.bui-ScrollAreaScrollbar{background-color:var(--bui-scrollbar);opacity:0;border-radius:.375rem;justify-content:center;width:.25rem;margin:.5rem;transition:opacity .15s .3s;display:flex;&[data-hovering],&[data-scrolling]{opacity:1;transition-duration:75ms;transition-delay:0s}&:before{content:"";width:1.25rem;height:100%;position:absolute}}.bui-ScrollAreaThumb{border-radius:inherit;background-color:var(--bui-scrollbar-thumb);width:100%}.bui-Select[data-invalid]{& .bui-SelectTrigger{border-color:var(--bui-fg-danger)}}.bui-SelectTrigger{box-sizing:border-box;border-radius:var(--bui-radius-3);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);cursor:pointer;justify-content:space-between;align-items:center;gap:var(--bui-space-2);width:100%;display:flex;& svg{color:var(--bui-fg-secondary);flex-shrink:0}&[data-size=small]{height:2rem;padding-inline:var(--bui-space-3)}&[data-size=medium]{height:3rem;padding-inline:var(--bui-space-4)}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}&::placeholder{color:var(--bui-fg-secondary)}&:hover{border-color:var(--bui-border-hover);transition:border-color .2s ease-in-out,outline-color .2s ease-in-out}&:focus-visible{border-color:var(--bui-border-pressed);outline:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-invalid]:hover,&[data-invalid]:focus-visible{border-width:2px}&[disabled]{cursor:not-allowed;border-color:var(--bui-border-disabled);color:var(--bui-fg-disabled)}&[disabled] .bui-SelectValue{color:var(--bui-fg-disabled)}&[data-popup-open] .bui-SelectIcon{transform:rotate(180deg)}}.bui-SelectValue{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);text-align:left;overflow:hidden;& .bui-SelectItemIndicator{display:none}&[disabled]{color:var(--bui-fg-disabled)}}.bui-SelectItem{width:var(--anchor-width);padding-block:var(--bui-space-2);padding-left:var(--bui-space-3);padding-right:var(--bui-space-4);color:var(--bui-fg-primary);border-radius:var(--bui-radius-3);cursor:pointer;user-select:none;font-size:var(--bui-font-size-3);align-items:center;gap:var(--bui-space-1);outline:none;grid-template-columns:1rem 1fr;grid-template-areas:"icon text";display:grid;position:relative;&[data-focused]{z-index:0;color:var(--bui-fg-primary);position:relative}&[data-focused]:before{content:"";z-index:-1;background-color:var(--bui-bg-tint-hover);border-radius:.25rem;position:absolute;inset-block:0;inset-inline:.25rem}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled)}&[data-selected] .bui-SelectItemIndicator{opacity:1}}.bui-SelectItemIndicator{opacity:0;grid-area:icon;justify-content:center;align-items:center;transition:opacity .2s ease-in-out;display:flex}.bui-SelectItemLabel{flex:1;grid-area:text}.bui-Switch{align-items:center;gap:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary);cursor:pointer;display:flex;position:relative;&[data-pressed] .bui-SwitchIndicator{&:before{background:var(--bui-fg-solid)}}&[data-selected]{& .bui-SwitchIndicator{background:var(--bui-bg-solid);&:before{background:var(--bui-fg-solid);transform:translate(100%)}}&[data-pressed]{& .indicator{background:var(--bui-gray-3)}}}&[data-focus-visible] .bui-SwitchIndicator{outline-offset:2px;outline:2px solid}}.bui-SwitchIndicator{background:var(--bui-gray-3);border:2px;border-radius:1.143rem;width:2rem;height:1.143rem;transition:all .2s;&:before{content:"";background:var(--bui-fg-solid);border-radius:16px;width:.857rem;height:.857rem;margin:.143rem;transition:all .2s;display:block}} \ No newline at end of file diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index 0554276dc2..d93ba87829 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -10431,16 +10431,6 @@ display: flex; } -.bui-PasswordField { - font-family: var(--bui-font-regular); - flex-direction: column; - flex-shrink: 0; - width: 100%; - display: flex; - /* Reserve space for browser/password manager icon (e.g. 1Password) */ - --bui-passwordmanager-icon-width: var(--bui-space-1); -} - .bui-InputWrapper { position: relative; @@ -10461,60 +10451,6 @@ } } -.bui-InputAction { - position: absolute; - display: flex; - flex-direction: row; - right: var(--bui-space-1); - top: 50%; - transform: translateY(-50%); - color: var(--bui-fg-primary); - flex-shrink: 0; - /* To animate the icon when the input is collapsed */ - transition: right 0.2s ease-in-out; - - &[data-size='small'], - &[data-size='small'] svg { - width: 1rem; - height: 1rem; - } - - &[data-size='medium'], - &[data-size='medium'] svg { - width: 1.25rem; - height: 1.25rem; - } -} - -.bui-InputVisibility { - background-color: transparent; - cursor: pointer; - border: none; - padding: 0; - margin: 0; - display: flex; - align-items: center; - justify-content: center; - color: var(--bui-fg-primary); - - /* Size: small */ - &[data-size="small"] { width: 2rem; height: 2rem; } - &[data-size="small"] svg { width: 1rem; height: 1rem; } - - /* Size: medium */ - &[data-size="medium"] { width: 2.5rem; height: 2.5rem; } - &[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } -} - -/* Ensure input has enough right padding for our toggle + PM icon */ -.bui-PasswordField .bui-InputWrapper[data-size='small'] .bui-Input { - padding-right: calc(2rem + var(--bui-passwordmanager-icon-width)); -} - -.bui-PasswordField .bui-InputWrapper[data-size='medium'] .bui-Input { - padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width)); -} - .bui-InputIcon { left: var(--bui-space-3); margin-right: var(--bui-space-1); @@ -10586,6 +10522,59 @@ } } +.bui-PasswordField { + font-family: var(--bui-font-regular); + --bui-passwordmanager-icon-width: var(--bui-space-1); + flex-direction: column; + flex-shrink: 0; + width: 100%; + display: flex; +} + +.bui-InputVisibility { + right: var(--bui-passwordmanager-icon-width); + cursor: pointer; + color: var(--bui-fg-primary); + background-color: #0000; + border: none; + justify-content: center; + align-items: center; + margin: 0; + padding: 0; + display: flex; + position: absolute; + top: 0; + bottom: 0; + + &[data-size="small"] { + width: 2rem; + height: 2rem; + } + + &[data-size="small"] svg { + width: 1rem; + height: 1rem; + } + + &[data-size="medium"] { + width: 2.5rem; + height: 2.5rem; + } + + &[data-size="medium"] svg { + width: 1.25rem; + height: 1.25rem; + } +} + +.bui-PasswordField .bui-InputWrapper[data-size="small"] .bui-Input { + padding-right: calc(2rem + var(--bui-passwordmanager-icon-width)); +} + +.bui-PasswordField .bui-InputWrapper[data-size="medium"] .bui-Input { + padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width)); +} + .bui-SearchField { flex: 1 0; From 1ca1b96e3048b54b7ce26e18bbe20da550a85529 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 26 Sep 2025 13:28:30 -0500 Subject: [PATCH 135/193] Removed the private flag so that we can publish the BUI Themer Signed-off-by: Andre Wanlin --- plugins/bui-themer/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json index a04bfbf474..70828fb893 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/bui-themer/package.json @@ -13,7 +13,6 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, - "private": true, "repository": { "type": "git", "url": "https://github.com/backstage/backstage", From 82540078e800ebeb26d6d0f751b67019ee682b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Mon, 29 Sep 2025 10:00:18 +0200 Subject: [PATCH 136/193] chore: update package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b510a5a864..babce203dd 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@yarnpkg/plugin-npm@npm:^3.1.0": "patch:@yarnpkg/plugin-npm@npm%3A3.1.0#~/.yarn/patches/@yarnpkg-plugin-npm-npm-3.1.0-6533d0f5a1.patch", + "GendocuPublicApis": "npm:gendocu-public-apis@^1.0.0", "ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", From 95935fbe22c379c545e0abd9ac1f7f2491e7ece8 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Fri, 26 Sep 2025 21:32:44 +0300 Subject: [PATCH 137/193] fix(core-components): fix deps graph scrolling fixes dependency graph scrolling down forever. also fixes the full screen mode to take all the space in the screen. closes #31296 Signed-off-by: Hellgren Heikki --- .changeset/public-wombats-say.md | 5 ++ .../DependencyGraph/DependencyGraph.tsx | 72 +++++++++---------- 2 files changed, 41 insertions(+), 36 deletions(-) create mode 100644 .changeset/public-wombats-say.md diff --git a/.changeset/public-wombats-say.md b/.changeset/public-wombats-say.md new file mode 100644 index 0000000000..b8a72e8436 --- /dev/null +++ b/.changeset/public-wombats-say.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fixed dependency graph automatically scrolling forever diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index df26bb86fd..88cb6ab5e8 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -54,7 +54,7 @@ const useStyles = makeStyles((theme: Theme) => ({ minWidth: '100%', }, fixedHeight: { - height: '100%', + maxHeight: '100%', }, fullscreen: { backgroundColor: theme.palette.background.paper, @@ -282,7 +282,8 @@ export function DependencyGraph( const [_measureRef] = useMeasure(); const measureRef = once(_measureRef); - const scalableHeight = fit === 'grow' ? maxHeight : containerHeight; + const scalableHeight = + fit === 'grow' && !fullScreenHandle.active ? maxHeight : '100%'; const containerRef = useMemo( () => @@ -335,10 +336,16 @@ export function DependencyGraph( const { width: newContainerWidth, height: newContainerHeight } = root.getBoundingClientRect(); - if (containerWidth !== newContainerWidth) { + if ( + containerWidth !== newContainerWidth && + newContainerWidth <= maxWidth + ) { setContainerWidth(newContainerWidth); } - if (containerHeight !== newContainerHeight) { + if ( + containerHeight !== newContainerHeight && + newContainerHeight <= maxHeight + ) { setContainerHeight(newContainerHeight); } }, 100), @@ -464,39 +471,32 @@ export function DependencyGraph( ); return ( -
- - {allowFullscreen && ( - - - {fullScreenHandle.active ? ( - - ) : ( - - )} - - - )} + {allowFullscreen && ( + + + {fullScreenHandle.active ? ( + + ) : ( + + )} + + + )} +
( height={graphHeight} y={maxHeight / 2 - graphHeight / 2} x={maxWidth / 2 - graphWidth / 2} - viewBox={`0 0 ${graphWidth} ${graphHeight}`} + viewBox={`-25 -25 ${graphWidth + 50} ${graphHeight + 50}`} > {graphEdges.map(e => { const edge = graph.current.edge(e) as GraphEdge; @@ -560,7 +560,7 @@ export function DependencyGraph( - -
+
+ ); } From f2cf56451897fd398a47c11a9c303c41e5a55ef3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 29 Sep 2025 13:53:28 +0200 Subject: [PATCH 138/193] cli: remove jest script module cache Signed-off-by: Patrik Oldsberg --- .changeset/full-chefs-roll.md | 5 +++++ packages/cli/config/jestCachingModuleLoader.js | 17 ----------------- 2 files changed, 5 insertions(+), 17 deletions(-) create mode 100644 .changeset/full-chefs-roll.md diff --git a/.changeset/full-chefs-roll.md b/.changeset/full-chefs-roll.md new file mode 100644 index 0000000000..2eaa8b8026 --- /dev/null +++ b/.changeset/full-chefs-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incomatible with Jest 30. diff --git a/packages/cli/config/jestCachingModuleLoader.js b/packages/cli/config/jestCachingModuleLoader.js index b2b6a4f8ae..a3fdf106e9 100644 --- a/packages/cli/config/jestCachingModuleLoader.js +++ b/packages/cli/config/jestCachingModuleLoader.js @@ -16,29 +16,12 @@ const { default: JestRuntime } = require('jest-runtime'); -const scriptTransformCache = new Map(); - module.exports = class CachingJestRuntime extends JestRuntime { constructor(config, ...restArgs) { super(config, ...restArgs); this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts'); } - // This may or may not be a good idea. Theoretically I don't know why this would impact - // test correctness and flakiness, but it seems like it may introduce flakiness and strange failures. - // It does seem to speed up test execution by a fair amount though. - createScriptFromCode(scriptSource, filename) { - let script = scriptTransformCache.get(scriptSource); - if (!script) { - script = super.createScriptFromCode(scriptSource, filename); - // Tried to store the script object in a WeakRef here. It starts out at - // about 90% hit rate, but eventually drops all the way to 20%, and overall - // it seemed to increase memory usage by 20% or so. - scriptTransformCache.set(scriptSource, script); - } - return script; - } - // Unfortunately we need to use this unstable API to make sure that .js files // are only loaded as modules where ESM is supported, i.e. Node.js packages. unstable_shouldLoadAsEsm(path, ...restArgs) { From eefabd0002e54e84e761157c8faa4c099ffbebff Mon Sep 17 00:00:00 2001 From: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:06:57 -0400 Subject: [PATCH 139/193] New technical overview per feature #21925 (#30998) * Create technical-overview.md First draft of technical overview. Just want to save these changes as I lost the last file when I had to reboot. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md with additional text and graphics Added additional text to describe the system model. Added graphics and started to add additional links. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md added more text Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md add additional text to plugin section Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md add plugin architecture overview Added text for plugin architecture overview and additional text in system model and purpose section. Added links for core features. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Create backstage-ui-group-ownership.png Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Add files via upload Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md added graphics Added a graphic illustrating My Group in the Backstage UI. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md formatting changes Made formatting changes. Changed bullets from + to - and some spacing changes trying to fix prettier errors. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md copied in text from vscode running prettier Trying to fix prettier errors. Edited code in vscode and save with prettier formatting. Replaced text with the vscode version. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md update links for search and techdocs There is a README.md file in each of these code directories (search and techdocs) but the online documentation displays the README file as index.html file for each of these features. So I replaced the links to the following: https://backstage.io/docs/features/search/ https://backstage.io/docs/features/techdocs/ Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md added graphic descriptions Added "description" for graphics for accessibility. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md update links Added a link for "plugin architecture" to go to "plugin architecture" section in the architectural overview document. Also moved the software catalog "entities" links around. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update sidebars.ts to add technical overview and remove some docs Added the technical overview to the Overview section. Removed the what-is-backstage, background, and vision pages as that information was folded into the new technical overview. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update sidebars.ts to add back what-is-backstage and roadmap Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update technical-overview.md updated text Fixed spelling error and updated text to incorporate review comments. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> --------- Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> --- .../backstage-ui-group-ownership.png | Bin 0 -> 247558 bytes docs/overview/technical-overview.md | 88 ++++++++++++++++++ microsite/sidebars.ts | 3 +- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 docs/assets/technical-overview/backstage-ui-group-ownership.png create mode 100644 docs/overview/technical-overview.md diff --git a/docs/assets/technical-overview/backstage-ui-group-ownership.png b/docs/assets/technical-overview/backstage-ui-group-ownership.png new file mode 100644 index 0000000000000000000000000000000000000000..607aa7ad479cda00ab0d42b55b90d46b113bd87b GIT binary patch literal 247558 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�NInaRTzb@0FBFGM?X`D@Wkf_xW#qm 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�NtCoXd^ND?Frx7MGs?0!x-zCMa@PEsUh)*e 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 literal 0 HcmV?d00001 diff --git a/docs/overview/technical-overview.md b/docs/overview/technical-overview.md new file mode 100644 index 0000000000..63c20dbf2f --- /dev/null +++ b/docs/overview/technical-overview.md @@ -0,0 +1,88 @@ +--- +id: technical-overview +title: Technical overview +description: Technical overview of Backstage +--- + +## Purpose + +Backstage is an open source framework for building developer portals that was created at Spotify to simplify end-to-end software development. As Spotify grew, their infrastructure became more fragmented and teams couldn't find the APIs they were supposed to use, or who owned a service, or documentation on anything. + +Backstage is powered by a centralized [software catalog](#software-catalog-system-model) and utilizes an abstraction layer that sits on top of all of your infrastructure and developer tooling, allowing you to manage all of your software, services, tooling, and testing in one place. + +Backstage uses a [plugin-architecture](#plugin-architecture-overview) which allows you customize the functionality of your Backstage application using a wide variety of available plugins or you can write your own. It also includes automated templates that your teams can use to create new microservices, helping to ensure consistency and adherence to your best practices. Backstage also provides the ability to create, maintain, and find the documentation for all of your software. + +Backstage is now a [CNCF incubation project](https://backstage.io/blog/2022/03/16/backstage-turns-two#out-of-the-sandbox-and-into-incubation). + +## Benefits + +- For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification. +- For _end users_ (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation. +- For _platform engineers_, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones. +- For _everyone_, it is a single, consistent experience that ties all of your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place. + +If you have question or want support, please join our [Discord server](https://discord.gg/backstage-687207715902193673). + +## Core Features + +Backstage includes the following set of core features: + +- [Authentication and Identity](../auth/index.md) - Sign-in and identification of users, and delegating access to third-party resources, using built-in authentication providers. +- [Kubernetes](../features/kubernetes/index.md) - A tool that allows developers to check the health of their services whether it is on a local host or in production. +- [Notifications](../notifications/index.md) - Provides a means for plugins and external services to send messages to either individual users or groups. +- [Permissions](../permissions/overview.md) - Ability to enforce rules concerning the type of access a user is given to specific data, APIs, or interface actions. +- [Search](https://backstage.io/docs/features/search/) - Search for information in the Backstage ecosystem. You can customize the look and feel of each search result and use your own search engine. +- [Software Catalog](../features/software-catalog/index.md) - A centralized system that contains metadata for all your software, such as services, websites, libraries, ML models, data pipelines, and so on. It can also contain metadata for the physical or virtual infrastructure needed to operate a piece of software. The software catalog can be viewed and searched through a UI. +- [Software Templates](../features/software-templates/index.md) - A tool to help you create components inside Backstage. A template can load skeletons of code, include some variables, and then publish the template to a location, such as GitHub. +- [TechDocs](https://backstage.io/docs/features/techdocs/) - A docs-like-code solution built into Backstage. Documentation is written in Markdown files which lives together with the code. + +## Plugin Architecture Overview + +Plugins are client side applications which mount themselves on the Backstage UI. They allow you to incorporate a wide variety of infrastructure and software development tools into your Backstage application. Backstage uses a [plugin-architecture](../overview/architecture-overview.md#plugin-architecture) to provide a consistent user experience, in a single UI, around all of your plugins. + +The Backstage architecture supports three types of plugins: + +- Standalone - runs entirely in a browser and it does not make any API requests to other services. +- Service backed - makes API requests to a service within the ecosystem of the organization running Backstage. +- Third-party backed - similar to service-backed, but the service backing the plugin is hosted outside of the ecosystem of the company hosting Backstage. + +Many of the features available in Backstage are provided by plugins. For example, the Software Catalog is a service backed plugin. When you view the catalog, it retrieves a set of services ("[entities](#software-catalog-system-model)") from the Backstage Backend service and renders them in a table in the UI for you. + +## Software Catalog System Model + +The system model behind the software catalog is based on [_entities_](../references/glossary.md#entity) and it models two main types: + +- Core Entities +- Organizational Entities + +`Core Entities` include: + +- `Components` - Individual pieces of software that can be tracked in source control and can implement APIs for other components to consume. +- `APIs` - Implemented by components and form the boundaries between different components. The API can be either public, restricted, or private. +- `Resources` - The physical or virtual infrastructure needed to operate a component. + +![core entity relationships in the software model](../assets/software-catalog/software-model-core-entities.drawio.svg) + +`Organizational Entities` include: + +- `User` - A person, such as an employee, contractor, or similar. +- `Group` - An organizational entity, such as a team, business unit, and so on. + +When you have a large catalogue of components, APIs, and resources, it can be difficult to understand how they work together. Ecosystem modeling allows you to organize a large catalog of core entities into: + +- Systems - A collection of resources and components that cooperate to perform a function by exposing one or several public APIs. It hides the resources and private APIs between the components from the consumer. +- Domains - A collection of systems that share terminology, domain models, metrics, KPIs, business purpose, or documentation. + +There are three additional items that can be part of the system model: + +- `Location` - A marker that references other places to look for catalog data. +- `Type` - It has no set meaning. You can assign your own types and use them as desired. +- `Template` - Describes both the parameters that are rendered in the frontend part of the scaffolding wizard, and the steps that are executed when scaffolding that component. + +The following diagram illustrates an example of ecosystem modeling, and provides sample relationships between a domain, system, core entities, and organization entities. + +![ecosystem modeling example](../assets/software-catalog/software-model-entities.drawio.svg) + +The following shows an example of viewing all of the components, APIs, and resources that are managed by your group after setting up the relationships to create a group organizational entity. + +![sample ui showing group ownership](../assets/technical-overview/backstage-ui-group-ownership.png) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 72608f4bac..bdebf915df 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -22,9 +22,8 @@ export default { docs: { Overview: [ 'overview/what-is-backstage', + 'overview/technical-overview', 'overview/architecture-overview', - 'overview/background', - 'overview/vision', 'overview/roadmap', 'overview/threat-model', 'overview/versioning-policy', From cee51f7c4e33e140711a6caf1509e618fc0b5744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EC=A4=80?= Date: Tue, 30 Sep 2025 10:42:44 +0900 Subject: [PATCH 140/193] chore: deelete public comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 김병준 --- .../ConfigmapsAccordions/ConfigmapsAccordions.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx index 81ab874737..bdb5b10d35 100644 --- a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx @@ -70,11 +70,6 @@ const ConfigmapCard = ({ configmap }: ConfigmapsCardProps) => { ); }; -/** - * - * - * @public - */ export type ConfigmapsAccordionsProps = {}; type ConfigmapsAccordionProps = { @@ -94,11 +89,6 @@ const ConfigmapsAccordion = ({ configmap }: ConfigmapsAccordionProps) => { ); }; -/** - * - * - * @public - */ export const ConfigmapsAccordions = ({}: ConfigmapsAccordionsProps) => { const groupedResponses = useContext(GroupedResponsesContext); return ( From 032a19f3fbe1405e0e4c46f8a824d99a0917b826 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 30 Sep 2025 09:34:03 +0200 Subject: [PATCH 141/193] Fix typo in changeset for coreExtensionData.title Signed-off-by: Patrik Oldsberg --- .changeset/nasty-moose-rescue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nasty-moose-rescue.md b/.changeset/nasty-moose-rescue.md index ff8cf1b02c..875c19e9a4 100644 --- a/.changeset/nasty-moose-rescue.md +++ b/.changeset/nasty-moose-rescue.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': patch --- -Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but availble for use for other cases too. +Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too. From 0e2ab392ada5533470c9386d56d23ca402575c78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 30 Sep 2025 11:29:16 +0200 Subject: [PATCH 142/193] github/copilot-instructions: update testing instructions Signed-off-by: Patrik Oldsberg --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4f0e0ac47d..509b12f2e1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ The following files contain guidelines for the project: Before any of these commands can be run, you need to run `yarn install` in the project root. - Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline. -- Test: Use `yarn test ` in the project root to run tests. The path can be either a single file or a directory, and be omitted to run tests for all changed files. +- Test: Use `yarn test --no-watch ` in the project root to run tests. The path can be either a single file or a directory. Always provide a patch, avoid running all tests. - Type checking: Use `yarn tsc` in the project root to run the type checker. - Code formatting: Use `yarn prettier --write ` to format code. - Lint: Use `yarn lint --fix` in the project root to run the linter. From 7be9bd7106593f3c2139a98a2a8128fd63c1dd03 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 30 Sep 2025 14:08:01 +0200 Subject: [PATCH 143/193] simplify changeset Signed-off-by: Claire Peng --- .changeset/twelve-oranges-grin.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.changeset/twelve-oranges-grin.md b/.changeset/twelve-oranges-grin.md index 174dbdcd66..ab1ede0a0d 100644 --- a/.changeset/twelve-oranges-grin.md +++ b/.changeset/twelve-oranges-grin.md @@ -2,10 +2,4 @@ '@backstage/plugin-catalog-backend-module-gitlab': patch --- -Update GitlabDiscoveryEntityProvider to use `project.id` rather than `project.path_with_namespace` for `hasFile` check in `shouldProcessProject` - -Solves [#30147](https://github.com/backstage/backstage/issues/30147): - -> Use of project.id avoids edge cases caused by path encoding or project structure changes -> [...] -> It also simplifies reasoning about the GitLab API usage, since IDs are immutable, while paths are not. +Fixed an issue in `GitlabDiscoveryEntityProvider` where entity fetching could fail for projects with special characters or that had been renamed or moved. From d4a122978277fd1ba239eddb5bf42266c24c794e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 30 Sep 2025 14:17:51 +0200 Subject: [PATCH 144/193] update testhandler to only check for id Signed-off-by: Claire Peng --- .../src/__testUtils__/handlers.ts | 35 +++++++------------ .../src/lib/client.test.ts | 17 ++------- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 241bc86df6..4f349a1978 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -272,28 +272,19 @@ const httpProjectFindByIdDynamic = all_projects_response.map(project => { * https://docs.gitlab.com/api/repository_files/#get-file-from-repository */ const httpProjectCatalogDynamic = all_projects_response.flatMap(project => { - const possibleIdSegments: string[] = [ - project.id.toString(), - project.path_with_namespace ?? '', - ]; - - return possibleIdSegments.map(seg => - rest.head( - `${apiBaseUrl}/projects/${encodeURIComponent( - seg, - )}/repository/files/catalog-info.yaml`, - (req, res, ctx) => { - const branch = req.url.searchParams.get('ref'); - if ( - branch === project.default_branch || - branch === 'main' || - branch === 'develop' - ) { - return res(ctx.status(200)); - } - return res(ctx.status(404, 'Not Found')); - }, - ), + return rest.head( + `${apiBaseUrl}/projects/${project.id.toString()}/repository/files/catalog-info.yaml`, + (req, res, ctx) => { + const branch = req.url.searchParams.get('ref'); + if ( + branch === project.default_branch || + branch === 'main' || + branch === 'develop' + ) { + return res(ctx.status(200)); + } + return res(ctx.status(404, 'Not Found')); + }, ); }); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index 35107c6e97..60974d2d0f 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -580,26 +580,13 @@ describe('hasFile', () => { }); }); - it('should find catalog file by id', async () => { + it('should find catalog file', async () => { const hasFile = await client.hasFile(1, 'main', 'catalog-info.yaml'); expect(hasFile).toBe(true); }); - it('should find catalog file by namespace path', async () => { - const hasFile = await client.hasFile( - 'group1/test-repo1', - 'main', - 'catalog-info.yaml', - ); - expect(hasFile).toBe(true); - }); - it('should not find catalog file', async () => { - const hasFile = await client.hasFile( - 'group1/test-repo1', - 'unknown', - 'catalog-info.yaml', - ); + const hasFile = await client.hasFile(1, 'unknown', 'catalog-info.yaml'); expect(hasFile).toBe(false); }); }); From 0aa318ede6a9dd45107e0e3524757075527b3daf Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 30 Sep 2025 16:36:17 +0200 Subject: [PATCH 145/193] chore: fixing prettuer Signed-off-by: benjdlambert --- docs/backend-system/core-services/http-router.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md index 583cd0212d..6de394f549 100644 --- a/docs/backend-system/core-services/http-router.md +++ b/docs/backend-system/core-services/http-router.md @@ -136,10 +136,10 @@ import { } from '@backstage/backend-defaults/httpRouter'; import PromiseRouter from 'express-promise-router'; import { Handler } from 'express'; -import { - createServiceFactory, - coreServices, - HttpRouterServiceAuthPolicy +import { + createServiceFactory, + coreServices, + HttpRouterServiceAuthPolicy, } from '@backstage/backend-plugin-api'; const backend = createBackend(); From ffb5b44d86083a19caad5ee94d0f7296bd7a00b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 30 Sep 2025 14:42:12 +0000 Subject: [PATCH 146/193] Version Packages (next) --- .changeset/create-app-1759243273.md | 5 + .changeset/pre.json | 22 +- docs/releases/v1.44.0-next.2-changelog.md | 2298 +++++++++++++++++ package.json | 2 +- packages/app-defaults/CHANGELOG.md | 8 + packages/app-defaults/package.json | 2 +- packages/app-next/CHANGELOG.md | 23 + packages/app-next/package.json | 2 +- packages/app/CHANGELOG.md | 22 + packages/app/package.json | 2 +- packages/backend-defaults/CHANGELOG.md | 6 + packages/backend-defaults/package.json | 2 +- packages/cli/CHANGELOG.md | 20 + packages/cli/package.json | 2 +- packages/config-loader/package.json | 2 +- packages/config/package.json | 2 +- packages/core-components/CHANGELOG.md | 8 + packages/core-components/package.json | 2 +- packages/create-app/CHANGELOG.md | 6 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 9 + packages/dev-utils/package.json | 2 +- .../techdocs-cli-embedded-app/CHANGELOG.md | 13 + .../techdocs-cli-embedded-app/package.json | 2 +- packages/test-utils/CHANGELOG.md | 7 + packages/test-utils/package.json | 2 +- packages/theme/CHANGELOG.md | 6 + packages/theme/package.json | 2 +- packages/ui/CHANGELOG.md | 7 + packages/ui/package.json | 2 +- plugins/app/CHANGELOG.md | 8 + plugins/app/package.json | 2 +- plugins/auth/CHANGELOG.md | 8 + plugins/auth/package.json | 2 +- plugins/bui-themer/CHANGELOG.md | 13 + plugins/bui-themer/package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 11 + plugins/catalog-backend/package.json | 2 +- plugins/home/CHANGELOG.md | 8 + plugins/home/package.json | 2 +- plugins/kubernetes-react/CHANGELOG.md | 9 + plugins/kubernetes-react/package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 6 + plugins/notifications-backend/package.json | 2 +- plugins/notifications/CHANGELOG.md | 8 + plugins/notifications/package.json | 2 +- plugins/org/CHANGELOG.md | 8 + plugins/org/package.json | 2 +- plugins/scaffolder-react/CHANGELOG.md | 9 + plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 10 + plugins/scaffolder/package.json | 2 +- plugins/search-react/CHANGELOG.md | 8 + plugins/search-react/package.json | 2 +- plugins/signals/CHANGELOG.md | 8 + plugins/signals/package.json | 2 +- plugins/techdocs/CHANGELOG.md | 10 + plugins/techdocs/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 8 + plugins/user-settings/package.json | 2 +- 62 files changed, 2627 insertions(+), 33 deletions(-) create mode 100644 .changeset/create-app-1759243273.md create mode 100644 docs/releases/v1.44.0-next.2-changelog.md create mode 100644 plugins/bui-themer/CHANGELOG.md diff --git a/.changeset/create-app-1759243273.md b/.changeset/create-app-1759243273.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1759243273.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 03b4d762e2..486fc27ac5 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -203,37 +203,55 @@ "@backstage/plugin-techdocs-react": "1.3.3", "@backstage/plugin-user-settings": "0.8.26", "@backstage/plugin-user-settings-backend": "0.3.6", - "@backstage/plugin-user-settings-common": "0.0.1" + "@backstage/plugin-user-settings-common": "0.0.1", + "@backstage/plugin-mui-to-bui": "0.1.0" }, "changesets": [ "brave-teeth-reply", "brown-falcons-own", + "brown-turkeys-send", + "bui-themer-bui-palette-additions", "calm-trains-tie", "clever-papers-watch", "cold-coats-show", "cool-baboons-count", "create-app-1758639549", "create-app-1758718573", + "create-app-1759243273", + "curvy-bobcats-melt", "eager-toes-start", + "famous-loops-tickle", "fast-heads-brake", "fast-queens-guess", "five-olives-bet", + "giant-weeks-jump", + "heavy-cooks-divide", "hungry-crews-fetch", "itchy-falcons-leave", "kind-places-reply", "legal-eagles-jog", + "modern-pugs-appear", + "moody-singers-deny", "nice-readers-judge", "public-sites-admire", + "public-wombats-say", "rare-states-pay", "ready-poems-change", "ready-pots-arrive", + "red-dodos-work", "red-times-bet", "salty-words-wash", "short-aliens-invite", "six-cooks-battle", + "slimy-signs-agree", "solid-bikes-leave", "tame-hairs-smash", "tender-cups-tap", - "wet-spiders-wait" + "thirty-rules-press", + "tired-mice-cheer", + "twelve-guests-sit", + "unified-theme-attr-stack", + "wet-spiders-wait", + "wide-flies-jog" ] } diff --git a/docs/releases/v1.44.0-next.2-changelog.md b/docs/releases/v1.44.0-next.2-changelog.md new file mode 100644 index 0000000000..74b467ebba --- /dev/null +++ b/docs/releases/v1.44.0-next.2-changelog.md @@ -0,0 +1,2298 @@ +# Release v1.44.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.44.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.44.0-next.2) + +## @backstage/config@1.3.5-next.0 + +# @backstage/config + +## 1.3.4-next.0 + +### Patch Changes + +- b45b094: Allow colon to be used as config key. + +## 1.3.3 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- 3507fcd: Just some more circular dep cleanup + +## 1.3.3-next.0 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- 3507fcd: Just some more circular dep cleanup + +## 1.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.1 + - @backstage/errors@1.2.7 + +## 1.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.1-next.0 + - @backstage/errors@1.2.7-next.0 + +## 1.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.6 + - @backstage/types@1.2.0 + +## 1.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.6-next.0 + - @backstage/types@1.2.0 + +## 1.3.0 + +### Minor Changes + +- d52d7f9: Make `readDurationFromConfig` support both ISO and ms formats as well, to make it easier to enter time as an end user + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.0 + - @backstage/errors@1.2.5 + +## 1.2.0 + +### Minor Changes + +- 50cf9df: The `ConfigReader` now treats `null` values as present but explicitly undefined, meaning it will not fall back to the next level of configuration. + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.2.0-next.1 + +### Minor Changes + +- 50cf9df: The `ConfigReader` now treats `null` values as present but explicitly undefined, meaning it will not fall back to the next level of configuration. + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## 1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## 1.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.3-next.0 + - @backstage/types@1.1.1 + +## 1.1.0 + +### Minor Changes + +- 62f448edb0b5: Added a `readDurationFromConfig` function + +### Patch Changes + +- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/errors@1.2.2 + - @backstage/types@1.1.1 + +## 1.1.0-next.2 + +### Patch Changes + +- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. +- Updated dependencies + - @backstage/errors@1.2.2-next.0 + - @backstage/types@1.1.1-next.0 + +## 1.1.0-next.1 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.1.0-next.0 + +### Minor Changes + +- 62f448edb0b5: Added a `readDurationFromConfig` function + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.1.0 + +## 1.0.7 + +### Patch Changes + +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.7-next.0 + +### Patch Changes + +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.6 + +### Patch Changes + +- ba2d69ee17: Adds the ability to coerce values to their boolean representatives. + Values such as `"true"` `1` `on` and `y` will become `true` when using `getBoolean` and the opposites `false`. + This happens particularly when such parameters are used with environmental substitution as environment variables are always strings. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.6-next.0 + +### Patch Changes + +- ba2d69ee17: Adds the ability to coerce values to their boolean representatives. + Values such as `"true"` `1` `on` and `y` will become `true` when using `getBoolean` and the opposites `false`. + This happens particularly when such parameters are used with environmental substitution as environment variables are always strings. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + +## 1.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + +## 1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + +## 1.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1-next.0 + +## 1.0.3 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.2 + +### Patch Changes + +- 7d47def9c4: Removed dependency on `@types/jest`. + +## 1.0.2-next.0 + +### Patch Changes + +- 7d47def9c4: Removed dependency on `@types/jest`. + +## 1.0.1 + +### Patch Changes + +- 6e830352d4: Updated dependency `@types/node` to `^16.0.0`. + +## 1.0.1-next.0 + +### Patch Changes + +- 6e830352d4: Updated dependency `@types/node` to `^16.0.0`. + +## 1.0.0 + +### Major Changes + +- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 0.1.15 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + +## 0.1.14 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/types@0.1.2 + +## 0.1.13 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +## 0.1.13-next.0 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +## 0.1.12 + +### Patch Changes + +- f5343e7c1a: The `ConfigReader#get` method now always returns a deep clone of the configuration data. + +## 0.1.11 + +### Patch Changes + +- 10d267a1b7: Minor exports cleanup +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils + +## 0.1.10 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability + +## 0.1.9 + +### Patch Changes + +- f88b2c7db: Documented `Config` interface and mark types as public. + +## 0.1.8 + +### Patch Changes + +- 47113f1f1: Only warn once per key when trying to read visibility-filtered values + +## 0.1.7 + +### Patch Changes + +- 90f25476a: Extended the `Config` interface to have an optional `subscribe` method that can be used be notified of updates to the configuration. + +## 0.1.6 + +### Patch Changes + +- e9d3983ee: Add warning when trying to access configuration values that have been filtered out by visibility. + +## 0.1.5 + +### Patch Changes + +- d8b81fd28: Bump `json-schema` dependency from `0.2.5` to `0.3.0`. + +## 0.1.4 + +### Patch Changes + +- 0434853a5: Reformulate the json types to break type recursion + +## 0.1.3 + +### Patch Changes + +- a1f5e6545: Adds an optional type to `config.get` & `config.getOptional`. This avoids the need for casting. For example: + + ```ts + const config = useApi(configApiRef); + + const myConfig = config.get('myPlugin.complexConfig'); + // vs + const myConfig config.get('myPlugin.complexConfig') as SomeTypeDefinition; + ``` + +## 0.1.2 + +### Patch Changes + +- e3bd9fc2f: Fix unneeded defensive code +- e3bd9fc2f: Fix useless conditional + +## @backstage/config-loader@1.10.5-next.0 + +# @backstage/config-loader + +## 1.10.4-next.0 + +### Patch Changes + +- b45b094: Allow colon to be used as config key. +- Updated dependencies + - @backstage/config@1.3.4-next.0 + +## 1.10.3 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files +- Updated dependencies + - @backstage/types@1.2.2 + +## 1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + +## 1.10.2 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- Updated dependencies + - @backstage/config@1.3.3 + +## 1.10.2-next.0 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- Updated dependencies + - @backstage/config@1.3.3-next.0 + +## 1.10.1 + +### Patch Changes + +- 72d019d: Removed various typos +- Updated dependencies + - @backstage/config@1.3.2 + - @backstage/cli-common@0.1.15 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.10.1-next.0 + +### Patch Changes + +- 72d019d: Removed various typos +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.10.0 + +### Minor Changes + +- 2fd73aa: The include transforms applied during config loading will now only apply to the known keys `$file`, `$env`, and `$include`. Any other key that begins with a \`# @backstage/config-loader will now be passed through as is. + +### Patch Changes + +- f422984: Added `@types/minimist` to `devDependencies` +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.10.0-next.0 + +### Minor Changes + +- 2fd73aa: The include transforms applied during config loading will now only apply to the known keys `$file`, `$env`, and `$include`. Any other key that begins with a \`# @backstage/config-loader will now be passed through as is. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.9.6 + +### Patch Changes + +- f866b86: Internal refactor to use explicit `require` for lazy-loading dependency. +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.9.6-next.0 + +### Patch Changes + +- f866b86: Internal refactor to use explicit `require` for lazy-loading dependency. +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.9.5 + +### Patch Changes + +- 8ecf8cb: Exclude `@backstage/backend-common` from schema collection if `@backstage/backend-defaults` is present +- Updated dependencies + - @backstage/types@1.2.1 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + +## 1.9.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.1-next.0 + - @backstage/config@1.3.2-next.0 + - @backstage/errors@1.2.7-next.0 + - @backstage/cli-common@0.1.15 + +## 1.9.5-next.0 + +### Patch Changes + +- 8ecf8cb: Exclude `@backstage/backend-common` from schema collection if `@backstage/backend-defaults` is present +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1 + - @backstage/errors@1.2.6 + - @backstage/types@1.2.0 + +## 1.9.3 + +### Patch Changes + +- 5c9cc05: Use native fetch instead of node-fetch +- Updated dependencies + - @backstage/errors@1.2.6 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1 + - @backstage/types@1.2.0 + +## 1.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.6-next.0 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1-next.0 + - @backstage/types@1.2.0 + +## 1.9.3-next.0 + +### Patch Changes + +- 5c9cc05: Use native fetch instead of node-fetch +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.0 + - @backstage/errors@1.2.5 + - @backstage/types@1.2.0 + +## 1.9.2 + +### Patch Changes + +- c5e39e7: Internal refactor to use the deferred from the types package +- Updated dependencies + - @backstage/config@1.3.0 + - @backstage/types@1.2.0 + - @backstage/cli-common@0.1.15 + - @backstage/errors@1.2.5 + +## 1.9.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.15-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.1 + +### Patch Changes + +- ef3c507: Updated dependency `typescript-json-schema` to `^0.65.0`. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.1-next.0 + +### Patch Changes + +- ef3c507: Updated dependency `typescript-json-schema` to `^0.65.0`. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.0 + +### Minor Changes + +- 274428f: Add configuration key to File and Remote `ConfigSource`s that enables configuration of parsing logic. Previously limited to yaml, these `ConfigSource`s now allow for a multitude of parsing options (e.g. JSON). + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 1edd6c2: The `env` option of `ConfigSources.default` now correctly allows undefined members. +- 493feac: Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and + `ConfigSources.defaultForTargets`, which results in omission of a ConfigSource + for the default app-config.yaml configuration file if it's not present. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.0-next.1 + +### Minor Changes + +- 274428f: Add configuration key to File and Remote `ConfigSource`s that enables configuration of parsing logic. Previously limited to yaml, these `ConfigSource`s now allow for a multitude of parsing options (e.g. JSON). + +### Patch Changes + +- 1edd6c2: The `env` option of `ConfigSources.default` now correctly allows undefined members. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.2-next.0 + +### Patch Changes + +- 493feac: Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and + `ConfigSources.defaultForTargets`, which results in omission of a ConfigSource + for the default app-config.yaml configuration file if it's not present. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.0 + +### Minor Changes + +- 2ce31b3: The default environment variable substitution function will now trim whitespace characters from the substituted value. This alleviates bugs where whitespace characters are mistakenly included in environment variables. + + If you depend on the old behavior, you can override the default substitution function with your own, for example: + + ```ts + ConfigSources.default({ + substitutionFunc: async name => process.env[name], + }); + ``` + +- 99bab65: Support parameter substitution for environment variables + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.0-next.0 + +### Minor Changes + +- 99bab65: Support parameter substitution for environment variables + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.7.0 + +### Minor Changes + +- db8358d: Forward `null` values read from configuration files in configuration data, rather than treating them as an absence of config. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## 1.7.0-next.1 + +### Minor Changes + +- db8358d: Forward `null` values read from configuration files in configuration data, rather than treating them as an absence of config. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0-next.1 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## 1.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## 1.6.2 + +### Patch Changes + +- 0a9a03c: Make schema processing gracefully handle an empty config. +- 6bb6f3e: Updated dependency `fs-extra` to `^11.2.0`. + Updated dependency `@types/fs-extra` to `^11.0.0`. +- bf3da16: Updated dependency `typescript-json-schema` to `^0.63.0`. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.2-next.0 + +### Patch Changes + +- 0a9a03c: Make schema processing gracefully handle an empty config. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.1 + +### Patch Changes + +- 7acbb5a: Removed `mock-fs` dev dependency. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.1-next.0 + +### Patch Changes + +- 7acbb5a: Removed `mock-fs` dev dependency. +- Updated dependencies + - @backstage/config@1.1.1 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.0 + +### Minor Changes + +- 24f5a85: Add "path" to `TransformFunc` context + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.0-next.0 + +### Minor Changes + +- 24f5a85: Add "path" to `TransformFunc` context + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.5.3 + +### Patch Changes + +- 22ca64f117: Correctly resolve config targets into absolute paths +- 087bab5b42: Updated dependency `typescript-json-schema` to `^0.62.0`. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.5.2-next.0 + +### Patch Changes + +- 22ca64f117: Correctly resolve config targets into absolute paths +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.5.1 + +### Patch Changes + +- 0b55f773a7: Removed some unused dependencies +- 30c553c1d2: Updated dependency `typescript-json-schema` to `^0.61.0`. +- 773ea341d2: The `FileConfigSource` will now retry file reading after a short delay if it reads an empty file. This is to avoid flakiness during watch mode where change events can trigger before the file content has been written. +- a4617c422a: Added `watch` option to configuration loaders that can be used to disable file watching by setting it to `false`. +- Updated dependencies + - @backstage/errors@1.2.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/types@1.1.1 + +## 1.5.1-next.1 + +### Patch Changes + +- 0b55f773a7: Removed some unused dependencies +- 30c553c1d2: Updated dependency `typescript-json-schema` to `^0.61.0`. +- a4617c422a: Added `watch` option to configuration loaders that can be used to disable file watching by setting it to `false`. +- Updated dependencies + - @backstage/errors@1.2.3-next.0 + - @backstage/cli-common@0.1.13-next.0 + - @backstage/config@1.1.1-next.0 + - @backstage/types@1.1.1 + +## 1.5.1-next.0 + +### Patch Changes + +- 773ea341d2: The `FileConfigSource` will now retry file reading after a short delay if it reads an empty file. This is to avoid flakiness during watch mode where change events can trigger before the file content has been written. +- Updated dependencies + - @backstage/cli-common@0.1.13-next.0 + - @backstage/config@1.1.0 + - @backstage/errors@1.2.2 + - @backstage/types@1.1.1 + +## 1.5.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/config@1.1.0 + - @backstage/errors@1.2.2 + - @backstage/types@1.1.1 + - @backstage/cli-common@0.1.12 + +## 1.5.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.2 + - @backstage/errors@1.2.2-next.0 + - @backstage/types@1.1.1-next.0 + - @backstage/cli-common@0.1.12 + +## 1.5.0-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.5.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.5.0-next.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.0 + +### Minor Changes + +- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored. + + In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`. + +- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`. + + Example usage: + + ```ts + export interface Config { + /** + * Enforces a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + secretValue: { + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + + Example of a schema that would not be allowed: + + ```ts + export interface Config { + /** + * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + frontendUrl: { + /** + * We can NOT override the visibility to reveal a property to the front end. + * @visibility frontend + */ + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.0-next.1 + +### Minor Changes + +- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored. + + In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.0-next.0 + +### Minor Changes + +- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`. + + Example usage: + + ```ts + export interface Config { + /** + * Enforces a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + secretValue: { + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + + Example of a schema that would not be allowed: + + ```ts + export interface Config { + /** + * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + frontendUrl: { + /** + * We can NOT override the visibility to reveal a property to the front end. + * @visibility frontend + */ + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 1.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 1.3.1 + +### Patch Changes + +- f25427f665f7: Fix a bug where config items with `/` in the key were incorrectly handled. +- a5c5491ff50c: Use `durationToMilliseconds` from `@backstage/types` instead of our own +- Updated dependencies + - @backstage/types@1.1.0 + - @backstage/errors@1.2.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + +## 1.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.0-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/types@1.0.2 + +## 1.3.1-next.0 + +### Patch Changes + +- f25427f665f7: Fix a bug where config items with `/` in the key were incorrectly handled. +- Updated dependencies + - @backstage/config@1.0.7 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.3.0 + +### Minor Changes + +- 201206132da: Introduced a new config source system to replace `loadConfig`. There is a new `ConfigSource` interface along with utilities provided by `ConfigSources`, as well as a number of built-in configuration source implementations. The new system is more flexible and makes it easier to create new and reusable sources of configuration, such as loading configuration from secret providers. + + The following is an example of how to load configuration using the default behavior: + + ```ts + const source = ConfigSources.default({ + argv: options?.argv, + remote: options?.remote, + }); + const config = await ConfigSources.toConfig(source); + ``` + + The `ConfigSource` interface looks like this: + + ```ts + export interface ConfigSource { + readConfigData(options?: ReadConfigDataOptions): AsyncConfigSourceIterator; + } + ``` + + It is best implemented using an async iterator: + + ```ts + class MyConfigSource implements ConfigSource { + async *readConfigData() { + yield { + config: [ + { + context: 'example', + data: { backend: { baseUrl: 'http://localhost' } }, + }, + ], + }; + } + } + ``` + +### Patch Changes + +- 7c116bcac7f: Fixed the way that some request errors are thrown +- 473db605a4f: Added a new `noUndeclaredProperties` option to `SchemaLoader` to support enforcing that there are no extra keys when verifying config. +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.3.0-next.0 + +### Minor Changes + +- 201206132da: Introduced a new config source system to replace `loadConfig`. There is a new `ConfigSource` interface along with utilities provided by `ConfigSources`, as well as a number of built-in configuration source implementations. The new system is more flexible and makes it easier to create new and reusable sources of configuration, such as loading configuration from secret providers. + + The following is an example of how to load configuration using the default behavior: + + ```ts + const source = ConfigSources.default({ + argv: options?.argv, + remote: options?.remote, + }); + const config = await ConfigSources.toConfig(source); + ``` + + The `ConfigSource` interface looks like this: + + ```ts + export interface ConfigSource { + readConfigData(options?: ReadConfigDataOptions): AsyncConfigSourceIterator; + } + ``` + + It is best implemented using an async iterator: + + ```ts + class MyConfigSource implements ConfigSource { + async *readConfigData() { + yield { + config: [ + { + context: 'example', + data: { backend: { baseUrl: 'http://localhost' } }, + }, + ], + }; + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.2.0 + +### Minor Changes + +- c791fcd96b9: Configuration validation is now more permissive when it comes to config whose values are `string` but whose schemas declare them to be `boolean` or `number`. + + For example, configuration was previously marked invalid when a string `'true'` was set on a property expecting type `boolean` or a string `'146'` was set on a property expecting type `number` (as when providing configuration via variable substitution sourced from environment variables). Now, such configurations will be considered valid and their values will be coerced to the right type at read-time. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.1.9 + +### Patch Changes + +- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/errors@1.1.5 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/types@1.0.2 + +## 1.1.9-next.0 + +### Patch Changes + +- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/errors@1.1.5-next.0 + - @backstage/cli-common@0.1.12-next.0 + - @backstage/config@1.0.7-next.0 + - @backstage/types@1.0.2 + +## 1.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.1.7 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 40e7e6e1a2: Updated dependency `typescript-json-schema` to `^0.55.0`. +- Updated dependencies + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.5 + +## 1.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.11-next.0 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/types@1.0.2-next.1 + +## 1.1.7-next.1 + +### Patch Changes + +- 40e7e6e1a2: Updated dependency `typescript-json-schema` to `^0.55.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/cli-common@0.1.10 + - @backstage/errors@1.1.4-next.1 + +## 1.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 1.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 1.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## 1.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3 + - @backstage/errors@1.1.2 + - @backstage/types@1.0.0 + +## 1.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.2 + - @backstage/errors@1.1.2-next.2 + - @backstage/types@1.0.0 + +## 1.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.1 + - @backstage/errors@1.1.2-next.1 + - @backstage/types@1.0.0 + +## 1.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.0 + - @backstage/errors@1.1.2-next.0 + - @backstage/types@1.0.0 + +## 1.1.4 + +### Patch Changes + +- 5ecca7e44b: No longer log when reloading remote config. +- 7d47def9c4: Removed dependency on `@types/jest`. +- 667d917488: Updated dependency `msw` to `^0.47.0`. +- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. +- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.2 + - @backstage/errors@1.1.1 + +## 1.1.4-next.2 + +### Patch Changes + +- 5ecca7e44b: No longer log when reloading remote config. +- 7d47def9c4: Removed dependency on `@types/jest`. +- Updated dependencies + - @backstage/cli-common@0.1.10-next.0 + - @backstage/config@1.0.2-next.0 + - @backstage/errors@1.1.1-next.0 + +## 1.1.4-next.1 + +### Patch Changes + +- 667d917488: Updated dependency `msw` to `^0.47.0`. +- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. + +## 1.1.4-next.0 + +### Patch Changes + +- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. + +## 1.1.3 + +### Patch Changes + +- bcada7cd9f: From now on the `$file` placeholder will trim the whitespaces and newline characters from the end of the file it reads. +- a70869e775: Updated dependency `msw` to `^0.43.0`. +- 72622d9143: Updated dependency `yaml` to `^2.0.0`. +- 8006d0f9bf: Updated dependency `msw` to `^0.44.0`. +- a3acec8819: Updated dependency `typescript-json-schema` to `^0.54.0`. +- Updated dependencies + - @backstage/errors@1.1.0 + +## 1.1.3-next.1 + +### Patch Changes + +- a70869e775: Updated dependency `msw` to `^0.43.0`. +- 72622d9143: Updated dependency `yaml` to `^2.0.0`. +- a3acec8819: Updated dependency `typescript-json-schema` to `^0.54.0`. + +## 1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.1.0-next.0 + +## 1.1.2 + +### Patch Changes + +- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. + +## 1.1.2-next.0 + +### Patch Changes + +- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. + +## 1.1.1 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9e8ef53498: Handle empty config files gracefully +- Updated dependencies + - @backstage/cli-common@0.1.9 + - @backstage/config@1.0.1 + +## 1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.9-next.0 + - @backstage/config@1.0.1-next.0 + +## 1.1.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9e8ef53498: Handle empty config files gracefully + +## 1.1.0 + +### Minor Changes + +- 19f6c6c32a: Added `ignoreSchemaErrors` to `schema.process`. + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- 230ad0826f: Bump to using `@types/node` v16 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +## 1.1.0-next.1 + +### Minor Changes + +- 19f6c6c32a: Added `ignoreSchemaErrors` to `schema.process`. + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 + +## 1.0.1-next.0 + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +## 1.0.0 + +### Major Changes + +- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). + +### Patch Changes + +- 664821371e: The `typescript-json-schema` dependency that is used during schema collection is now lazy loaded, as it eagerly loads in the TypeScript compiler. +- f910c2a3f8: build(deps): bump `typescript-json-schema` from 0.52.0 to 0.53.0 +- Updated dependencies + - @backstage/config@1.0.0 + - @backstage/errors@1.0.0 + - @backstage/types@1.0.0 + +## 0.9.7 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 + +## 0.9.7-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 + +## 0.9.6 + +### Patch Changes + +- c3a1300f79: Include any files included in configuration via $include or $file directives when watching for configuration changes. + +## 0.9.5 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/cli-common@0.1.8 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/types@0.1.3 + +## 0.9.4 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/errors@0.2.1 + - @backstage/cli-common@0.1.7 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + +## 0.9.3 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +- Updated dependencies + - @backstage/config@0.1.13 + +## 0.9.3-next.0 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +- Updated dependencies + - @backstage/config@0.1.13-next.0 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@0.1.12 + - @backstage/errors@0.2.0 + +## 0.9.1 + +### Patch Changes + +- 84663d59a3: Bump `typescript-json-schema` from `^0.51.0` to `^0.52.0`. + +## 0.9.0 + +### Minor Changes + +- f6722d2458: Removed deprecated option `env` from `LoadConfigOptions` and associated tests +- 67d6cb3c7e: Removed deprecated option `configPaths` as it has been superseded by `configTargets` + +### Patch Changes + +- 1e7070443d: In case remote.reloadIntervalSeconds is passed, it must be a valid positive value + +## 0.8.1 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- 4bea7b81d3: Uses key visibility as fallback in non-object arrays + +## 0.8.0 + +### Minor Changes + +- 1e99c73c75: Update `loadConfig` to return `LoadConfigResult` instead of an array of `AppConfig`. + + This function is primarily used internally by other config loaders like `loadBackendConfig` which means no changes are required for most users. + + If you use `loadConfig` directly you will need to update your usage from: + + ```diff + - const appConfigs = await loadConfig(options) + + const { appConfigs } = await loadConfig(options) + ``` + +### Patch Changes + +- 8809b6c0dd: Update the json-schema dependency version. +- Updated dependencies + - @backstage/cli-common@0.1.6 + +## 0.7.2 + +### Patch Changes + +- 0611f3b3e2: Reading app config from a remote server +- 26c5659c97: Bump msw to the same version as the rest + +## 0.7.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- ea21f7f567: bump `typescript-json-schema` from 0.50.1 to 0.51.0 +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + +## 0.7.0 + +### Minor Changes + +- 7e97d0b8c1: Removed the `EnvFunc` public export. Its only usage was to be passed in to `LoadConfigOptions.experimentalEnvFunc`. If you were using this type, add a definition in your own project instead with the signature `(name: string) => Promise`. + +### Patch Changes + +- 223e8de6b4: Configuration schema errors are now filtered using the provided visibility option. This means that schema errors due to missing backend configuration will no longer break frontend builds. +- 7e97d0b8c1: Add public tags and documentation +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/errors@0.1.3 + +## 0.6.10 + +### Patch Changes + +- 957e4b3351: Updated dependencies +- Updated dependencies + - @backstage/cli-common@0.1.4 + +## 0.6.9 + +### Patch Changes + +- ee7a1a4b64: Add option to collect configuration schemas from explicit package paths in addition to by package name. +- e68bd978e2: Allow collection of configuration schemas from multiple versions of the same package. + +## 0.6.8 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config@0.1.9 + +## 0.6.7 + +### Patch Changes + +- 0ade9d02b: Include `devDependencies` and `optionalDependencies` in the detection of Backstage packages when collecting configuration schema. +- 9b8cec063: Add support for config file watching through a new group of `watch` options to `loadConfig`. +- Updated dependencies + - @backstage/config@0.1.7 + +## 0.6.6 + +### Patch Changes + +- e9d3983ee: Add option to populate the `filteredKeys` property when processing configuration with a schema. +- Updated dependencies + - @backstage/config@0.1.6 + +## 0.6.5 + +### Patch Changes + +- ae84b20cf: Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. + +## 0.6.4 + +### Patch Changes + +- f00493739: Removed workaround for breaking change in typescript 4.3 and bump `typescript-json-schema` instead. This should again allow the usage of `@items.visibility ` to set the visibility of array items. + +## 0.6.3 + +### Patch Changes + +- 2cf98d279: Resolve the path to app-config.yaml from the current working directory. This will allow use of `yarn link` or running the CLI in other directories and improve the experience for local backstage development. +- 438a512eb: Fixed configuration schema parsing when using TypeScript `4.3`. + +## 0.6.2 + +### Patch Changes + +- 290405276: Updated dependencies + +## 0.6.1 + +### Patch Changes + +- d8b81fd28: Bump `json-schema` dependency from `0.2.5` to `0.3.0`. +- Updated dependencies [d8b81fd28] + - @backstage/config@0.1.5 + +## 0.6.0 + +### Minor Changes + +- 82c66b8cd: Fix bug where `${...}` was not being escaped to `${...}` + + Add support for environment variable substitution in `$include`, `$file` and + `$env` transform values. + + - This change allows for including dynamic paths, such as environment specific + secrets by using the same environment variable substitution (`${..}`) already + supported outside of the various include transforms. + - If you are currently using the syntax `${...}` in your include transform values, + you will need to escape the substitution by using `${...}` instead to maintain + the same behavior. + +## 0.5.1 + +### Patch Changes + +- 062df71db: Bump `config-loader` to `ajv` 7, to enable v7 feature use elsewhere +- e9aab60c7: Each piece of the configuration schema is now validated upfront, in order to produce more informative errors. + +## 0.5.0 + +### Minor Changes + +- ef7957be4: Removed support for the deprecated `$data` placeholder. +- ef7957be4: Enable further processing of configuration files included using the `$include` placeholder. Meaning that for example for example `$env` includes will be processed as usual in included files. + +### Patch Changes + +- ef7957be4: Added support for environment variable substitutions in string configuration values using a `${VAR}` placeholder. All environment variables must be available, or the entire expression will be evaluated to `undefined`. To escape a substitution, use `${...}`, which will end up as `${...}`. + + For example: + + ```yaml + app: + baseUrl: https://${BASE_HOST} + ``` + +## 0.4.1 + +### Patch Changes + +- ad5c56fd9: Deprecate `$data` and replace it with `$include` which allows for any type of json value to be read from external files. In addition, `$include` can be used without a path, which causes the value at the root of the file to be loaded. + + Most usages of `$data` can be directly replaced with `$include`, except if the referenced value is not a string, in which case the value needs to be changed. For example: + + ```yaml + # app-config.yaml + foo: + $data: foo.yaml#myValue # replacing with $include will turn the value into a number + $data: bar.yaml#myValue # replacing with $include is safe + + # foo.yaml + myValue: 0xf00 + + # bar.yaml + myValue: bar + ``` + +## 0.4.0 + +### Minor Changes + +- 4e7091759: Fix typo of "visibility" in config schema reference + + If you have defined a config element named `visiblity`, you + will need to fix the spelling to `visibility`. For more info, + see . + +### Patch Changes + +- b4488ddb0: Added a type alias for PositionError = GeolocationPositionError + +## 0.3.0 + +### Minor Changes + +- 1722cb53c: Added support for loading and validating configuration schemas, as well as declaring config visibility through schemas. + + The new `loadConfigSchema` function exported by `@backstage/config-loader` allows for the collection and merging of configuration schemas from all nearby dependencies of the project. + + A configuration schema is declared using the `https://backstage.io/schema/config-v1` JSON Schema meta schema, which is based on draft07. The only difference to the draft07 schema is the custom `visibility` keyword, which is used to indicate whether the given config value should be visible in the frontend or not. The possible values are `frontend`, `backend`, and `secret`, where `backend` is the default. A visibility of `secret` has the same scope at runtime, but it will be treated with more care in certain contexts, and defining both `frontend` and `secret` for the same value in two different schemas will result in an error during schema merging. + + Packages that wish to contribute configuration schema should declare it in a root `"configSchema"` field in `package.json`. The field can either contain an inlined JSON schema, or a relative path to a schema file. Schema files can be in either `.json` or `.d.ts` format. + + TypeScript configuration schema files should export a single `Config` type, for example: + + ```ts + export interface Config { + app: { + /** + * Frontend root URL + * @visibility frontend + */ + baseUrl: string; + }; + } + ``` + +## 0.2.0 + +### Minor Changes + +- 8c2b76e45: **BREAKING CHANGE** + + The existing loading of additional config files like `app-config.development.yaml` using APP_ENV or NODE_ENV has been removed. + Instead, the CLI and backend process now accept one or more `--config` flags to load config files. + + Without passing any flags, `app-config.yaml` and, if it exists, `app-config.local.yaml` will be loaded. + If passing any `--config ` flags, only those files will be loaded, **NOT** the default `app-config.yaml` one. + + The old behaviour of for example `APP_ENV=development` can be replicated using the following flags: + + ```bash + --config ../../app-config.yaml --config ../../app-config.development.yaml + ``` + +- ce5512bc0: Added support for new shorthand when defining secrets, where `$env: ENV` can be used instead of `$secret: { env: ENV }` etc. + +## @backstage/backend-defaults@0.13.0-next.2 + +### Minor Changes + +- 8495b18: Add a new `externalTokenHandlersServiceRef` to allow custom external token validations + +## @backstage/plugin-mui-to-bui@0.2.0-next.0 + +### Minor Changes + +- d5cbdba: This is the first release of the Material UI to Backstage UI migration helper plugin. It adds a new page at `/mui-to-bui` that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, with live preview and copy/download. + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.2-next.1 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-notifications-backend-module-slack@0.2.0-next.1 + +### Minor Changes + +- 3d09bb2: Adds username as optional config in order to send Slack notifications with a specific username in the case when using one Slack App for more than just Backstage. + +## @backstage/app-defaults@1.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/cli@0.34.4-next.2 + +### Patch Changes + +- ab96bb7: Added a new `--entrypoint` option to the `package start` command, which allows you to specify a custom entry directory/file for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). + + **Example usage:** + + Consider the following plugin dev folder structure: + + dev/ + index.tsx + alpha/ + index.ts + + - The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` file; + - Running `yarn package start --entrypoint dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` file. + +## @backstage/core-components@0.18.2-next.2 + +### Patch Changes + +- 95935fb: Fixed dependency graph automatically scrolling forever +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + +## @backstage/create-app@0.7.5-next.2 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + +## @backstage/test-utils@1.7.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + +## @backstage/theme@0.6.9-next.0 + +### Patch Changes + +- d5cbdba: The `UnifiedThemeProvider` now coordinates theme attributes on the document `body` in case multiple theme providers are rendered. + +## @backstage/ui@0.7.2-next.1 + +### Patch Changes + +- a9b88be: Enable tooltips on disabled buttons with automatic wrapper +- 4adbb03: Avoid overriding onChange when spreading props + +## @backstage/plugin-app@0.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-auth@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-catalog-backend@3.1.2-next.2 + +### Patch Changes + +- 6493c98: Log before provider-orphaning eviction happens +- 77516c5: Added new `catalog:validate-entity` action to actions registry. + + This action can be used to validate entities against the software catalog. + This is useful for validating `catalog-info.yaml` file changes locally using the + Backstage MCP server. + +## @backstage/plugin-home@0.8.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-kubernetes-react@0.5.12-next.2 + +### Patch Changes + +- ac405f2: The configmaps added to be rendered +- f7a4144: Fixes calculation of CPU utilization in the PodTable +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + +## @backstage/plugin-notifications@0.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-notifications-backend@0.5.11-next.1 + +### Patch Changes + +- 3b8e156: Fixed exclude entity reference not working in notification sending + +## @backstage/plugin-org@0.6.45-next.2 + +### Patch Changes + +- 8b7351f: Add `initialRelationAggregation` and `showAggregateMembersToggle` options to `EntityMembersListCard` as well to `EntityOwnershipCard` +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + +## @backstage/plugin-scaffolder@1.34.2-next.2 + +### Patch Changes + +- 075e064: Added missing form fields for the new frontend system. +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/core-components@0.18.2-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + +## @backstage/plugin-scaffolder-react@1.19.2-next.2 + +### Patch Changes + +- e61f89e: Don't change loading to false until we've actually got some log state +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-search-react@1.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-signals@0.0.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-techdocs@1.15.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + +## @backstage/plugin-user-settings@0.8.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## example-app@0.2.114-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mui-to-bui@0.2.0-next.0 + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + +## example-app-next@0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-app@0.3.1-next.2 + - @backstage/plugin-auth@0.1.1-next.1 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + +## techdocs-cli-embedded-app@0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.4-next.2 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/test-utils@1.7.12-next.1 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 diff --git a/package.json b/package.json index c41ba744b2..a22ab1666a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.44.0-next.1", + "version": "1.44.0-next.2", "backstage": { "cli": { "new": { diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 7e0fe1b55d..2253e0e2cd 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/app-defaults +## 1.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 1.7.1-next.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 1a2cbecc12..3361b7e7b3 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.7.1-next.1", + "version": "1.7.1-next.2", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 87c84cf26c..95149a2a8a 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,28 @@ # example-app-next +## 0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-app@0.3.1-next.2 + - @backstage/plugin-auth@0.1.1-next.1 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + ## 0.0.28-next.1 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index ebfb59c96a..67218d0cf6 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.28-next.1", + "version": "0.0.28-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 675001c296..c7f8d48b37 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,27 @@ # example-app +## 0.2.114-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mui-to-bui@0.2.0-next.0 + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + ## 0.2.114-next.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 1623ca09e5..b1cd05ac7a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.114-next.1", + "version": "0.2.114-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 1201918fb5..5cf7aa3414 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/backend-defaults +## 0.13.0-next.2 + +### Minor Changes + +- 8495b18: Add a new `externalTokenHandlersServiceRef` to allow custom external token validations + ## 0.13.0-next.1 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index f85d34db9e..b5a4acd229 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.13.0-next.1", + "version": "0.13.0-next.2", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 28b2293982..7382ef147b 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/cli +## 0.34.4-next.2 + +### Patch Changes + +- ab96bb7: Added a new `--entrypoint` option to the `package start` command, which allows you to specify a custom entry directory/file for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). + + **Example usage:** + + Consider the following plugin dev folder structure: + + ``` + dev/ + index.tsx + alpha/ + index.ts + ``` + + - The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` file; + - Running `yarn package start --entrypoint dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` file. + ## 0.34.4-next.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 3c12b383ad..770aac72a6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.4-next.1", + "version": "0.34.4-next.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index dda4d6e222..7c8e46f3f8 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.4-next.0", + "version": "1.10.5-next.0", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/config/package.json b/packages/config/package.json index ec6cd78422..87e69d0157 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config", - "version": "1.3.4-next.0", + "version": "1.3.5-next.0", "description": "Config API used by Backstage core, backend, and CLI", "backstage": { "role": "common-library" diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index b6109caf2e..807d844712 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/core-components +## 0.18.2-next.2 + +### Patch Changes + +- 95935fb: Fixed dependency graph automatically scrolling forever +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + ## 0.18.2-next.1 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index c2d09762b3..3418bbe033 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.18.2-next.1", + "version": "0.18.2-next.2", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index b2960b4c83..baa76026b6 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/create-app +## 0.7.5-next.2 + +### Patch Changes + +- Bumped create-app version. + ## 0.7.5-next.1 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index cd37f6eef6..eac44e445a 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.5-next.1", + "version": "0.7.5-next.2", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 7f7338afeb..4a0af6926c 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/dev-utils +## 1.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + ## 1.1.15-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 707717f6a4..9540f79c9e 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.15-next.1", + "version": "1.1.15-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 54f526a1f2..121bf3b763 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,18 @@ # techdocs-cli-embedded-app +## 0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.4-next.2 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/test-utils@1.7.12-next.1 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + ## 0.2.113-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 951a3df06e..a4019eb918 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.113-next.1", + "version": "0.2.113-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 8425798421..a7005cc96d 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/test-utils +## 1.7.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + ## 1.7.12-next.0 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 6e914f3b50..1eb1849d1f 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.7.12-next.0", + "version": "1.7.12-next.1", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 9cb3a6527b..5d531e0a9b 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.6.9-next.0 + +### Patch Changes + +- d5cbdba: The `UnifiedThemeProvider` now coordinates theme attributes on the document `body` in case multiple theme providers are rendered. + ## 0.6.8 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 3699d858a4..1ac8aeb9bb 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/theme", - "version": "0.6.8", + "version": "0.6.9-next.0", "description": "material-ui theme for use with Backstage.", "backstage": { "role": "web-library" diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 4c9d6f833b..2efdbb2b9e 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/ui +## 0.7.2-next.1 + +### Patch Changes + +- a9b88be: Enable tooltips on disabled buttons with automatic wrapper +- 4adbb03: Avoid overriding onChange when spreading props + ## 0.7.2-next.0 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index b4432c8b6b..448905ab9d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.7.2-next.0", + "version": "0.7.2-next.1", "backstage": { "role": "web-library" }, diff --git a/plugins/app/CHANGELOG.md b/plugins/app/CHANGELOG.md index f4bf0f1255..8e3730b128 100644 --- a/plugins/app/CHANGELOG.md +++ b/plugins/app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app +## 0.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.3.1-next.1 ### Patch Changes diff --git a/plugins/app/package.json b/plugins/app/package.json index 5a31bcc847..d2e405e4b9 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app", - "version": "0.3.1-next.1", + "version": "0.3.1-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "app", diff --git a/plugins/auth/CHANGELOG.md b/plugins/auth/CHANGELOG.md index 506006165a..af891de687 100644 --- a/plugins/auth/CHANGELOG.md +++ b/plugins/auth/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.1.1-next.0 ### Patch Changes diff --git a/plugins/auth/package.json b/plugins/auth/package.json index 578a7246ec..ca242b37ca 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth", - "version": "0.1.1-next.0", + "version": "0.1.1-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "auth", diff --git a/plugins/bui-themer/CHANGELOG.md b/plugins/bui-themer/CHANGELOG.md new file mode 100644 index 0000000000..14720ff0e7 --- /dev/null +++ b/plugins/bui-themer/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-mui-to-bui + +## 0.2.0-next.0 + +### Minor Changes + +- d5cbdba: This is the first release of the Material UI to Backstage UI migration helper plugin. It adds a new page at `/mui-to-bui` that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, with live preview and copy/download. + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.2-next.1 + - @backstage/theme@0.6.9-next.0 diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json index 70828fb893..c7d9ef7a63 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/bui-themer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mui-to-bui", - "version": "0.1.0", + "version": "0.2.0-next.0", "backstage": { "role": "frontend-plugin", "pluginId": "mui-to-bui", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 0f2c5cb2ae..7051f03873 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend +## 3.1.2-next.2 + +### Patch Changes + +- 6493c98: Log before provider-orphaning eviction happens +- 77516c5: Added new `catalog:validate-entity` action to actions registry. + + This action can be used to validate entities against the software catalog. + This is useful for validating `catalog-info.yaml` file changes locally using the + Backstage MCP server. + ## 3.1.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index fbb6789146..d3ca09ead2 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "3.1.1-next.1", + "version": "3.1.2-next.2", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 54dbb31ae0..68ef6448a5 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-home +## 0.8.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.8.13-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 52a4fdd395..910d6c036f 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.13-next.1", + "version": "0.8.13-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index ee05844a0b..7704c954b2 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-react +## 0.5.12-next.2 + +### Patch Changes + +- ac405f2: The configmaps added to be rendered +- f7a4144: Fixes calculation of CPU utilization in the PodTable +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + ## 0.5.12-next.1 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 583bb5ef0d..76d1a08cdf 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.5.12-next.1", + "version": "0.5.12-next.2", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/notifications-backend-module-slack/CHANGELOG.md b/plugins/notifications-backend-module-slack/CHANGELOG.md index 757678ac3e..55ef69b671 100644 --- a/plugins/notifications-backend-module-slack/CHANGELOG.md +++ b/plugins/notifications-backend-module-slack/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-notifications-backend-module-slack +## 0.2.0-next.1 + +### Minor Changes + +- 3d09bb2: Adds username as optional config in order to send Slack notifications with a specific username in the case when using one Slack App for more than just Backstage. + ## 0.1.6-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 118e54493b..e3c7624fb8 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-slack", - "version": "0.1.6-next.0", + "version": "0.2.0-next.1", "description": "The slack backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 15e529d14f..713066e692 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-notifications-backend +## 0.5.11-next.1 + +### Patch Changes + +- 3b8e156: Fixed exclude entity reference not working in notification sending + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index c12535bd50..1c515a4702 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.5.11-next.0", + "version": "0.5.11-next.1", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 6f3ae7c818..caedf21e35 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-notifications +## 0.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.5.10-next.1 ### Patch Changes diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index eb1a616832..7a81fc8914 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.5.10-next.1", + "version": "0.5.10-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 74115ab64e..f986191147 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-org +## 0.6.45-next.2 + +### Patch Changes + +- 8b7351f: Add `initialRelationAggregation` and `showAggregateMembersToggle` options to `EntityMembersListCard` as well to `EntityOwnershipCard` +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + ## 0.6.45-next.1 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index b879153b9d..541195f461 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.45-next.1", + "version": "0.6.45-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index ad51eccb64..729343c096 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-react +## 1.19.2-next.2 + +### Patch Changes + +- e61f89e: Don't change loading to false until we've actually got some log state +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 1.19.2-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 1c61387895..06fdf0cd2f 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.19.2-next.1", + "version": "1.19.2-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 60ef454a7c..649965149d 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder +## 1.34.2-next.2 + +### Patch Changes + +- 075e064: Added missing form fields for the new frontend system. +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/core-components@0.18.2-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + ## 1.34.2-next.1 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index ef05bbd203..21e57a97c8 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.34.2-next.1", + "version": "1.34.2-next.2", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 06ef62a738..2da63fb848 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-react +## 1.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 1.9.5-next.1 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 320eef5c5d..8ffffb865d 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.9.5-next.1", + "version": "1.9.5-next.2", "backstage": { "role": "web-library", "pluginId": "search", diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index fc017d9bce..1e41067d82 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-signals +## 0.0.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.0.24-next.1 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 3e2a2fc337..13bf863b60 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.24-next.1", + "version": "0.0.24-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "signals", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 9fe41a7e39..b035159c51 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-techdocs +## 1.15.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + ## 1.15.1-next.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 97480471c6..242ca96c77 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.15.1-next.1", + "version": "1.15.1-next.2", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index a5e5104c71..ea0c76e43f 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-user-settings +## 0.8.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.8.27-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index ca2b5f2e9e..2ac494cd61 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.27-next.1", + "version": "0.8.27-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", From 90496fe4727593f070384fb2741ac26a0f0422f2 Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Tue, 30 Sep 2025 17:10:30 +0200 Subject: [PATCH 147/193] fix: forward disabled prop Signed-off-by: Tommy Le --- .../components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 0651066cfb..2e93c8b71d 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -115,6 +115,7 @@ function buildEntityPickerUISchema( ...extraOptions, catalogFilter, }, + 'ui:disabled': uiSchema['ui:disabled'], }; } From d9aed7400c5ac6f383b1384ea8a43cc80341e7dd Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Tue, 30 Sep 2025 17:11:22 +0200 Subject: [PATCH 148/193] chore: add changeset Signed-off-by: Tommy Le --- .changeset/tidy-coats-know.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-coats-know.md diff --git a/.changeset/tidy-coats-know.md b/.changeset/tidy-coats-know.md new file mode 100644 index 0000000000..e641c12784 --- /dev/null +++ b/.changeset/tidy-coats-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Forward `ui:disabled` in `OwnedEntityPicker` to allow disabling it From c10c5ee1102314f92654b78533c2bfb20ada3684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 1 Oct 2025 11:29:33 +0200 Subject: [PATCH 149/193] Update plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../src/__testUtils__/handlers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 4f349a1978..2b28381ceb 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -268,8 +268,7 @@ const httpProjectFindByIdDynamic = all_projects_response.map(project => { }); /** - * Checks for both project id and namespaced path, as these can both be used for the :id segment in Gitlab API: - * https://docs.gitlab.com/api/repository_files/#get-file-from-repository + * See https://docs.gitlab.com/api/repository_files/#get-file-from-repository */ const httpProjectCatalogDynamic = all_projects_response.flatMap(project => { return rest.head( From d14ef24882b25e9c35c34a8076acc54ef38bcf57 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Oct 2025 15:09:57 +0200 Subject: [PATCH 150/193] cli: add Backstage Yarn plugin support for templating Signed-off-by: Patrik Oldsberg --- .changeset/yarn-plugin-integration.md | 5 + docs/tooling/cli/04-templates.md | 10 ++ packages/cli/src/lib/version.test.ts | 106 ++++++++++++++ packages/cli/src/lib/version.ts | 32 +++-- packages/cli/src/lib/yarnPlugin.test.ts | 131 ++++++++++++++++++ packages/cli/src/lib/yarnPlugin.ts | 66 +++++++++ .../modules/migrate/commands/versions/bump.ts | 42 +----- .../new/lib/execution/PortableTemplater.ts | 6 +- 8 files changed, 346 insertions(+), 52 deletions(-) create mode 100644 .changeset/yarn-plugin-integration.md create mode 100644 packages/cli/src/lib/yarnPlugin.test.ts create mode 100644 packages/cli/src/lib/yarnPlugin.ts diff --git a/.changeset/yarn-plugin-integration.md b/.changeset/yarn-plugin-integration.md new file mode 100644 index 0000000000..2847e89273 --- /dev/null +++ b/.changeset/yarn-plugin-integration.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies. diff --git a/docs/tooling/cli/04-templates.md b/docs/tooling/cli/04-templates.md index c3bfb57d11..f634894c4f 100644 --- a/docs/tooling/cli/04-templates.md +++ b/docs/tooling/cli/04-templates.md @@ -147,3 +147,13 @@ The `role` property in the template yaml file is used to determine what input wi | `plugin-web-library` | `pluginId` | `plugins` | none | | `plugin-node-library` | `pluginId` | `plugins` | none | | `plugin-common-library` | `pluginId` | `plugins` | none | + +## Dependency Versioning + +The `yarn new` command automatically detects if the [Backstage Yarn plugin](https://github.com/backstage/backstage/tree/master/packages/yarn-plugin) is installed in your repository and adjusts dependency versioning accordingly. + +When the Backstage Yarn plugin is installed (detected via `.yarnrc.yml`), `yarn new` will generate `backstage:^` ranges for all `@backstage/*` dependencies. This ensures that new packages use the same Backstage version as defined in your `backstage.json` file. + +When the plugin is not installed, `yarn new` uses the standard npm version ranges (e.g., `^1.0.0`) for all dependencies, maintaining backward compatibility. + +Regardless of plugin installation, `workspace:` ranges found in your `yarn.lock` file will always take precedence over both `backstage:^` and npm ranges. This ensures that packages within monorepos continue to use workspace linking when available. diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts index 744874d7e9..4cb7591faf 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/lib/version.test.ts @@ -79,4 +79,110 @@ describe('createPackageVersionProvider', () => { `^${corePluginApiPkg.version}`, ); }); + + describe('with backstage protocol options', () => { + it('should return backstage:^ for @backstage packages when preferBackstageProtocol is true', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: true, + }); + + expect(provider('@backstage/core-plugin-api')).toBe('backstage:^'); + expect(provider('@backstage/cli')).toBe('backstage:^'); + }); + + it('should not return backstage:^ for non-@backstage packages even when preferBackstageProtocol is true', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"react@^18.0.0": + version "18.0.0" + +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" + +"@internal/library@workspace:packages/internal": + version "0.0.0-use.local" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: true, + }); + + expect(provider('react', '18.0.0')).toBe('^18.0.0'); + expect(provider('@backstage/core-plugin-api')).toBe('backstage:^'); + expect(provider('@internal/library')).toBe('workspace:^'); + }); + + it('should prefer workspace ranges over backstage protocol', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"react@workspace:packages/internal": + version "0.0.0-use.local" + +"@backstage/core-plugin-api@workspace:packages/internal": + version "0.0.0-use.local" + +"@internal/library@workspace:packages/internal": + version "0.0.0-use.local" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: true, + }); + + expect(provider('react')).toBe('workspace:^'); + expect(provider('@backstage/core-plugin-api')).toBe('workspace:^'); + expect(provider('@internal/library')).toBe('workspace:^'); + }); + + it('should not use backstage protocol when preferBackstageProtocol is false', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: false, + }); + + expect(provider('@backstage/core-plugin-api')).toBe( + `^${corePluginApiPkg.version}`, + ); + }); + + it('should not use backstage protocol when options are not provided', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile); + + expect(provider('@backstage/core-plugin-api')).toBe( + `^${corePluginApiPkg.version}`, + ); + }); + }); }); diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 5a2a8c7838..a7a7ca1bdb 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -89,23 +89,35 @@ export function findVersion() { export const version = findVersion(); export const isDev = fs.pathExistsSync(paths.resolveOwn('src')); -export function createPackageVersionProvider(lockfile?: Lockfile) { +export function createPackageVersionProvider( + lockfile?: Lockfile, + options?: { + preferBackstageProtocol?: boolean; + }, +) { return (name: string, versionHint?: string): string => { const packageVersion = packageVersions[name]; + + // 1) workspace precedence (existing logic) - check this first + const lockfileEntries = lockfile?.get(name); + const lockfileEntry = lockfileEntries?.find(entry => + entry.range.startsWith('workspace:'), + ); + if (lockfileEntry) { + return 'workspace:^'; + } + + // 2) backstage:^ when plugin is present and allowed + if (options?.preferBackstageProtocol && name.startsWith('@backstage/')) { + return 'backstage:^'; + } + + // 3) fallback to current npm resolution const targetVersion = versionHint || packageVersion; if (!targetVersion) { throw new Error(`No version available for package ${name}`); } - const lockfileEntries = lockfile?.get(name); - - for (const specifier of ['^', '~', '*']) { - const range = `workspace:${specifier}`; - if (lockfileEntries?.some(entry => entry.range === range)) { - return range; - } - } - const validRanges = lockfileEntries?.filter(entry => semver.satisfies(targetVersion, entry.range), ); diff --git a/packages/cli/src/lib/yarnPlugin.test.ts b/packages/cli/src/lib/yarnPlugin.test.ts new file mode 100644 index 0000000000..d5c865e1c4 --- /dev/null +++ b/packages/cli/src/lib/yarnPlugin.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright 2020 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 { createMockDirectory } from '@backstage/backend-test-utils'; +import { getHasYarnPlugin } from './yarnPlugin'; + +const mockDir = createMockDirectory(); + +jest.mock('./paths', () => ({ + paths: { + resolveTargetRoot(filename: string) { + return mockDir.resolve(filename); + }, + }, +})); + +describe('getHasYarnPlugin', () => { + beforeEach(() => { + mockDir.clear(); + }); + + it('should return false when .yarnrc.yml does not exist', async () => { + mockDir.setContent({}); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return false when .yarnrc.yml is empty', async () => { + mockDir.setContent({ + '.yarnrc.yml': '', + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return false when plugins array is empty', async () => { + mockDir.setContent({ + '.yarnrc.yml': 'plugins: []', + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return false when plugins array does not contain backstage plugin', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return true when backstage plugin is present', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs + - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(true); + }); + + it('should return true when backstage plugin is the only plugin', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(true); + }); + + it('should throw error when .yarnrc.yml has invalid content', async () => { + mockDir.setContent({ + '.yarnrc.yml': 'invalid: yaml: content: [', + }); + + await expect(getHasYarnPlugin()).rejects.toThrow(); + }); + + it('should throw error when .yarnrc.yml has unexpected structure', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: "not an array" +`, + }); + + await expect(getHasYarnPlugin()).rejects.toThrow( + 'Unexpected content in .yarnrc.yml', + ); + }); + + it('should handle plugins with different structure', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs + - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(true); + }); +}); diff --git a/packages/cli/src/lib/yarnPlugin.ts b/packages/cli/src/lib/yarnPlugin.ts new file mode 100644 index 0000000000..5ad49b2524 --- /dev/null +++ b/packages/cli/src/lib/yarnPlugin.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2020 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 fs from 'fs-extra'; +import yaml from 'yaml'; +import z from 'zod'; +import { paths } from './paths'; + +const yarnRcSchema = z.object({ + plugins: z + .array( + z.object({ + path: z.string(), + }), + ) + .optional(), +}); + +/** + * Detects whether the Backstage Yarn plugin is installed in the target repository. + * + * @returns Promise - true if the plugin is installed, false otherwise + */ +export async function getHasYarnPlugin(): Promise { + const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml'); + const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { + if (e.code === 'ENOENT') { + // gracefully continue in case the file doesn't exist + return ''; + } + throw e; + }); + + if (!yarnRcContent) { + return false; + } + + const parseResult = yarnRcSchema.safeParse(yaml.parse(yarnRcContent)); + + if (!parseResult.success) { + throw new Error( + `Unexpected content in .yarnrc.yml: ${parseResult.error.toString()}`, + ); + } + + const yarnRc = parseResult.data; + + const backstagePlugin = yarnRc.plugins?.some( + plugin => plugin.path === '.yarn/plugins/@yarnpkg/plugin-backstage.cjs', + ); + + return Boolean(backstagePlugin); +} diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 391ccf8f22..39cfeddbd5 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -21,11 +21,10 @@ import chalk from 'chalk'; import { minimatch } from 'minimatch'; import semver from 'semver'; import { OptionValues } from 'commander'; -import yaml from 'yaml'; -import z from 'zod'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; import { paths } from '../../../../lib/paths'; +import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; import { mapDependencies, fetchPackageInfo, @@ -497,42 +496,3 @@ async function asLockfileVersion(version: string) { return version; } - -const yarnRcSchema = z.object({ - plugins: z - .array( - z.object({ - path: z.string(), - }), - ) - .optional(), -}); - -async function getHasYarnPlugin() { - const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml'); - const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { - if (e.code === 'ENOENT') { - // gracefully continue in case the file doesn't exist - return ''; - } - throw e; - }); - - if (!yarnRcContent) { - return false; - } - - const parseResult = yarnRcSchema.safeParse(yaml.parse(yarnRcContent)); - - if (!parseResult.success) { - throw new Error( - `Unexpected content in .yarnrc.yml: ${parseResult.error.toString()}`, - ); - } - - const yarnRc = parseResult.data; - - return yarnRc.plugins?.some( - plugin => plugin.path === '.yarn/plugins/@yarnpkg/plugin-backstage.cjs', - ); -} diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts index 519f7c6dbc..9dfd60baba 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts @@ -27,6 +27,7 @@ import lowerFirst from 'lodash/lowerFirst'; import { Lockfile } from '../../../../lib/versioning'; import { paths } from '../../../../lib/paths'; import { createPackageVersionProvider } from '../../../../lib/version'; +import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; const builtInHelpers = { camelCase, @@ -53,7 +54,10 @@ export class PortableTemplater { /* ignored */ } - const versionProvider = createPackageVersionProvider(lockfile); + const hasYarnPlugin = await getHasYarnPlugin(); + const versionProvider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: hasYarnPlugin, + }); const templater = new PortableTemplater( { From 64dd4920aa12dbf3a3fd56025168928bfd815cff Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Oct 2025 15:38:34 +0200 Subject: [PATCH 151/193] Update .github/copilot-instructions.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 509b12f2e1..98073c5128 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ The following files contain guidelines for the project: Before any of these commands can be run, you need to run `yarn install` in the project root. - Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline. -- Test: Use `yarn test --no-watch ` in the project root to run tests. The path can be either a single file or a directory. Always provide a patch, avoid running all tests. +- Test: Use `yarn test --no-watch ` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests. - Type checking: Use `yarn tsc` in the project root to run the type checker. - Code formatting: Use `yarn prettier --write ` to format code. - Lint: Use `yarn lint --fix` in the project root to run the linter. From bdef1e2254b9de7f5eed10fa7f8a568ddfc8ba2b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 30 Sep 2025 11:21:48 +0200 Subject: [PATCH 152/193] docs/tooling/package-metadata: add docs for backstage.features Signed-off-by: Patrik Oldsberg --- docs/tooling/package-metadata.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/tooling/package-metadata.md b/docs/tooling/package-metadata.md index ad9d5ec5a0..3911ada413 100644 --- a/docs/tooling/package-metadata.md +++ b/docs/tooling/package-metadata.md @@ -96,6 +96,32 @@ This field can be generated by the `backstage-cli repo fix --publish` command. T The presence of this field is checked by the `backstage-cli package prepack` command, which is used to prepare a package for publishing. You can read more about this requirement in the section on [metadata for published packages](#metadata-for-published-packages). +### `backstage.features` + +This field declares where to find Backstage features exported from the package. It is a map of features export paths to their feature type, similar to the `exports` field. + +```js title="Example usage of the backstage.features field" +{ + "name": "@backstage/plugin-catalog", + "backstage": { + "features": { + "./alpha": "@backstage/FrontendPlugin" + } + } +} +``` + +This field is automatically generated by the `backstage-cli package prepack` command when publishing a package and therefore does not need to be committed to the repository. + +If you are using your own tooling to publish a package, you need to populate this field manually, using the following feature types: + +- `@backstage/BackendFeature` +- `@backstage/FrontendPlugin` +- `@backstage/FrontendModule` +- `@backstage/FrontendFeatureLoader` + +You can read more about the feature types in the [backend system](../backend-system/architecture/01-index.md) and [frontend system](../frontend-system/architecture/00-index.md) documentation. + ### `backstage.pluginPackages` For any package that is part of a plugin, this field should be set to a list of all packages that are directly part of the same plugin. This includes frontend and backend plugin packages as well as related libraries, but not modules. From a57ad4ba0bcef45605a0b30b08a3bbc75e688f61 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 30 Sep 2025 14:18:55 +0200 Subject: [PATCH 153/193] bui-themer: rename folder to mui-to-bui Signed-off-by: Patrik Oldsberg --- plugins/{bui-themer => mui-to-bui}/.eslintrc.js | 0 plugins/{bui-themer => mui-to-bui}/CHANGELOG.md | 0 plugins/{bui-themer => mui-to-bui}/README.md | 0 plugins/{bui-themer => mui-to-bui}/catalog-info.yaml | 0 plugins/{bui-themer => mui-to-bui}/dev/index.tsx | 0 plugins/{bui-themer => mui-to-bui}/knip-report.md | 0 plugins/{bui-themer => mui-to-bui}/package.json | 2 +- plugins/{bui-themer => mui-to-bui}/report.api.md | 0 .../src/components/BuiThemerPage/BuiThemePreview.test.tsx | 0 .../src/components/BuiThemerPage/BuiThemePreview.tsx | 0 .../src/components/BuiThemerPage/BuiThemerPage.test.tsx | 0 .../src/components/BuiThemerPage/BuiThemerPage.tsx | 0 .../src/components/BuiThemerPage/MuiThemeExtractor.test.tsx | 0 .../src/components/BuiThemerPage/MuiThemeExtractor.tsx | 0 .../src/components/BuiThemerPage/ThemeContent.test.tsx | 0 .../src/components/BuiThemerPage/ThemeContent.tsx | 0 .../src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts | 0 .../src/components/BuiThemerPage/convertMuiToBuiTheme.ts | 0 .../src/components/BuiThemerPage/index.ts | 0 plugins/{bui-themer => mui-to-bui}/src/index.ts | 0 plugins/{bui-themer => mui-to-bui}/src/plugin.test.ts | 0 plugins/{bui-themer => mui-to-bui}/src/plugin.tsx | 0 plugins/{bui-themer => mui-to-bui}/src/routes.ts | 0 yarn.lock | 4 ++-- 24 files changed, 3 insertions(+), 3 deletions(-) rename plugins/{bui-themer => mui-to-bui}/.eslintrc.js (100%) rename plugins/{bui-themer => mui-to-bui}/CHANGELOG.md (100%) rename plugins/{bui-themer => mui-to-bui}/README.md (100%) rename plugins/{bui-themer => mui-to-bui}/catalog-info.yaml (100%) rename plugins/{bui-themer => mui-to-bui}/dev/index.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/knip-report.md (100%) rename plugins/{bui-themer => mui-to-bui}/package.json (97%) rename plugins/{bui-themer => mui-to-bui}/report.api.md (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/BuiThemePreview.test.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/BuiThemePreview.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/BuiThemerPage.test.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/BuiThemerPage.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/MuiThemeExtractor.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/ThemeContent.test.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/ThemeContent.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/convertMuiToBuiTheme.ts (100%) rename plugins/{bui-themer => mui-to-bui}/src/components/BuiThemerPage/index.ts (100%) rename plugins/{bui-themer => mui-to-bui}/src/index.ts (100%) rename plugins/{bui-themer => mui-to-bui}/src/plugin.test.ts (100%) rename plugins/{bui-themer => mui-to-bui}/src/plugin.tsx (100%) rename plugins/{bui-themer => mui-to-bui}/src/routes.ts (100%) diff --git a/plugins/bui-themer/.eslintrc.js b/plugins/mui-to-bui/.eslintrc.js similarity index 100% rename from plugins/bui-themer/.eslintrc.js rename to plugins/mui-to-bui/.eslintrc.js diff --git a/plugins/bui-themer/CHANGELOG.md b/plugins/mui-to-bui/CHANGELOG.md similarity index 100% rename from plugins/bui-themer/CHANGELOG.md rename to plugins/mui-to-bui/CHANGELOG.md diff --git a/plugins/bui-themer/README.md b/plugins/mui-to-bui/README.md similarity index 100% rename from plugins/bui-themer/README.md rename to plugins/mui-to-bui/README.md diff --git a/plugins/bui-themer/catalog-info.yaml b/plugins/mui-to-bui/catalog-info.yaml similarity index 100% rename from plugins/bui-themer/catalog-info.yaml rename to plugins/mui-to-bui/catalog-info.yaml diff --git a/plugins/bui-themer/dev/index.tsx b/plugins/mui-to-bui/dev/index.tsx similarity index 100% rename from plugins/bui-themer/dev/index.tsx rename to plugins/mui-to-bui/dev/index.tsx diff --git a/plugins/bui-themer/knip-report.md b/plugins/mui-to-bui/knip-report.md similarity index 100% rename from plugins/bui-themer/knip-report.md rename to plugins/mui-to-bui/knip-report.md diff --git a/plugins/bui-themer/package.json b/plugins/mui-to-bui/package.json similarity index 97% rename from plugins/bui-themer/package.json rename to plugins/mui-to-bui/package.json index c7d9ef7a63..b8cb51b1c8 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/mui-to-bui/package.json @@ -16,7 +16,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "plugins/bui-themer" + "directory": "plugins/mui-to-bui" }, "license": "Apache-2.0", "sideEffects": false, diff --git a/plugins/bui-themer/report.api.md b/plugins/mui-to-bui/report.api.md similarity index 100% rename from plugins/bui-themer/report.api.md rename to plugins/mui-to-bui/report.api.md diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.test.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.test.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.test.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.test.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/MuiThemeExtractor.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/MuiThemeExtractor.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.test.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.test.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.tsx similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.tsx rename to plugins/mui-to-bui/src/components/BuiThemerPage/ThemeContent.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts rename to plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts rename to plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts diff --git a/plugins/bui-themer/src/components/BuiThemerPage/index.ts b/plugins/mui-to-bui/src/components/BuiThemerPage/index.ts similarity index 100% rename from plugins/bui-themer/src/components/BuiThemerPage/index.ts rename to plugins/mui-to-bui/src/components/BuiThemerPage/index.ts diff --git a/plugins/bui-themer/src/index.ts b/plugins/mui-to-bui/src/index.ts similarity index 100% rename from plugins/bui-themer/src/index.ts rename to plugins/mui-to-bui/src/index.ts diff --git a/plugins/bui-themer/src/plugin.test.ts b/plugins/mui-to-bui/src/plugin.test.ts similarity index 100% rename from plugins/bui-themer/src/plugin.test.ts rename to plugins/mui-to-bui/src/plugin.test.ts diff --git a/plugins/bui-themer/src/plugin.tsx b/plugins/mui-to-bui/src/plugin.tsx similarity index 100% rename from plugins/bui-themer/src/plugin.tsx rename to plugins/mui-to-bui/src/plugin.tsx diff --git a/plugins/bui-themer/src/routes.ts b/plugins/mui-to-bui/src/routes.ts similarity index 100% rename from plugins/bui-themer/src/routes.ts rename to plugins/mui-to-bui/src/routes.ts diff --git a/yarn.lock b/yarn.lock index dcba3ac6ae..563496b6f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5708,9 +5708,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-mui-to-bui@workspace:^, @backstage/plugin-mui-to-bui@workspace:plugins/bui-themer": +"@backstage/plugin-mui-to-bui@workspace:^, @backstage/plugin-mui-to-bui@workspace:plugins/mui-to-bui": version: 0.0.0-use.local - resolution: "@backstage/plugin-mui-to-bui@workspace:plugins/bui-themer" + resolution: "@backstage/plugin-mui-to-bui@workspace:plugins/mui-to-bui" dependencies: "@backstage/cli": "workspace:^" "@backstage/core-compat-api": "workspace:^" From 533bbe21fe2cfe91dce0d0d2f832d2b598ebf913 Mon Sep 17 00:00:00 2001 From: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:23:33 -0400 Subject: [PATCH 154/193] Rewrite architectural overview for feature #21926 (#31209) * Update architecture-overview.md Updated the architectural overview. Rewrote sections and created new graphics. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Add files via upload Architectural diagram of the Backstage frontend and backend. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Add files via upload Added new graphics to illustrate simplified standalone plugin architecture, service-based-plugin-architecture, and third-party-plugin architecture. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Add files via upload Cleaned up graphic Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Cleaned up service-based plugin graphic Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md Updated graphics and added note to plugin architecture graphics. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Updated plugin-architecture heading links in architecture-overview.md Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md fix formatting Fixed formatting that occurred during copy-paste. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md Rearranged some of the sections. Added additional text and links. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md fixed link for02-backends Fixed link to backend-system/architecture/02-backends.md. Forgot the "architecture" in the path. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md updated database section Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md added links for core frontend plugins Added links and descriptions of the core frontend plugins. The links are back to the overview pages under features for each of the plugins Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Updated diagram to show Kubernetes as core plugin Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Updated graphics to remove the work Container Changed "Frontend Container" to "Frontend". Changed "Backend Container" to "Backend" Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md Changed "Frontend Container" to "Frontend" everywhere in the doc. Changed "Backend Container" to "Backend" everywhere in the doc. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md fixed backend spelling errors Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md try to fix prettier errors Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update architecture-overview.md fix spelling error Changed backed to backend Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> --------- Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> --- .../backstage-front-back-arch.jpeg | Bin 0 -> 167346 bytes ...ied-service-based-plugin-architecture.jpeg | Bin 0 -> 220793 bytes ...lified-standalone-plugin-architecture.jpeg | Bin 0 -> 141598 bytes ...ified-third-party-plugin-architecture.jpeg | Bin 0 -> 236373 bytes docs/overview/architecture-overview.md | 364 +++++++----------- 5 files changed, 142 insertions(+), 222 deletions(-) create mode 100644 docs/assets/architecture-overview/backstage-front-back-arch.jpeg create mode 100644 docs/assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg create mode 100644 docs/assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg create mode 100644 docs/assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg diff --git a/docs/assets/architecture-overview/backstage-front-back-arch.jpeg b/docs/assets/architecture-overview/backstage-front-back-arch.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1cca115c95a067937b016bc355377ab47f3a9f63 GIT binary patch literal 167346 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 E01?7=H2?qr literal 0 HcmV?d00001 diff --git a/docs/assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg b/docs/assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e8f5144c9aee3e983f6f35fd6bd9ceb46f89b681 GIT binary patch literal 220793 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 literal 0 HcmV?d00001 diff --git a/docs/assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg b/docs/assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..bf96367b17906027796ef9ac01985937c224481d GIT binary patch literal 141598 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 KYXJ7K^M3#&jQL*x literal 0 HcmV?d00001 diff --git a/docs/assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg b/docs/assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..4b9fd1d35f4253073d63334d1590a3c27c52e65f GIT binary patch literal 236373 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 +); + // Extract layout decorator as a reusable constant const layoutDecorator = [ (Story: StoryFn) => ( - <> +
- +
), ]; @@ -129,6 +143,7 @@ export const Default: Story = { args: { title: 'Page Title', }, + decorators: [withWhiteBackground], }; export const WithTabs: Story = { @@ -136,11 +151,11 @@ export const WithTabs: Story = { ...Default.args, tabs, }, - decorators: [withRouter], + decorators: [withRouter, withWhiteBackground], }; export const WithCustomActions: Story = { - decorators: [withRouter], + decorators: [withRouter, withWhiteBackground], render: () => ( ( ( ); +// White background decorator for stories +const withWhiteBackground = (Story: StoryFn) => ( +
+ +
+); + export const Default: Story = { args: { children: '', }, - decorators: [withRouter], + decorators: [withRouter, withWhiteBackground], render: () => ( - Tab 1 - Tab 2 - Tab 3 With long title + + Tab 1 + + + Tab 2 + + + Tab 3 With long title + ), @@ -54,7 +69,7 @@ export const WithTabPanels: Story = { args: { children: '', }, - decorators: [withRouter], + decorators: [withRouter, withWhiteBackground], render: () => ( @@ -79,6 +94,7 @@ export const WithMockedURLTab2: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -111,6 +127,7 @@ export const WithMockedURLTab3: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -143,6 +160,7 @@ export const WithMockedURLNoMatch: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -181,6 +199,7 @@ export const ExactMatchingDefault: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -217,6 +236,7 @@ export const PrefixMatchingForNestedRoutes: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -257,6 +277,7 @@ export const PrefixMatchingDeepNesting: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -292,6 +313,7 @@ export const MixedMatchingStrategies: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -343,6 +365,7 @@ export const PrefixMatchingEdgeCases: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -385,6 +408,7 @@ export const PrefixMatchingWithSlash: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -426,6 +450,7 @@ export const RootPathMatching: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => ( @@ -460,6 +485,7 @@ export const AutoSelectionOfTabs: Story = { args: { children: '', }, + decorators: [withWhiteBackground], render: () => (
From 23d8df0ce0534026e5e913576724faa68f74a4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Mon, 6 Oct 2025 11:31:35 +0200 Subject: [PATCH 162/193] fix: add react-aria label to button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .../ui/src/components/HeaderPage/HeaderPage.stories.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index c3c2b59ae1..bba6e8a857 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -163,7 +163,11 @@ export const WithCustomActions: Story = { <> - } /> + } + aria-label="More options" + /> {menuItems.map(option => ( Date: Mon, 6 Oct 2025 11:32:59 +0200 Subject: [PATCH 163/193] Get rid of visualizer "missing key" errors in browser console MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/two-emus-like.md | 5 +++++ .../src/components/AppVisualizerPage/TextVisualizer.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/two-emus-like.md diff --git a/.changeset/two-emus-like.md b/.changeset/two-emus-like.md new file mode 100644 index 0000000000..e8af7a7fa0 --- /dev/null +++ b/.changeset/two-emus-like.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-visualizer': patch +--- + +Ensure that the text rendering has react keys for all elements diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx index b388da80b4..4385ba0938 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx @@ -63,15 +63,15 @@ function nodeToText( .filter(e => options?.showDisabled || e.instance) .sort((a, b) => a.spec.id.localeCompare(b.spec.id)); if (children.length === 0) { - return mkDiv(`${key} []`, { indent: true }); + return mkDiv(`${key} []`, { key, indent: true }); } return mkDiv( [ - mkDiv(`${key} [`), + mkDiv(`${key} [`, { key: 'start' }), ...children.map(e => - mkDiv(nodeToText(e, options), { indent: true }), + mkDiv(nodeToText(e, options), { indent: true, key: e.spec.id }), ), - mkDiv(']'), + mkDiv(']', { key: 'end' }), ], { key, indent: true }, ); From 09a14e809f0fd1dce66842f2ce58239338d2dcc4 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Thu, 18 Sep 2025 09:03:50 +0200 Subject: [PATCH 164/193] Clean up type Signed-off-by: TA31OU --- .../src/components/DependencyGraph/types.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index 2629d9fda1..d3e51db85b 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -99,17 +99,7 @@ export namespace DependencyGraphTypes { * * @public */ - export type RenderEdgeProps = { - edge: DependencyEdge; - id: dagre.Edge; - }; - - /** - * Custom React component for graph {@link DependencyGraphTypes.DependencyEdge} - * - * @public - */ - export type RenderEdgeFunction = (props: { + export type RenderEdgeProps = { edge: T & { points: { x: number; y: number }[]; label?: string; @@ -124,8 +114,17 @@ export namespace DependencyGraphTypes { to?: string; relations?: string[]; }; - id: { v: string; w: string }; - }) => ReactNode; + id: dagre.Edge; + }; + + /** + * Custom React component for graph {@link DependencyGraphTypes.DependencyEdge} + * + * @public + */ + export type RenderEdgeFunction = ( + props: RenderEdgeProps, + ) => ReactNode; /** * Graph direction From 1c25050ee974d15707f4ef55939eb2ed638455b4 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Thu, 18 Sep 2025 09:42:42 +0200 Subject: [PATCH 165/193] undo package change Signed-off-by: TA31OU --- plugins/catalog-graph/package.json | 2 -- yarn.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 57d6f7544a..86bd930095 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -61,7 +61,6 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "classnames": "^2.3.1", - "d3-shape": "^3.0.0", "lodash": "^4.17.15", "p-limit": "^3.1.0", "qs": "^6.9.4", @@ -77,7 +76,6 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.0.0", - "@types/d3-shape": "^3.0.1", "@types/react": "^18.0.0", "react": "^18.0.2", "react-dom": "^18.0.2", diff --git a/yarn.lock b/yarn.lock index 8c8cf7fe00..e606b8246d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4820,10 +4820,8 @@ __metadata: "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" - "@types/d3-shape": "npm:^3.0.1" "@types/react": "npm:^18.0.0" classnames: "npm:^2.3.1" - d3-shape: "npm:^3.0.0" lodash: "npm:^4.17.15" p-limit: "npm:^3.1.0" qs: "npm:^6.9.4" From 3beb1dee934863a25ce90841526a4b9115a31e14 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Thu, 18 Sep 2025 13:43:57 +0200 Subject: [PATCH 166/193] update api report Signed-off-by: TA31OU --- packages/core-components/report.api.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index c363ae9d56..97eb7d9761 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -318,7 +318,10 @@ export namespace DependencyGraphTypes { NETWORK_SIMPLEX = 'network-simplex', TIGHT_TREE = 'tight-tree', } - export type RenderEdgeFunction = (props: { + export type RenderEdgeFunction = ( + props: RenderEdgeProps, + ) => ReactNode; + export type RenderEdgeProps = { edge: T & { points: { x: number; @@ -336,13 +339,6 @@ export namespace DependencyGraphTypes { to?: string; relations?: string[]; }; - id: { - v: string; - w: string; - }; - }) => ReactNode; - export type RenderEdgeProps = { - edge: DependencyEdge; id: dagre.Edge; }; export type RenderLabelFunction = ( From 25aec46a8b0d63e9755170fa364a9cc144a8a07b Mon Sep 17 00:00:00 2001 From: TA31OU Date: Mon, 6 Oct 2025 11:26:23 +0200 Subject: [PATCH 167/193] Remove edge import Signed-off-by: TA31OU --- .../src/components/DependencyGraph/types.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index d3e51db85b..4436f8d1aa 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -21,7 +21,6 @@ */ import { ReactNode } from 'react'; -import dagre from '@dagrejs/dagre'; /** * Types for the {@link DependencyGraph} component. @@ -114,7 +113,11 @@ export namespace DependencyGraphTypes { to?: string; relations?: string[]; }; - id: dagre.Edge; + id: { + v: string; + w: string; + name?: string | undefined; + }; }; /** From 57e279d780ca06132994fdbc17dca74ac4b530f6 Mon Sep 17 00:00:00 2001 From: MrOpperman Date: Wed, 24 Sep 2025 09:32:39 +0200 Subject: [PATCH 168/193] Update .changeset/flat-peas-run.md Co-authored-by: Ben Lambert Signed-off-by: MrOpperman Signed-off-by: TA31OU --- .changeset/flat-peas-run.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/flat-peas-run.md b/.changeset/flat-peas-run.md index c09eadecf8..64d80107ba 100644 --- a/.changeset/flat-peas-run.md +++ b/.changeset/flat-peas-run.md @@ -1,6 +1,6 @@ --- -'@backstage/core-components': minor -'@backstage/plugin-catalog-graph': minor +'@backstage/core-components': patch +'@backstage/plugin-catalog-graph': patch --- Added `renderEdge` prop to component in @backstage/core-components to allow custom rendering of graph edges. From f9946a3edfdc1d23929be4888fb4473b66b93740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 6 Oct 2025 10:53:49 +0200 Subject: [PATCH 169/193] Update .changeset/flat-peas-run.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw Signed-off-by: TA31OU --- .changeset/flat-peas-run.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/flat-peas-run.md b/.changeset/flat-peas-run.md index 64d80107ba..bb075ce910 100644 --- a/.changeset/flat-peas-run.md +++ b/.changeset/flat-peas-run.md @@ -3,4 +3,4 @@ '@backstage/plugin-catalog-graph': patch --- -Added `renderEdge` prop to component in @backstage/core-components to allow custom rendering of graph edges. +Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. From 81fc26c6a4eaab95ea77dc5b611dff7dd6f71758 Mon Sep 17 00:00:00 2001 From: TA31OU Date: Mon, 6 Oct 2025 11:48:32 +0200 Subject: [PATCH 170/193] Update api report Signed-off-by: TA31OU --- packages/core-components/report.api.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 97eb7d9761..9c9992deff 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -18,7 +18,6 @@ import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; -import dagre from '@dagrejs/dagre'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; import IconButton from '@material-ui/core/IconButton'; @@ -339,7 +338,11 @@ export namespace DependencyGraphTypes { to?: string; relations?: string[]; }; - id: dagre.Edge; + id: { + v: string; + w: string; + name?: string | undefined; + }; }; export type RenderLabelFunction = ( props: RenderLabelProps, @@ -1575,8 +1578,8 @@ export type WarningPanelClassKey = // Warnings were encountered during analysis: // -// src/components/DependencyGraph/types.d.ts:23:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" -// src/components/DependencyGraph/types.d.ts:27:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" +// src/components/DependencyGraph/types.d.ts:22:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" +// src/components/DependencyGraph/types.d.ts:26:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" // src/components/TabbedLayout/RoutedTabs.d.ts:8:5 - (ae-forgotten-export) The symbol "SubRoute_2" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts From dd69cf60c4b211b8df9cbec7cdd8f57d15d0aa16 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Oct 2025 13:21:35 +0200 Subject: [PATCH 171/193] backend-app-api: earlier handling or unhandled errors Signed-off-by: Patrik Oldsberg --- .changeset/cuddly-mugs-act.md | 5 +++ .../src/wiring/BackendInitializer.ts | 43 +++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 .changeset/cuddly-mugs-act.md diff --git a/.changeset/cuddly-mugs-act.md b/.changeset/cuddly-mugs-act.md new file mode 100644 index 0000000000..1b939d8196 --- /dev/null +++ b/.changeset/cuddly-mugs-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 9b925adb30..09b22e3a7a 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -258,6 +258,30 @@ export class BackendInitializer { createInstanceMetadataServiceFactory(this.#registrations), ); + // This makes sure that any uncaught errors or unhandled rejections are + // caught and logged, rather than terminating the process. We register these + // as early as possible while still using the root logger service, the + // tradeoff that if there are any unhandled errors as part of the that + // instationation, it will cause the process to crash. If there are multiple + // backend instances, each instance will log the error, because we can't + // determine which instance the error came from. + if (process.env.NODE_ENV !== 'test') { + const rootLogger = await this.#serviceRegistry.get( + coreServices.rootLogger, + 'root', + ); + process.on('unhandledRejection', (reason: Error) => { + rootLogger + ?.child({ type: 'unhandledRejection' }) + ?.error('Unhandled rejection', reason); + }); + process.on('uncaughtException', error => { + rootLogger + ?.child({ type: 'uncaughtException' }) + ?.error('Uncaught exception', error); + }); + } + // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); @@ -449,25 +473,6 @@ export class BackendInitializer { await lifecycleService.startup(); initLogger.onAllStarted(); - - // Once the backend is started, any uncaught errors or unhandled rejections are caught - // and logged, in order to avoid crashing the entire backend on local failures. - if (process.env.NODE_ENV !== 'test') { - const rootLogger = await this.#serviceRegistry.get( - coreServices.rootLogger, - 'root', - ); - process.on('unhandledRejection', (reason: Error) => { - rootLogger - ?.child({ type: 'unhandledRejection' }) - ?.error('Unhandled rejection', reason); - }); - process.on('uncaughtException', error => { - rootLogger - ?.child({ type: 'uncaughtException' }) - ?.error('Uncaught exception', error); - }); - } } // It's fine to call .stop() multiple times, which for example can happen with manual stop + process exit From 04530de989224f2b6477a484888b068e03d92286 Mon Sep 17 00:00:00 2001 From: Marley Date: Fri, 26 Sep 2025 08:55:39 +0100 Subject: [PATCH 172/193] Change `target` to `targetRef` in catalog customization Signed-off-by: Marley Powell --- docs/features/software-catalog/catalog-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 2ed4b34822..a828e6591a 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -693,6 +693,6 @@ filter: relations: $contains: type: ownedBy - target: + targetRef: $in: [group:default/admins, group:default/viewers] ``` From fd23d3ed71cbcf57b93000fee12a7b9ed2e9fa0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Mon, 6 Oct 2025 16:25:39 +0200 Subject: [PATCH 173/193] use react router for internal routing in menus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- packages/ui/src/components/Menu/Menu.tsx | 46 +++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index a55d6fdafb..cea8aa2cb8 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -51,8 +51,8 @@ import { RiCheckLine, RiCloseCircleLine, } from '@remixicon/react'; -import { isExternalLink } from '../../utils/isExternalLink'; import { useNavigate, useHref } from 'react-router-dom'; +import { isExternalLink } from '../../utils/isExternalLink'; const MenuEmptyState = () => { const { classNames } = useStyles('Menu'); @@ -74,19 +74,22 @@ export const SubmenuTrigger = (props: SubmenuTriggerProps) => { export const Menu = (props: MenuProps) => { const { placement = 'bottom start', ...rest } = props; const { classNames } = useStyles('Menu'); + const navigate = useNavigate(); return ( - - - - {props.children} - - - - - - + + + + + {props.children} + + + + + + + ); }; @@ -201,7 +204,6 @@ export const MenuAutocompleteListbox = ( export const MenuItem = (props: MenuItemProps) => { const { iconStart, color = 'primary', children, href, ...rest } = props; const { classNames } = useStyles('Menu'); - const navigate = useNavigate(); const isLink = href !== undefined; const isExternal = isExternalLink(href); @@ -224,11 +226,23 @@ export const MenuItem = (props: MenuItemProps) => { ); - if (isLink && !isExternal) { + if (isLink && isExternal) { return ( - - {content} - + window.open(href, '_blank', 'noopener,noreferrer')} + {...rest} + > +
+ {iconStart} + {children} +
+
+ +
+
); } From f6dff5b4de96a6f600004f0741435cf8775a2fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Mon, 6 Oct 2025 16:27:15 +0200 Subject: [PATCH 174/193] add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .changeset/tough-clocks-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tough-clocks-attack.md diff --git a/.changeset/tough-clocks-attack.md b/.changeset/tough-clocks-attack.md new file mode 100644 index 0000000000..caf3b33fe4 --- /dev/null +++ b/.changeset/tough-clocks-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Using react router for internal links in the Menu component From c49f392f9e1ee1194e91d75dbf6150ece81d8eb9 Mon Sep 17 00:00:00 2001 From: Christine Lee Date: Mon, 6 Oct 2025 10:34:52 -0400 Subject: [PATCH 175/193] add datadog-entity-sync.yaml Signed-off-by: Christine Lee --- microsite/data/plugins/datadog-entity-sync.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 microsite/data/plugins/datadog-entity-sync.yaml diff --git a/microsite/data/plugins/datadog-entity-sync.yaml b/microsite/data/plugins/datadog-entity-sync.yaml new file mode 100644 index 0000000000..2f0cd06cd5 --- /dev/null +++ b/microsite/data/plugins/datadog-entity-sync.yaml @@ -0,0 +1,10 @@ +--- +title: Datadog Entity Sync +author: datadog +authorUrl: https://www.datadoghq.com/ +category: Development +description: Sync entities from the Backstage catalog to the Datadog Software Catalog. +documentation: https://github.com/DataDog/datadog-backstage-plugins/tree/main/plugins/datadog-entity-sync-backend#installation +iconUrl: https://imgix.datadoghq.com/img/about/presskit/logo-v/dd_vertical_purple.png?auto=format&fit=max&w=847&dpr=2 +npmPackageName: '@datadog/backstage-plugin-datadog-entity-sync-backend' +addedDate: '2025-09-19' From f3c9a474004b78da6028517e707c00789f575bf7 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 7 Oct 2025 07:27:01 +0100 Subject: [PATCH 176/193] Small fixes Signed-off-by: Charles de Dreuille --- .github/vale/config/vocabularies/Backstage/accept.txt | 1 + docs-ui/src/app/theming/page.mdx | 2 +- docs/conf/user-interface/index.md | 2 +- docs/dls/component-design-guidelines.md | 4 ---- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index b0225be547..1497386418 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -280,6 +280,7 @@ modularization monorepo Monorepo monorepos +monospace morgan msgraph msw diff --git a/docs-ui/src/app/theming/page.mdx b/docs-ui/src/app/theming/page.mdx index 73275020bc..92b5256791 100644 --- a/docs-ui/src/app/theming/page.mdx +++ b/docs-ui/src/app/theming/page.mdx @@ -226,7 +226,7 @@ color of your app. ### Foreground colors -Foreground colours are meant to work in pair with a background colours. Typeically this would work +Foreground colours are meant to work in pair with a background colours. Typically this would work for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors are prefixed with `fg` to make it easier to identify. diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index aeae086b59..81acfdf720 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -178,7 +178,7 @@ These colors are used for the background of your application. We are mostly usin #### Foreground colors -Foreground colours are meant to work in pair with a background colours. Typeically this would work for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors are prefixed with `fg` to make it easier to identify. +Foreground colours are meant to work in pair with a background colours. Typically this would work for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors are prefixed with `fg` to make it easier to identify. | Token Name | Description | | ------------------------ | ----------------------------------------------------------------- | diff --git a/docs/dls/component-design-guidelines.md b/docs/dls/component-design-guidelines.md index 17e9d1f11a..fddffd9fd9 100644 --- a/docs/dls/component-design-guidelines.md +++ b/docs/dls/component-design-guidelines.md @@ -104,7 +104,3 @@ accessibility. [8]: https://v4.mui.com/customization/palette/#default-values [9]: https://v4.mui.com/customization/typography/ [10]: https://backstage.io/docs/conf/user-interface - - - -[12]: https://v4.mui.com/customization/default-theme/#explore From eb9b17d45ee642db519f55d3c68d0d3005d54859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 7 Oct 2025 09:48:51 +0200 Subject: [PATCH 177/193] Revert "fix: update stories to comply with react-aria" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 67cf9829946b301952ec74a467578b71364e285b. Signed-off-by: Sofia Sjöblad --- .../HeaderPage/HeaderPage.stories.tsx | 32 ++++------------- .../ui/src/components/Tabs/Tabs.stories.tsx | 36 +++---------------- 2 files changed, 12 insertions(+), 56 deletions(-) diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index bba6e8a857..9e6e63ddf5 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -44,27 +44,22 @@ const tabs: HeaderTab[] = [ { id: 'overview', label: 'Overview', - href: '/overview', }, { id: 'checks', label: 'Checks', - href: '/checks', }, { id: 'tracks', label: 'Tracks', - href: '/tracks', }, { id: 'campaigns', label: 'Campaigns', - href: '/campaigns', }, { id: 'integrations', label: 'Integrations', - href: '/integrations', }, ]; @@ -94,19 +89,10 @@ const withRouter = (Story: StoryFn) => ( ); -// White background decorator for stories -const withWhiteBackground = (Story: StoryFn) => ( -
- -
-); - // Extract layout decorator as a reusable constant const layoutDecorator = [ (Story: StoryFn) => ( -
+ <>
-
+ ), ]; @@ -143,7 +129,6 @@ export const Default: Story = { args: { title: 'Page Title', }, - decorators: [withWhiteBackground], }; export const WithTabs: Story = { @@ -151,11 +136,11 @@ export const WithTabs: Story = { ...Default.args, tabs, }, - decorators: [withRouter, withWhiteBackground], + decorators: [withRouter], }; export const WithCustomActions: Story = { - decorators: [withRouter, withWhiteBackground], + decorators: [withRouter], render: () => ( ( ( ); -// White background decorator for stories -const withWhiteBackground = (Story: StoryFn) => ( -
- -
-); - export const Default: Story = { args: { children: '', }, - decorators: [withRouter, withWhiteBackground], + decorators: [withRouter], render: () => ( - - Tab 1 - - - Tab 2 - - - Tab 3 With long title - + Tab 1 + Tab 2 + Tab 3 With long title ), @@ -69,7 +54,7 @@ export const WithTabPanels: Story = { args: { children: '', }, - decorators: [withRouter, withWhiteBackground], + decorators: [withRouter], render: () => ( @@ -94,7 +79,6 @@ export const WithMockedURLTab2: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -127,7 +111,6 @@ export const WithMockedURLTab3: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -160,7 +143,6 @@ export const WithMockedURLNoMatch: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -199,7 +181,6 @@ export const ExactMatchingDefault: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -236,7 +217,6 @@ export const PrefixMatchingForNestedRoutes: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -277,7 +257,6 @@ export const PrefixMatchingDeepNesting: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -313,7 +292,6 @@ export const MixedMatchingStrategies: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -365,7 +343,6 @@ export const PrefixMatchingEdgeCases: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -408,7 +385,6 @@ export const PrefixMatchingWithSlash: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -450,7 +426,6 @@ export const RootPathMatching: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => ( @@ -485,7 +460,6 @@ export const AutoSelectionOfTabs: Story = { args: { children: '', }, - decorators: [withWhiteBackground], render: () => (
From ea3970a51d5a4e4c7c802baf8b1a22d41a8365b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 7 Oct 2025 10:35:01 +0200 Subject: [PATCH 178/193] add mandatory href for tabs in header component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- packages/ui/src/components/Header/types.ts | 2 +- packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Header/types.ts b/packages/ui/src/components/Header/types.ts index 41bc437a18..c7c602abfc 100644 --- a/packages/ui/src/components/Header/types.ts +++ b/packages/ui/src/components/Header/types.ts @@ -39,7 +39,7 @@ export interface HeaderProps { export interface HeaderTab { id: string; label: string; - href?: string; + href: string; /** * Strategy for matching the current route to determine if this tab should be active. * - 'exact': Tab href must exactly match the current pathname (default) diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index c4849ddc20..385ff0b10d 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -44,22 +44,27 @@ const tabs: HeaderTab[] = [ { id: 'overview', label: 'Overview', + href: '/overview', }, { id: 'checks', label: 'Checks', + href: '/checks', }, { id: 'tracks', label: 'Tracks', + href: '/tracks', }, { id: 'campaigns', label: 'Campaigns', + href: '/campaigns', }, { id: 'integrations', label: 'Integrations', + href: '/integrations', }, ]; From 3c921c551810059dcea2809ae1dc78da72ccfc7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 7 Oct 2025 10:36:21 +0200 Subject: [PATCH 179/193] add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .changeset/few-weeks-create.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/few-weeks-create.md diff --git a/.changeset/few-weeks-create.md b/.changeset/few-weeks-create.md new file mode 100644 index 0000000000..c9de7cb4be --- /dev/null +++ b/.changeset/few-weeks-create.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Making href mandatory in tabs that are part of a Header component From 12c929164ca0c9d6004454b56fdec9b58094a602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 7 Oct 2025 11:12:17 +0200 Subject: [PATCH 180/193] add api report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- packages/ui/report.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 5d04bf8626..f546761837 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1038,7 +1038,7 @@ export interface HeaderProps { // @public export interface HeaderTab { // (undocumented) - href?: string; + href: string; // (undocumented) id: string; // (undocumented) From 7b41d9dd31e831c00f6c08f50090b9c1acdd94ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 7 Oct 2025 15:23:00 +0200 Subject: [PATCH 181/193] Remove app-root-element:signals/signals-display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/forty-crabs-travel.md | 5 +++++ plugins/signals/report-alpha.api.md | 12 ------------ plugins/signals/src/alpha.tsx | 12 +----------- 3 files changed, 6 insertions(+), 23 deletions(-) create mode 100644 .changeset/forty-crabs-travel.md diff --git a/.changeset/forty-crabs-travel.md b/.changeset/forty-crabs-travel.md new file mode 100644 index 0000000000..83e281aa51 --- /dev/null +++ b/.changeset/forty-crabs-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-signals': patch +--- + +Remove `app-root-element:signals/signals-display` which was not doing anything useful diff --git a/plugins/signals/report-alpha.api.md b/plugins/signals/report-alpha.api.md index cd2f384e10..91bd2b6726 100644 --- a/plugins/signals/report-alpha.api.md +++ b/plugins/signals/report-alpha.api.md @@ -8,7 +8,6 @@ import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; -import { JSX as JSX_2 } from 'react'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) @@ -31,17 +30,6 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; - 'app-root-element:signals/signals-display': ExtensionDefinition<{ - kind: 'app-root-element'; - name: 'signals-display'; - config: {}; - configInput: {}; - output: ExtensionDataRef; - inputs: {}; - params: { - element: JSX.Element; - }; - }>; } >; export default _default; diff --git a/plugins/signals/src/alpha.tsx b/plugins/signals/src/alpha.tsx index 5f9cd6f91b..c32bff140e 100644 --- a/plugins/signals/src/alpha.tsx +++ b/plugins/signals/src/alpha.tsx @@ -16,15 +16,12 @@ import { ApiBlueprint, - AppRootElementBlueprint, createFrontendPlugin, discoveryApiRef, identityApiRef, } from '@backstage/frontend-plugin-api'; import { signalApiRef } from '@backstage/plugin-signals-react'; import { SignalClient } from './api/SignalClient'; -import { SignalsDisplay } from './plugin'; -import { compatWrapper } from '@backstage/core-compat-api'; const api = ApiBlueprint.make({ params: defineParams => @@ -43,16 +40,9 @@ const api = ApiBlueprint.make({ }), }); -const signalsDisplayAppRootElement = AppRootElementBlueprint.make({ - name: 'signals-display', - params: { - element: compatWrapper(), - }, -}); - /** @alpha */ export default createFrontendPlugin({ pluginId: 'signals', info: { packageJson: () => import('../package.json') }, - extensions: [api, signalsDisplayAppRootElement], + extensions: [api], }); From b8cf31a93a21fba498a48e29ffcde17e1265b4f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:57:14 +0000 Subject: [PATCH 182/193] chore(deps): bump nodemailer from 6.9.16 to 7.0.7 Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 6.9.16 to 7.0.7. - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.16...v7.0.7) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 7.0.7 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .changeset/dependabot-eaf5987.md | 5 +++++ .../notifications-backend-module-email/package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/dependabot-eaf5987.md diff --git a/.changeset/dependabot-eaf5987.md b/.changeset/dependabot-eaf5987.md new file mode 100644 index 0000000000..bf1475613a --- /dev/null +++ b/.changeset/dependabot-eaf5987.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7 diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 7c1e279c6c..9f16340057 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -48,7 +48,7 @@ "@backstage/plugin-notifications-node": "workspace:^", "@backstage/types": "workspace:^", "lodash": "^4.17.21", - "nodemailer": "^6.9.13", + "nodemailer": "^7.0.7", "p-throttle": "^4.1.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 8e09ebae47..73647d22b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5723,7 +5723,7 @@ __metadata: "@backstage/types": "workspace:^" "@types/nodemailer": "npm:^6.4.14" lodash: "npm:^4.17.21" - nodemailer: "npm:^6.9.13" + nodemailer: "npm:^7.0.7" p-throttle: "npm:^4.1.1" languageName: unknown linkType: soft @@ -38906,10 +38906,10 @@ __metadata: languageName: node linkType: hard -"nodemailer@npm:^6.9.13": - version: 6.9.16 - resolution: "nodemailer@npm:6.9.16" - checksum: 10/f131888d3111238fde4ee03539e62f1764b99365ff31d556dde0367dfefcee1f2eb8948558f35ba84fe5cd805f2d01294eee63a5675d3aa501e7df548a2518ce +"nodemailer@npm:^7.0.7": + version: 7.0.9 + resolution: "nodemailer@npm:7.0.9" + checksum: 10/88883c58afe356d2b4c24b1e976c04857e8a7a7145e1752dab69072900b0cc2e3daa0964a08c653e692fb64382453e1cfcee3d863828844c8d6f6239727b9023 languageName: node linkType: hard From da97dad6253f6723fde6974799ef68ed8252f258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Tue, 7 Oct 2025 16:20:49 +0200 Subject: [PATCH 183/193] Revert "Merge pull request #31230 from backstage/bui-fix-tooltip" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 60c5db13568fe1acbb2a7c64298d11e6a1d98647, reversing changes made to 47f5d67bea83b3179f2540ad18a06cb44fabc5b9. Signed-off-by: Sofia Sjöblad --- .changeset/moody-singers-deny.md | 5 ----- .../ui/src/components/Button/Button.stories.tsx | 10 ---------- packages/ui/src/components/Button/Button.tsx | 16 ++-------------- 3 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 .changeset/moody-singers-deny.md diff --git a/.changeset/moody-singers-deny.md b/.changeset/moody-singers-deny.md deleted file mode 100644 index 28072baf4b..0000000000 --- a/.changeset/moody-singers-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Enable tooltips on disabled buttons with automatic wrapper diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 1604c04eb4..db8e999f98 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -19,7 +19,6 @@ import { Button } from './Button'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { Icon } from '../Icon'; -import { Tooltip, TooltipTrigger } from '../Tooltip'; const meta = { title: 'Backstage UI/Button', @@ -217,12 +216,3 @@ export const Playground: Story = { ), }; - -export const DisabledWithTooltips: Story = { - render: () => ( - - - Why this is disabled - - ), -}; diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index 9f658fd00a..40879e51ed 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -16,7 +16,7 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; -import { Button as RAButton, Focusable } from 'react-aria-components'; +import { Button as RAButton } from 'react-aria-components'; import type { ButtonProps } from './types'; import { useStyles } from '../../hooks/useStyles'; @@ -30,7 +30,6 @@ export const Button = forwardRef( iconEnd, children, className, - isDisabled, ...rest } = props; @@ -39,29 +38,18 @@ export const Button = forwardRef( variant, }); - const btn = ( + return ( {iconStart} {children} {iconEnd} ); - - return isDisabled ? ( - - - {btn} - - - ) : ( - btn - ); }, ); From 67fc6fe48ce0f692acb0cf5db808d6682846da9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 7 Oct 2025 16:40:11 +0200 Subject: [PATCH 184/193] Update .changeset/full-chefs-roll.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/full-chefs-roll.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/full-chefs-roll.md b/.changeset/full-chefs-roll.md index 2eaa8b8026..0a13f116ec 100644 --- a/.changeset/full-chefs-roll.md +++ b/.changeset/full-chefs-roll.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incomatible with Jest 30. +Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30. From 316d0774b854d1a617937d9d03a938bd2a5c6ae6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Oct 2025 15:00:59 +0000 Subject: [PATCH 185/193] Version Packages (next) --- .changeset/create-app-1759849206.md | 5 + .changeset/pre.json | 21 +- docs/releases/v1.44.0-next.3-changelog.md | 219 ++++++++++++++++++ package.json | 2 +- packages/backend-app-api/CHANGELOG.md | 6 + packages/backend-app-api/package.json | 2 +- packages/cli/CHANGELOG.md | 13 ++ packages/cli/package.json | 2 +- packages/core-components/CHANGELOG.md | 6 + packages/core-components/package.json | 2 +- packages/create-app/CHANGELOG.md | 6 + packages/create-app/package.json | 2 +- packages/frontend-plugin-api/CHANGELOG.md | 8 + packages/frontend-plugin-api/package.json | 2 +- packages/ui/CHANGELOG.md | 9 + packages/ui/package.json | 2 +- plugins/app-visualizer/CHANGELOG.md | 9 + plugins/app-visualizer/package.json | 2 +- .../catalog-backend-module-aws/CHANGELOG.md | 6 + .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/CHANGELOG.md | 6 + .../catalog-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- plugins/catalog-graph/CHANGELOG.md | 9 + plugins/catalog-graph/package.json | 2 +- plugins/home/CHANGELOG.md | 9 + plugins/home/package.json | 2 +- plugins/kubernetes-cluster/CHANGELOG.md | 8 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 9 + plugins/kubernetes/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 6 + plugins/notifications-backend/package.json | 2 +- plugins/notifications/CHANGELOG.md | 9 + plugins/notifications/package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 9 + plugins/scaffolder/package.json | 2 +- plugins/signals/CHANGELOG.md | 9 + plugins/signals/package.json | 2 +- 62 files changed, 489 insertions(+), 31 deletions(-) create mode 100644 .changeset/create-app-1759849206.md create mode 100644 docs/releases/v1.44.0-next.3-changelog.md diff --git a/.changeset/create-app-1759849206.md b/.changeset/create-app-1759849206.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1759849206.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 486fc27ac5..458c97896e 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -218,12 +218,20 @@ "create-app-1758639549", "create-app-1758718573", "create-app-1759243273", + "create-app-1759849206", + "cuddly-mugs-act", "curvy-bobcats-melt", + "dependabot-eaf5987", "eager-toes-start", "famous-loops-tickle", "fast-heads-brake", "fast-queens-guess", + "few-weeks-create", "five-olives-bet", + "flat-peas-run", + "forty-crabs-travel", + "full-chefs-roll", + "fuzzy-trams-kick", "giant-weeks-jump", "heavy-cooks-divide", "hungry-crews-fetch", @@ -232,6 +240,7 @@ "legal-eagles-jog", "modern-pugs-appear", "moody-singers-deny", + "nasty-moose-rescue", "nice-readers-judge", "public-sites-admire", "public-wombats-say", @@ -240,18 +249,28 @@ "ready-pots-arrive", "red-dodos-work", "red-times-bet", + "sad-women-rule", "salty-words-wash", + "shiny-candles-hide", "short-aliens-invite", + "silent-mice-play", "six-cooks-battle", "slimy-signs-agree", "solid-bikes-leave", "tame-hairs-smash", "tender-cups-tap", + "thin-hoops-bathe", "thirty-rules-press", + "tidy-coats-know", "tired-mice-cheer", + "tough-clocks-attack", "twelve-guests-sit", + "twelve-oranges-grin", + "two-emus-like", "unified-theme-attr-stack", + "warm-items-look", "wet-spiders-wait", - "wide-flies-jog" + "wide-flies-jog", + "yarn-plugin-integration" ] } diff --git a/docs/releases/v1.44.0-next.3-changelog.md b/docs/releases/v1.44.0-next.3-changelog.md new file mode 100644 index 0000000000..8a65d6d6a6 --- /dev/null +++ b/docs/releases/v1.44.0-next.3-changelog.md @@ -0,0 +1,219 @@ +# Release v1.44.0-next.3 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.44.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.44.0-next.3) + +## @backstage/backend-app-api@1.2.8-next.1 + +### Patch Changes + +- dd69cf6: Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process. + +## @backstage/cli@0.34.4-next.3 + +### Patch Changes + +- f2cf564: Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30. + +- 024645e: Remove unused @octokit modules from cli package + + - @octokit/graphql + - @octokit/graphql-schema + - @octokit/oauth-app + +- d14ef24: Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies. + +## @backstage/core-components@0.18.2-next.3 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. + +## @backstage/create-app@0.7.5-next.3 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/frontend-plugin-api@0.12.1-next.2 + +### Patch Changes + +- 8ed53eb: Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too. +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + +## @backstage/ui@0.7.2-next.2 + +### Patch Changes + +- 3c921c5: Making href mandatory in tabs that are part of a Header component +- 5c21e45: Add react router for internal routing for ButtonLinks +- 9781815: Remove auto selection of tabs for tabs that all have href defined +- f6dff5b: Using react router for internal links in the Menu component + +## @backstage/plugin-app-visualizer@0.1.24-next.2 + +### Patch Changes + +- 4406144: Ensure that the text rendering has react keys for all elements +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-catalog-backend-module-aws@0.4.16-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-azure@0.3.10-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.4-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.4-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.7-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-github@0.11.1-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-github-org@0.3.15-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.11.1-next.2 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.4-next.2 + +### Patch Changes + +- 0443119: Fixed an issue in `GitlabDiscoveryEntityProvider` where entity fetching could fail for projects with special characters or that had been renamed or moved. +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.14-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.7.4-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.1-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.15-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-graph@0.5.2-next.2 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-home@0.8.13-next.3 + +### Patch Changes + +- e7d59d3: fix(home): correct `clearAll` logic to properly handle `deletable` flag +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-kubernetes@0.12.12-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-kubernetes-cluster@0.0.30-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + +## @backstage/plugin-notifications@0.5.10-next.3 + +### Patch Changes + +- f5e0963: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-notifications-backend@0.5.11-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + +## @backstage/plugin-notifications-backend-module-email@0.3.14-next.1 + +### Patch Changes + +- b8cf31a: chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7 +- f5e0963: Removed unused dependencies + +## @backstage/plugin-scaffolder@1.34.2-next.3 + +### Patch Changes + +- d9aed74: Forward `ui:disabled` in `OwnedEntityPicker` to allow disabling it +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.14-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.9.6-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + +## @backstage/plugin-signals@0.0.24-next.3 + +### Patch Changes + +- 7b41d9d: Remove `app-root-element:signals/signals-display` which was not doing anything useful +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 diff --git a/package.json b/package.json index a22ab1666a..5ef45ce170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.44.0-next.2", + "version": "1.44.0-next.3", "backstage": { "cli": { "new": { diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index a0176a008f..e36a8a781b 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/backend-app-api +## 1.2.8-next.1 + +### Patch Changes + +- dd69cf6: Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process. + ## 1.2.8-next.0 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index b082de0cfa..1613e05cc0 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "1.2.8-next.0", + "version": "1.2.8-next.1", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7382ef147b..900401a8fa 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/cli +## 0.34.4-next.3 + +### Patch Changes + +- f2cf564: Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30. +- 024645e: Remove unused @octokit modules from cli package + + - @octokit/graphql + - @octokit/graphql-schema + - @octokit/oauth-app + +- d14ef24: Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies. + ## 0.34.4-next.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 5b62c9a02a..19d9428979 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.4-next.2", + "version": "0.34.4-next.3", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 807d844712..90b0695ab9 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core-components +## 0.18.2-next.3 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. + ## 0.18.2-next.2 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 3418bbe033..fdde2c7e79 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.18.2-next.2", + "version": "0.18.2-next.3", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index baa76026b6..dfff2af4f3 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/create-app +## 0.7.5-next.3 + +### Patch Changes + +- Bumped create-app version. + ## 0.7.5-next.2 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index eac44e445a..9fa8af4c1b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.5-next.2", + "version": "0.7.5-next.3", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index e12aedb385..9f6a380b07 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/frontend-plugin-api +## 0.12.1-next.2 + +### Patch Changes + +- 8ed53eb: Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too. +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + ## 0.12.1-next.1 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 15e557101c..0dd0748f97 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.12.1-next.1", + "version": "0.12.1-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 2efdbb2b9e..95314bf768 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/ui +## 0.7.2-next.2 + +### Patch Changes + +- 3c921c5: Making href mandatory in tabs that are part of a Header component +- 5c21e45: Add react router for internal routing for ButtonLinks +- 9781815: Remove auto selection of tabs for tabs that all have href defined +- f6dff5b: Using react router for internal links in the Menu component + ## 0.7.2-next.1 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 448905ab9d..fd6ecd169c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.7.2-next.1", + "version": "0.7.2-next.2", "backstage": { "role": "web-library" }, diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index ea0b147aff..cdd5db4986 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-app-visualizer +## 0.1.24-next.2 + +### Patch Changes + +- 4406144: Ensure that the text rendering has react keys for all elements +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.1.24-next.1 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index fa30acefe2..f55c085ed0 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.24-next.1", + "version": "0.1.24-next.2", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index a77ccf785c..2ee7746b87 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.4.16-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.4.16-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 39b199d03d..eef358548a 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.4.16-next.1", + "version": "0.4.16-next.2", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 8f04f5cae5..d170204031 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.3.10-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.3.10-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index a390224e50..2b2ef5fe35 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.3.10-next.1", + "version": "0.3.10-next.2", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index bce5bc231b..f9ac3b6966 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.5.4-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.5.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index e10df5a382..e77dfa9ee0 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.5.4-next.1", + "version": "0.5.4-next.2", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index cb7767ce16..fe2cae0334 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.5.4-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.5.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 21d6ece100..c9021d42c1 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.5.4-next.1", + "version": "0.5.4-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index c1f9fe3867..0235f98580 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.3.7-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.3.7-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 7427ed0906..e345190ece 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.3.7-next.1", + "version": "0.3.7-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index df63d6fb8f..61b5aa19dd 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.3.15-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.11.1-next.2 + ## 0.3.15-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 0329bebbf6..93bc6517e0 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.3.15-next.1", + "version": "0.3.15-next.2", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index 2921c3960d..da546ffe8e 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-github +## 0.11.1-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.11.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index c91bb5e96a..32c8a14614 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.11.1-next.1", + "version": "0.11.1-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index b162bf1103..b6e85af5fd 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.2.14-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.7.4-next.2 + ## 0.2.14-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 80c1ba769c..9be7de1635 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.2.14-next.1", + "version": "0.2.14-next.2", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 18a1915ee6..91dbf57f9e 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.7.4-next.2 + +### Patch Changes + +- 0443119: Fixed an issue in `GitlabDiscoveryEntityProvider` where entity fetching could fail for projects with special characters or that had been renamed or moved. +- 99fcf98: Removed unused dependencies + ## 0.7.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 0c20cdd30a..1de716499e 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.7.4-next.1", + "version": "0.7.4-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 3db73151fe..9bd2ff31d0 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.8.1-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.8.1-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 35d38e12d4..bcf0ae17a3 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.8.1-next.0", + "version": "0.8.1-next.1", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index cdbee9bfb8..7645b8d390 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.2.15-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.2.15-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index d76b965b90..4a01305a1a 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.2.15-next.0", + "version": "0.2.15-next.1", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index b492c41a37..41c3c1c519 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-graph +## 0.5.2-next.2 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.5.2-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index f455dbce31..7531a926a4 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.5.2-next.1", + "version": "0.5.2-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 68ef6448a5..85e2a009f4 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-home +## 0.8.13-next.3 + +### Patch Changes + +- e7d59d3: fix(home): correct `clearAll` logic to properly handle `deletable` flag +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.8.13-next.2 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 910d6c036f..060f090974 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.13-next.2", + "version": "0.8.13-next.3", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 527c13c88d..be3fed6035 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.30-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + ## 0.0.30-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index ea1bb80570..033e1138f4 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.30-next.1", + "version": "0.0.30-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 62ffb354bf..670ff085f6 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes +## 0.12.12-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.12.12-next.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 2fe8506310..113deee19f 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.12-next.1", + "version": "0.12.12-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 99f2277dad..cf891bebc1 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-notifications-backend-module-email +## 0.3.14-next.1 + +### Patch Changes + +- b8cf31a: chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7 +- f5e0963: Removed unused dependencies + ## 0.3.14-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 7f5711d27d..94a3635ef1 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.3.14-next.0", + "version": "0.3.14-next.1", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 713066e692..d99785fd12 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-notifications-backend +## 0.5.11-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + ## 0.5.11-next.1 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index e03f07c1c9..0f270b1e37 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.5.11-next.1", + "version": "0.5.11-next.2", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index caedf21e35..b5a6da6ba1 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-notifications +## 0.5.10-next.3 + +### Patch Changes + +- f5e0963: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.5.10-next.2 ### Patch Changes diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 9d7e36c1ac..68996bc7c0 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.5.10-next.2", + "version": "0.5.10-next.3", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 9df81bc074..055f52a047 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.2.14-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + ## 0.2.14-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 53564814d1..f53b075403 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.2.14-next.1", + "version": "0.2.14-next.2", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index c1312cc2a6..c0ea9e94dd 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.9.6-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + ## 0.9.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index c9644543a5..d34526b65b 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.9.6-next.1", + "version": "0.9.6-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 649965149d..8abe46b3c7 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder +## 1.34.2-next.3 + +### Patch Changes + +- d9aed74: Forward `ui:disabled` in `OwnedEntityPicker` to allow disabling it +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 1.34.2-next.2 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 21e57a97c8..4a64a18ca4 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.34.2-next.2", + "version": "1.34.2-next.3", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index 1e41067d82..02e57ebeca 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-signals +## 0.0.24-next.3 + +### Patch Changes + +- 7b41d9d: Remove `app-root-element:signals/signals-display` which was not doing anything useful +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.0.24-next.2 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 13bf863b60..d67709a5ca 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.24-next.2", + "version": "0.0.24-next.3", "backstage": { "role": "frontend-plugin", "pluginId": "signals", From 74dcd32da46125c1c18d33ad8137a2e0ee784072 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 7 Oct 2025 17:45:12 +0100 Subject: [PATCH 186/193] Update sidebars.ts Signed-off-by: Charles de Dreuille --- microsite/sidebars.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index b7e89825ca..2d5065400b 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -396,17 +396,7 @@ export default { 'conf/index', 'conf/reading', 'conf/writing', - 'conf/defining', - { - type: 'category', - label: 'User Interface', - items: [ - 'conf/user-interface/index', - 'conf/user-interface/logo', - 'conf/user-interface/icons', - 'conf/user-interface/sidebar', - ], - }, + 'conf/defining' ], Framework: [ { @@ -551,6 +541,16 @@ export default { 'tooling/package-metadata', ], }, + { + type: 'category', + label: 'User Interface', + items: [ + 'conf/user-interface/index', + 'conf/user-interface/logo', + 'conf/user-interface/icons', + 'conf/user-interface/sidebar', + ], + }, ], Tutorials: [ { 'Non-technical': ['overview/adopting'] }, From 1ac09a8060abf264d9558ceed17f7cc547779dfb Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 7 Oct 2025 18:01:11 +0100 Subject: [PATCH 187/193] Update docusaurus.config.ts Signed-off-by: Charles de Dreuille --- microsite/docusaurus.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 83f2f75689..206dbba97f 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -272,7 +272,7 @@ const config: Config = { { from: '/docs/getting-started/app-custom-theme', to: '/docs/conf/user-interface', - } + }, ], }), [ From 79d72fa86e3dc00c4af07066ed243ed4bf0a3a16 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 7 Oct 2025 18:15:18 +0100 Subject: [PATCH 188/193] Update sidebars.ts Signed-off-by: Charles de Dreuille --- microsite/sidebars.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 2d5065400b..1f05660d7e 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -396,7 +396,7 @@ export default { 'conf/index', 'conf/reading', 'conf/writing', - 'conf/defining' + 'conf/defining', ], Framework: [ { From 43afbe50aa3c30968b8443dbbf69c637673069c5 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Tue, 7 Oct 2025 19:29:14 +0200 Subject: [PATCH 189/193] feat(techdocs): POC livereload for techdocs-cli serve (#30541) * feat(techdocs): POC livereload for techdocs-cli serve Signed-off-by: Gabriel Dugny * chore: techdocs reload tests, refactor Signed-off-by: Gabriel Dugny --------- Signed-off-by: Gabriel Dugny --- .changeset/brown-symbols-create.md | 5 + .../techdocs-cli-embedded-app/app-config.yaml | 4 + .../techdocs-cli-embedded-app/src/App.tsx | 10 + .../src/LiveReloadAddon.test.tsx | 111 +++++++++++ .../src/LiveReloadAddon.tsx | 121 ++++++++++++ packages/techdocs-cli/src/lib/httpServer.ts | 32 +++- .../techdocs-cli/src/lib/livereload.test.ts | 180 ++++++++++++++++++ packages/techdocs-cli/src/lib/livereload.ts | 175 +++++++++++++++++ 8 files changed, 636 insertions(+), 2 deletions(-) create mode 100644 .changeset/brown-symbols-create.md create mode 100644 packages/techdocs-cli-embedded-app/src/LiveReloadAddon.test.tsx create mode 100644 packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx create mode 100644 packages/techdocs-cli/src/lib/livereload.test.ts create mode 100644 packages/techdocs-cli/src/lib/livereload.ts diff --git a/.changeset/brown-symbols-create.md b/.changeset/brown-symbols-create.md new file mode 100644 index 0000000000..d4a62b1280 --- /dev/null +++ b/.changeset/brown-symbols-create.md @@ -0,0 +1,5 @@ +--- +'@techdocs/cli': minor +--- + +Techdocs CLI serve supports automatic refresh, relying on `mkdocs` `watch` feature. diff --git a/packages/techdocs-cli-embedded-app/app-config.yaml b/packages/techdocs-cli-embedded-app/app-config.yaml index 6ede05b587..c8dfa1683c 100644 --- a/packages/techdocs-cli-embedded-app/app-config.yaml +++ b/packages/techdocs-cli-embedded-app/app-config.yaml @@ -7,3 +7,7 @@ backend: techdocs: builder: 'external' + sanitizer: + # Allow live reload locally. Added in this config to avoid updating the main techdocs plugin. + allowedCustomElementTagNameRegExp: '^live-reload$' + allowedCustomElementAttributeNameRegExp: '^live-reload-(epoch|request-id)$' diff --git a/packages/techdocs-cli-embedded-app/src/App.tsx b/packages/techdocs-cli-embedded-app/src/App.tsx index 273a828b4a..13d5b925f0 100644 --- a/packages/techdocs-cli-embedded-app/src/App.tsx +++ b/packages/techdocs-cli-embedded-app/src/App.tsx @@ -36,6 +36,7 @@ import * as plugins from './plugins'; import { configLoader } from './config'; import { Root } from './components/Root'; import { techDocsPage, TechDocsThemeToggle } from './components/TechDocsPage'; +import { TechDocsLiveReload } from './LiveReloadAddon'; const app = createApp({ apis, @@ -54,6 +55,14 @@ const ThemeToggleAddon = techdocsPlugin.provide( }), ); +const LiveReloadAddon = techdocsPlugin.provide( + createTechDocsAddonExtension({ + name: 'LiveReloadAddon', + component: TechDocsLiveReload, + location: TechDocsAddonLocations.Content, + }), +); + const routes = ( @@ -71,6 +80,7 @@ const routes = ( > {techDocsPage} + diff --git a/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.test.tsx b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.test.tsx new file mode 100644 index 0000000000..1a80def9e9 --- /dev/null +++ b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.test.tsx @@ -0,0 +1,111 @@ +/* + * 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 { render } from '@testing-library/react'; +import { TechDocsLiveReload } from './LiveReloadAddon'; + +jest.mock('@backstage/plugin-techdocs-react', () => ({ + useShadowRootElements: jest.fn(() => [ + { + querySelector: jest.fn((selector: string) => { + if (selector === 'live-reload') { + return { + getAttribute: (name: string) => { + if (name === 'live-reload-epoch') return '10'; + if (name === 'live-reload-request-id') return '1'; + return null; + }, + }; + } + return null; + }), + }, + ]), +})); + +describe('TechDocsLiveReload', () => { + const originalXHR = global.XMLHttpRequest; + let originalLocation: Location; + let openSpy: jest.Mock; + let sendSpy: jest.Mock; + + beforeEach(() => { + originalLocation = window.location; + openSpy = jest.fn(); + sendSpy = jest.fn(function (this: any) { + // simulate long-poll response that does NOT trigger reload (epoch unchanged) + setTimeout(() => { + (this as any).status = 200; + (this as any).responseText = '10'; + (this as any).onloadend?.call(this); + }, 0); + }); + + class MockXHR { + onloadend: ((this: any) => void) | null = null; + status = 0; + responseText = ''; + open = openSpy; + send = sendSpy as any; + abort = jest.fn(); + } + + global.XMLHttpRequest = MockXHR as any; + + // Replace window.location with a mutable object for tests + delete (window as any).location; + (window as any).location = { ...originalLocation, reload: jest.fn() }; + jest.spyOn(window, 'addEventListener').mockImplementation(() => {}); + jest.spyOn(window, 'removeEventListener').mockImplementation(() => {}); + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + configurable: true, + }); + }); + + afterEach(() => { + global.XMLHttpRequest = originalXHR; + jest.restoreAllMocks(); + // restore original window.location + delete (window as any).location; + (window as any).location = originalLocation; + }); + + it('polls livereload endpoint and does not reload when epoch unchanged', async () => { + const reloadSpy = window.location.reload as unknown as jest.Mock; + render(); + expect(openSpy).toHaveBeenCalledWith('GET', '/.livereload/10/1'); + // give microtask queue a tick + await new Promise(res => setTimeout(res, 0)); + expect(reloadSpy).not.toHaveBeenCalled(); + }); + + it('reloads when server epoch increases', async () => { + const reloadSpy = window.location.reload as unknown as jest.Mock; + + sendSpy.mockImplementation(function (this: any) { + setTimeout(() => { + (this as any).status = 200; + (this as any).responseText = '11'; + (this as any).onloadend?.call(this); + }, 0); + }); + + render(); + await new Promise(res => setTimeout(res, 0)); + expect(reloadSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx new file mode 100644 index 0000000000..905a339c72 --- /dev/null +++ b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx @@ -0,0 +1,121 @@ +/* + * 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 { useShadowRootElements } from '@backstage/plugin-techdocs-react'; +import { useEffect, useRef } from 'react'; + +interface TechDocsLiveReloadProps { + /** Whether to enable livereload (default: true in development) */ + enabled?: boolean; +} + +/** + * LiveReload addon for Techdocs CLI. + * + * Support mkdocs built-in livereload, in a TechDocs CLI preview environment. + * See https://github.com/backstage/backstage/issues/30514 for more details. + */ +export const TechDocsLiveReload = ({ + enabled = true, +}: TechDocsLiveReloadProps) => { + const body = useShadowRootElements(['body']); + const reqRef = useRef(null); + const timeoutRef = useRef(null); + const LIVE_RELOAD_ELEMENT = 'live-reload'; + const LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch'; + const LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id'; + const CLI_LIVERELOAD_PATH = '/.livereload'; + + useEffect(() => { + if (!enabled || !body[0]) { + return undefined; + } + + const liveReloadElement = body[0].querySelector(LIVE_RELOAD_ELEMENT); + + if (!liveReloadElement) { + return undefined; + } + + const epoch = parseInt( + liveReloadElement.getAttribute(LIVE_RELOAD_ATTR_EPOCH) || '0', + 10, + ); + const requestId = parseInt( + liveReloadElement.getAttribute(LIVE_RELOAD_ATTR_REQUEST_ID) || '0', + 10, + ); + + if (!epoch || !requestId) { + return undefined; + } + + const livereloadUrl = CLI_LIVERELOAD_PATH; + + const poll = () => { + reqRef.current = new XMLHttpRequest(); + reqRef.current.onloadend = function handleLoadEnd(this: XMLHttpRequest) { + if (parseFloat(this.responseText) > epoch) { + window.location.reload(); + } else { + timeoutRef.current = setTimeout(poll, this.status === 200 ? 0 : 3000); + } + }; + reqRef.current.open('GET', `${livereloadUrl}/${epoch}/${requestId}`); + reqRef.current.send(); + }; + + const stop = () => { + if (reqRef.current) { + reqRef.current.abort(); + reqRef.current = null; + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + + // Stop when tab is inactive + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + poll(); + } else { + stop(); + } + }; + + const handleBeforeUnload = () => { + stop(); + }; + + // Start polling if page is visible + if (document.visibilityState === 'visible') { + poll(); + } + + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + stop(); + document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [body, enabled]); + + return null; +}; diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts index c38bc86d7a..d7e8f62ea0 100644 --- a/packages/techdocs-cli/src/lib/httpServer.ts +++ b/packages/techdocs-cli/src/lib/httpServer.ts @@ -18,6 +18,10 @@ import serveHandler from 'serve-handler'; import http from 'http'; import httpProxy from 'http-proxy'; import { createLogger } from './utility'; +import { + proxyHtmlWithLivereloadInjection, + proxyMkdocsLivereload, +} from './livereload'; export default class HTTPServer { private readonly proxyEndpoint: string; @@ -59,8 +63,8 @@ export default class HTTPServer { const proxyHandler = this.createProxy(); const server = http.createServer( (request: http.IncomingMessage, response: http.ServerResponse) => { - // This endpoind is used by the frontend to issue a cookie for the user. - // But the MkDocs server doesn't expose it as a the Backestage backend does. + // This endpoint is used by the frontend to issue a cookie for the user. + // But the MkDocs server doesn't expose it as a the Backstage backend does. // So we need to fake it here to prevent 404 errors. if (request.url === '/api/techdocs/.backstage/auth/v1/cookie') { const oneHourInMilliseconds = 60 * 60 * 1000; @@ -72,6 +76,19 @@ export default class HTTPServer { } if (request.url?.startsWith(this.proxyEndpoint)) { + // Handle HTML files with livereload parameter injection + if (request.url?.endsWith('.html')) { + proxyHtmlWithLivereloadInjection({ + request, + response, + mkdocsTargetAddress: this.mkdocsTargetAddress, + proxyEndpoint: this.proxyEndpoint, + onError: (error: Error) => reject(error), + }); + return; + } + + // Handle non-HTML files with regular proxy const [proxy, forwardPath] = proxyHandler(request); proxy.on('error', (error: Error) => { @@ -93,6 +110,17 @@ export default class HTTPServer { return; } + // This endpoint is used by the frontend to pass livereload requests to the mkdocs server. + if (request.url?.startsWith('/.livereload')) { + proxyMkdocsLivereload({ + request, + response, + mkdocsTargetAddress: this.mkdocsTargetAddress, + onError: (error: Error) => reject(error), + }); + return; + } + serveHandler(request, response, { public: this.backstageBundleDir, trailingSlash: true, diff --git a/packages/techdocs-cli/src/lib/livereload.test.ts b/packages/techdocs-cli/src/lib/livereload.test.ts new file mode 100644 index 0000000000..b21251c45a --- /dev/null +++ b/packages/techdocs-cli/src/lib/livereload.test.ts @@ -0,0 +1,180 @@ +/* + * 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 http from 'http'; +import { + injectLivereloadParameters, + proxyHtmlWithLivereloadInjection, + proxyMkdocsLivereload, +} from './livereload'; + +// Note: This mock returns a singleton proxy object so tests can access the +// registered event handlers (e.g. `proxyRes`) from code under test. +jest.mock('http-proxy', () => { + const handlers: Record = {}; + const fakeProxy = { + on: jest.fn((event: string, cb: Function) => { + handlers[event] = cb; + }), + web: jest.fn((_req: unknown, _res: unknown) => { + // no-op; tests will manually trigger handlers['proxyRes'] when needed + }), + __handlers: handlers, + }; + const create = jest.fn(() => fakeProxy); + return { + __esModule: true, + default: { createProxyServer: create }, + createProxyServer: create, + }; +}); + +describe('livereload helpers', () => { + describe('injectLivereloadParameters', () => { + it('injects live-reload element when mkdocs script is present', () => { + const html = + ''; + const result = injectLivereloadParameters(html); + expect(result).toContain(''); + }); + + it('returns original html when mkdocs script is absent', () => { + const html = '

No livereload

'; + const result = injectLivereloadParameters(html); + expect(result).toBe(html); + }); + }); + + describe('proxyHtmlWithLivereloadInjection', () => { + it('injects parameters into HTML responses and sets CORS headers', () => { + const { createProxyServer } = jest.requireMock('http-proxy') as any; + const proxy = createProxyServer(); + + const req = { + url: '/api/techdocs/some/path/index.html', + } as unknown as http.IncomingMessage; + const headers: Record = {}; + const res = { + setHeader: (k: string, v: any) => { + headers[k] = String(v); + }, + end: jest.fn(), + } as unknown as http.ServerResponse; + + proxyHtmlWithLivereloadInjection({ + request: req, + response: res, + mkdocsTargetAddress: 'http://localhost:8000', + proxyEndpoint: '/api/techdocs/', + onError: () => {}, + }); + + // Simulate mkdocs proxy response with HTML + const proxyRes: any = { + headers: { 'content-type': 'text/html; charset=utf-8' }, + on: (event: string, cb: Function) => { + if (event === 'data') { + cb(''); + } + if (event === 'end') { + cb(); + } + }, + pipe: jest.fn(), + }; + + (proxy as any).__handlers.proxyRes(proxyRes, {} as any, res); + + expect(res.end).toHaveBeenCalled(); + const injectedHtml = (res.end as jest.Mock).mock.calls[0][0] as string; + expect(injectedHtml).toContain(' { + const { createProxyServer } = jest.requireMock('http-proxy') as any; + const proxy = createProxyServer(); + + const req = { + url: '/api/techdocs/some/path/asset.css', + } as unknown as http.IncomingMessage; + const headers: Record = {}; + const res = { + setHeader: (k: string, v: any) => { + headers[k] = String(v); + }, + end: jest.fn(), + } as unknown as http.ServerResponse; + + proxyHtmlWithLivereloadInjection({ + request: req, + response: res, + mkdocsTargetAddress: 'http://localhost:8000', + proxyEndpoint: '/api/techdocs/', + onError: () => {}, + }); + + const proxyRes: any = { + headers: { 'content-type': 'text/css' }, + on: jest.fn(), + pipe: jest.fn(), + }; + + (proxy as any).__handlers.proxyRes(proxyRes, {} as any, res); + + expect(res.end).not.toHaveBeenCalled(); + expect(proxyRes.pipe).toHaveBeenCalledWith(res); + expect(headers['Access-Control-Allow-Origin']).toBe('*'); + expect(headers['Access-Control-Allow-Methods']).toBe('GET, OPTIONS'); + }); + }); + + describe('proxyMkdocsLivereload', () => { + it('rewrites path and sets CORS headers', () => { + const { createProxyServer } = jest.requireMock('http-proxy') as any; + const proxy = createProxyServer(); + + const req = { + url: '/.livereload/1/2', + } as unknown as http.IncomingMessage; + const headers: Record = {}; + const res = { + setHeader: (k: string, v: any) => { + headers[k] = String(v); + }, + } as unknown as http.ServerResponse; + + proxyMkdocsLivereload({ + request: req, + response: res, + mkdocsTargetAddress: 'http://localhost:8000', + onError: () => {}, + }); + + expect(req.url).toBe('/livereload/1/2'); + expect(headers['Access-Control-Allow-Origin']).toBe('*'); + expect(headers['Access-Control-Allow-Methods']).toBe('GET, OPTIONS'); + expect(headers['Access-Control-Allow-Headers']).toBe('Content-Type'); + expect((proxy as any).web).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/techdocs-cli/src/lib/livereload.ts b/packages/techdocs-cli/src/lib/livereload.ts new file mode 100644 index 0000000000..c108971b82 --- /dev/null +++ b/packages/techdocs-cli/src/lib/livereload.ts @@ -0,0 +1,175 @@ +/* + * 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 http from 'http'; +import httpProxy from 'http-proxy'; + +/** + * Livereload support for techdocs-cli. + * + * Context: + * - MkDocs implements autoreload using a long-poll endpoint `/livereload` and a script that injects + * a call like: `livereload(epoch, request_id)`, where `epoch` is derived from Python's + * `time.monotonic()`. + * - Node.js monotonic clocks (`process.hrtime`/`performance.now`) are not compatible with Python's + * value across processes and platforms. We therefore CANNOT reliably re-create the same epoch on + * the frontend or in this CLI, and must read the values produced by MkDocs itself. + * - The MkDocs script tag is removed by DOM sanitization (DomPurify) in TechDocs, so we can't rely + * on the script being present in the embedded app. To bridge this, we extract the parameters on + * the server side while proxying HTML and inject them as a safe custom element that survives + * sanitization: ``. + * - The frontend addon reads that element and polls `/.livereload` (served by techdocs-cli), which + * this module maps to MkDocs `/livereload` with permissive CORS headers. + * - Quality-of-life: if extraction fails or the endpoint is unavailable, normal docs still work. + * + * See issue for background and rationale: https://github.com/backstage/backstage/issues/30514 + */ + +const LIVE_RELOAD_ELEMENT = 'live-reload'; +const LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch'; +const LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id'; +const CLI_LIVERELOAD_PATH = '/.livereload'; +const MKDOCS_LIVERELOAD_PATH = '/livereload'; +const CONTENT_TYPE_HTML = 'text/html'; +const HEADER_CONTENT_LENGTH = 'content-length'; + +const BODY_START_RE = /]*>/; +// Matches mkdocs injected call livereload(epoch, requestId) +const MKDOCS_LIVERELOAD_CALL_RE = /livereload\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*;?/; + +/** + * Extract livereload parameters from mkdocs HTML and inject them as a custom element. + * The injected element will later be read by the frontend addon even after DOM sanitization. + * + * Note: + * - we don't add to because of DomPurify sanitization. + * - we add close to the body opening to avoid reading too far into the body. + * - we should use streamed injection to improve performance. + */ +export function injectLivereloadParameters(html: string): string { + const livereloadMatch = html.match(MKDOCS_LIVERELOAD_CALL_RE); + + // If we couldn't find livereload parameters, return original HTML untouched. + if (!livereloadMatch) { + return html; + } + + const [, epoch, requestId] = livereloadMatch; + // Insert a minimal custom element that the frontend addon can discover post-sanitization. + // Note: embedded app needs a custom config to allow the element to survive sanitization. + const liveReloadTag = `<${LIVE_RELOAD_ELEMENT} ${LIVE_RELOAD_ATTR_EPOCH}="${epoch}" ${LIVE_RELOAD_ATTR_REQUEST_ID}="${requestId}">`; + + // Naively find where to insert the livereload tag. + const bodyStart = html.match(BODY_START_RE); + const bodyStartIndex = bodyStart?.index ?? 0; + const bodyStartLength = bodyStart?.[0]?.length ?? 0; + if (bodyStartIndex === 0 || bodyStartLength === 0) { + return html; + } + const bodyEndIndex = bodyStartIndex + bodyStartLength; + + return html.slice(0, bodyEndIndex) + liveReloadTag + html.slice(bodyEndIndex); +} + +function setCorsHeaders(response: http.ServerResponse) { + response.setHeader('Access-Control-Allow-Origin', '*'); + response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); +} + +/** + * Proxies a mkdocs HTML response, injecting livereload parameters into the HTML body. + */ +export function proxyHtmlWithLivereloadInjection(options: { + request: http.IncomingMessage; + response: http.ServerResponse; + mkdocsTargetAddress: string; + proxyEndpoint: string; + onError: (error: Error) => void; +}): void { + const { request, response, mkdocsTargetAddress, proxyEndpoint, onError } = + options; + + const htmlProxy = httpProxy.createProxyServer({ + target: mkdocsTargetAddress, + selfHandleResponse: true, + }); + + htmlProxy.on('error', onError); + + // Intercept HTML responses to inject `` + htmlProxy.on('proxyRes', (proxyRes, _req, res) => { + const contentType = proxyRes.headers['content-type']; + const contentEncoding = proxyRes.headers['content-encoding']; + const isHtml = + contentType && + typeof contentType === 'string' && + contentType.startsWith(CONTENT_TYPE_HTML); + if (isHtml && !contentEncoding) { + const chunks: Buffer[] = []; + proxyRes.on('data', (chunk: Buffer | string) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + proxyRes.on('end', () => { + const body = Buffer.concat(chunks).toString('utf8'); + const modifiedHtml = injectLivereloadParameters(body); + res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200; + Object.keys(proxyRes.headers).forEach(key => { + if (key.toLowerCase() !== HEADER_CONTENT_LENGTH) { + res.setHeader(key, proxyRes.headers[key]!); + } + }); + setCorsHeaders(res); + res.end(modifiedHtml); + }); + } else { + res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200; + Object.keys(proxyRes.headers).forEach(key => { + res.setHeader(key, proxyRes.headers[key]!); + }); + setCorsHeaders(res); + proxyRes.pipe(res); + } + }); + + const forwardPath = + request.url?.replace(new RegExp(`^${proxyEndpoint}`, 'i'), '') || ''; + request.url = forwardPath; + htmlProxy.web(request, response); +} + +/** + * Proxies mkdocs livereload long-polling requests, mapping the CLI path to mkdocs path. + */ +export function proxyMkdocsLivereload(options: { + request: http.IncomingMessage; + response: http.ServerResponse; + mkdocsTargetAddress: string; + onError: (error: Error) => void; +}): void { + const { request, response, mkdocsTargetAddress, onError } = options; + + const proxy = httpProxy.createProxyServer({ target: mkdocsTargetAddress }); + proxy.on('error', onError); + + setCorsHeaders(response); + response.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + request.url = request.url?.replace( + CLI_LIVERELOAD_PATH, + MKDOCS_LIVERELOAD_PATH, + ); + proxy.web(request, response); +} From 7dcedff2fbf180fda446563a31daedabf83bdeb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 8 Oct 2025 10:17:19 +0200 Subject: [PATCH 190/193] bump create-app sqlite version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/modern-cats-double.md | 5 +++++ .../templates/next-app/packages/backend/package.json.hbs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/modern-cats-double.md diff --git a/.changeset/modern-cats-double.md b/.changeset/modern-cats-double.md new file mode 100644 index 0000000000..0c5681aaad --- /dev/null +++ b/.changeset/modern-cats-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bump `better-sqlite3` to the latest version diff --git a/packages/create-app/templates/next-app/packages/backend/package.json.hbs b/packages/create-app/templates/next-app/packages/backend/package.json.hbs index 483c0b7a4e..518683ffd7 100644 --- a/packages/create-app/templates/next-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/next-app/packages/backend/package.json.hbs @@ -44,7 +44,7 @@ "@backstage/plugin-signals-backend": "^{{version '@backstage/plugin-signals-backend'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", "app": "link:../app", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^12.0.0", "node-gyp": "^10.0.0", "pg": "^8.11.3" }, From 74b810a301fb4065b159eada0fc6928499db69fd Mon Sep 17 00:00:00 2001 From: birdhb Date: Wed, 8 Oct 2025 10:37:44 +0100 Subject: [PATCH 191/193] Update .changeset/eighty-cows-care.md Co-authored-by: Patrik Oldsberg Signed-off-by: birdhb --- .changeset/eighty-cows-care.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eighty-cows-care.md b/.changeset/eighty-cows-care.md index 73ec239b36..d4c9fafa4d 100644 --- a/.changeset/eighty-cows-care.md +++ b/.changeset/eighty-cows-care.md @@ -2,4 +2,4 @@ '@backstage/ui': minor --- -**Breaking** We are adding a new component `PasswordField`. We will be removing support for types `password` and `search` on `TextField`. +**BREAKING**: Added a new `PasswordField` component. As part of this change, the `password` and `search` types have been removed from `TextField`. From 019f3a64aa995252da1758289ae65faa9c0ea53e Mon Sep 17 00:00:00 2001 From: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:28:08 -0400 Subject: [PATCH 192/193] Create doc-landing-page.md using html for table (#30884) * Create doc-landing-page.md using html for table HTML is used for the tables in this new doc landing page. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md Changed id of documentation and corrected some spelling mistakes. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md rearrange table and add links Incorporated PR suggestions. Added trial links in two different flavors to see which way works. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md fix prettier spacing Changed all indents to 2 spaces to try to fix prettier errors. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md changed top to "top" for valign attribute Changed valign=top to valign="top" to see if it fixes prettier errors. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md changes double quotes to single quotes Changed all double quotes to single quotes and removed all hrefs to see if I can get the file to pass the prettier check. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md fix prettier errors Hopefully, fixed prettier errors by removing extra lines. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md try to fix prettier errors Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md added links for understanding, using, and administering backstage Added links for each of the bullet points in "Understanding Backstage", "Using Backstage" and "Backstage Administration". Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md added remainder of links Added links for additional features, plugin development, references, and contributing to Backstage. Changed "Scaffolder" bullet to "Software Templates (aka Scaffolder)" Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md to remove Update a Component The document explaining how to update a component has not yet been written. So removing this bullet until the updating documentation exists. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update docusaurus.config.ts for new doc landing page Changed landing page from What is Backstage to the new doc-landing-page.md file. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Update doc-landing-page.md added new technical overview Added a bullet under Learn about Backstage for the new Technical Overview. Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> * Apply suggestion from @aramissennyeydd Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --------- Signed-off-by: milliehartnt123 <108491788+milliehartnt123@users.noreply.github.com> Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --- docs/landing-page/doc-landing-page.md | 109 ++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/landing-page/doc-landing-page.md diff --git a/docs/landing-page/doc-landing-page.md b/docs/landing-page/doc-landing-page.md new file mode 100644 index 0000000000..cb1b889851 --- /dev/null +++ b/docs/landing-page/doc-landing-page.md @@ -0,0 +1,109 @@ +--- +id: doc-landing-page +title: Documentation +description: Documentation landing page. +--- + +## Understand, try, and administer Backstage + +

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 RKrnz{0Kve2h5=6f{{ZQLaHaqN literal 0 HcmV?d00001 diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 63e2c16e3c..de283beec0 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -10,285 +10,232 @@ Backstage is organized into three main components, each catering to different gr - Core - This includes the base functionality developed by core developers within the open-source project. - App - The app represents a deployed instance of a Backstage application, customized and maintained by app developers, typically a productivity team within an organization. It integrates core functionalities with additional plugins. -- Plugins - These provide additional functionalities to enhance the usefulness of your Backstage app. Plugins can be company-specific or open-sourced and reusable. At Spotify, we have over 100 plugins created by more than 50 different teams, significantly enriching the unified developer experience by incorporating contributions from various infrastructure teams. +- Plugins - These provide additional functionalities to enhance the usefulness of your Backstage app. Plugins can be company-specific or open-sourced and reusable. ## Overview -The following diagram shows how Backstage might look when deployed inside a -company which uses the Tech Radar plugin, the Lighthouse plugin, the CircleCI -plugin and the software catalog. +The following diagram shows a high level view of the overall architecture of Backstage. Running this architecture in a real environment typically involves +containerizing the components. Various commands are provided for accomplishing this. There are 3 main components in this architecture: -1. The core Backstage UI -2. The UI plugins and their backing services -3. Databases +- The [frontend](#frontend-building-blocks) includes the core Backstage [UI](#user-interface) which is an [extension](#extensions) that interacts directly with the user to present the information from the integrated core feature plugins, and other plugins added by a user. +- The [backend](#backend-building-blocks) includes the backend plugins, [core services](https://backstage.io/docs/backend-system/core-services/index), and other services. This is the server-side part of Backstage that is responsible for wiring things together. You can deploy more than one backend, and more than one backend container, depending on your need to scale and isolate individual features. +- Databases host your Backstage data. -Running this architecture in a real environment typically involves -containerizing the components. Various commands are provided for accomplishing -this. +![The architecture of a basic Backstage application](../assets/architecture-overview/backstage-front-back-arch.jpeg) -![The architecture of a basic Backstage application](../assets/architecture-overview/backstage-typical-architecture.png) +## Frontend building blocks -## User Interface +The architectural diagram provides an overview of the different building blocks and the other blocks that each of them interacts with. -The UI is a thin, client-side wrapper around a set of plugins. It provides some -core UI components and libraries for shared activities such as config -management. [[live demo](https://demo.backstage.io/catalog)] +### App + +This is the app instance itself that you create and use as the root of your Backstage frontend application. It does not have any direct functionality in and of itself, but is simply responsible for wiring things together. + +### Extensions + +[Extensions](../frontend-system/architecture/20-extensions.md) are the building blocks that build out both the visual and non-visual structure of the application. There are both built-in extensions provided by the app itself as well as extensions provided by plugins. Each extension is attached to a parent with which it shares data and can have any number of children of its own. It is up to the app to wire together all extensions into a single tree known as the app extension tree. It is from this structure that the entire app can then be instantiated and rendered. + +#### User Interface + +The UI is one of the extensions in the frontend. It is a thin, client-side wrapper around a set of plugins. It provides some core UI components and libraries for shared activities such as config management. [[live demo](https://demo.backstage.io/catalog)] ![UI with different components highlighted](../assets/architecture-overview/core-vs-plugin-components-highlighted.png) -Each plugin typically makes itself available in the UI on a dedicated URL. For -example, the Lighthouse plugin is registered with the UI on `/lighthouse`. -[[learn more](https://backstage.io/blog/2020/04/06/lighthouse-plugin)] +Each plugin typically makes itself available in the UI on a dedicated URL. For example, the Service Catalog plugin is registered with the UI on `/catalog`. -![The lighthouse plugin UI](../assets/architecture-overview/lighthouse-plugin.png) +### Frontend plugins -The CircleCI plugin is available on `/circleci`. +Plugins provide the actual features inside an app. The size of a plugin can range from a tiny component to an entire new system in which other plugins can be composed and integrated. Plugins can be completely standalone or built on top of each other to extend existing plugins and augment their features. Plugins can communicate with each other by composing their extensions or by sharing Utility APIs and routes. -![CircleCI Plugin UI](../assets/architecture-overview/circle-ci.png) +Backstage includes the following set of core plugins: -## Plugins and plugin backends +- [Software Catalog](../features/software-catalog/index.md) - A centralized system that contains metadata for all your software, such as services, websites, libraries, ML models, data pipelines, and so on. It can also contain metadata for the physical or virtual infrastructure needed to operate a piece of software. The software catalog can be viewed and searched through a UI. +- [Software Templates](../features/software-templates/index.md) - A tool to help you create components inside Backstage. A template can load skeletons of code, include some variables, and then publish the template to a location, such as GitHub. +- [TechDocs](https://backstage.io/docs/features/techdocs/) - A docs-like-code solution built into Backstage. Documentation is written in Markdown files which lives together with the code. +- [Kubernetes](../features/kubernetes/index.md) - A tool that allows developers to check the health of their services whether it is on a local host or in production. +- [Search](https://backstage.io/docs/features/search/) - Search for information in the Backstage ecosystem. You can customize the look and feel of each search result and use your own search engine. -Each plugin is a client side application which mounts itself on the UI. Plugins -are written in TypeScript or JavaScript. They each live in their own directory -in the `plugins` folder. For example, the source code for the catalog plugin -is available at -[plugins/catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog). +[Plugin architecture](#plugin-architecture) provides greater detail about the architecture of the plugins themselves. -### Installing plugins +### Extension Overrides -Plugins are typically installed as React components in your Backstage -application. For example, -[here](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) -is a file that imports many full-page plugins in the Backstage sample app. +In addition to the built-in extensions and extensions provided by plugins, it is also possible to install [extension overrides](../frontend-system/architecture/25-extension-overrides.md). This is a collection of extensions with high priority that can replace existing extensions. They can for example be used to override an individual extension provided by a plugin, or install a completely new extension, such as a new app theme. -An example of one of these plugin components is the `CatalogIndexPage`, which is -a full-page view that allows you to browse entities in the Backstage catalog. It -is installed in the app by importing it and adding it as an element like this: +### Utility APIs -```tsx -import { CatalogIndexPage } from '@backstage/plugin-catalog'; +[Utility APIs](../api/utility-apis.md) provide functionality that makes it easier to build plugins, make it possible for plugins to share functionality with other plugins, as well as serve as a customization point for integrators to change the behaviour of the app. Each Utility API is defined by a TypeScript interface as well as a reference used to access the implementations. The implementations of Utility APIs are defined by extensions that are provided and can be overridden the same as any other extension. -... +### Routes -const routes = ( - - ... - } /> - ... - -); -``` +The [Backstage routing system](../frontend-system/architecture/36-routes.md) adds a layer of indirection that makes it possible for plugins to route to each other's extensions without explicit knowledge of what URL paths the extensions are rendered at or if they even exist at all. It makes it possible for plugins to share routes with each other and dynamically generate concrete links at runtime. It is the responsibility of the app to resolve these links to actual URLs, but it is also possible for integrators to define their own route bindings that decide how the links should be resolved. The routing system also lets plugins define internal routes, aiding in the linking to different content in the same plugin. -Note that we use `"/catalog"` as our path to this plugin page, but we can choose -any route we want for the page, as long as it doesn't collide with the routes -that we choose for the other plugins in the app. +## Backend building blocks -These components that are exported from plugins are referred to as "Plugin -Extension Components", or "Extension Components". They are regular React -components, but in addition to being able to be rendered by React, they also -contain various pieces of metadata that is used to wire together the entire app. -Extension components are created using `create*Extension` methods, which you can -read more about in the -[composability documentation](../plugins/composability.md). +The architectural diagram provides an overview of the different building blocks, and the other blocks that each of them interact with. -As of this moment, there is no config based install procedure for plugins. Some -code changes are required. +### Backend -### Plugin architecture +This is the [backend instance](../backend-system/architecture/02-backends.md) itself, which you can think of as the unit of deployment. It does not have any functionality in and of itself, but is simply responsible for wiring things together. + +It is up to you to decide how many different backends you want to deploy. You can have all features in a single one, or split things out into multiple smaller deployments, depending on your need to scale and isolate individual features. + +### Backend plugins + +[Plugins](../backend-system/architecture/04-plugins.md) provide the actual features. They operate completely independently of each other. If plugins want to communicate with each other, they must do so over the wire. There can be no direct communication between plugins through code. Because of this constraint, each plugin can be considered to be its own microservice. + +[Plugin architecture](#plugin-architecture) provides greater detail about the architecture of the plugins themselves. + +### Services + +[Services](../backend-system/architecture/03-services.md) provide utilities to help make it simpler to implement plugins, so that each plugin doesn't need to implement everything from scratch. There are many built-in core services, such as the ones for logging, database access, and reading configuration, but you can also import third-party services, or create your own. + +Services are also a customization point for individual backend installations. You can override services with your own implementations, as well as make smaller customizations to existing services. + +### Extension Points + +Many plugins have ways in which you can extend them, for example entity providers for the Catalog, or custom actions for the Scaffolder. These extension patterns are now encoded into Extension Points. + +[Extension Points](../backend-system/architecture/05-extension-points.md) look a little bit like services, since you depend on them just like you would a service. A key difference is that extension points are registered and provided by plugins or modules themselves, based on what customizations each of them want to expose. + +Extension Points are exported separately from the plugin or module instance itself, and it is possible to expose multiple different extension points at once. This makes it easier to evolve and deprecate individual Extension Points over time, rather than dealing with a single large API surface. + +### Modules + +[Modules](../backend-system/architecture/06-modules.md) use Extension Points to add new features to other plugins or modules. They might for example add an individual Catalog Entity Provider, or one or more Scaffolder Actions. + +Each module may only use Extension Points that belong to a single plugin, and the module must be deployed together with that plugin in the same backend instance. Modules may only communicate with their plugin or other modules through the registered extension points. + +Just like plugins, modules also have access to services and can depend on their own service implementations. They will however share services with the plugin that they extend - there are no module-specific service implementations. + +## Databases + +The databases host your Backstage data. The Backstage backend and its built-in plugins are based on the [Knex](http://knexjs.org/) library, and set up a separate logical database per plugin. This gives great isolation and lets them perform migrations and evolve separately from each other. + +The Knex library supports a multitude of databases, but Backstage at this time of writing is tested primarily against two of them: + +- SQLite, which is mainly used as an in-memory mock/test database +- PostgreSQL, which is the preferred production database. + +Other databases such as the MySQL variants are reported to work but [aren't fully tested](https://github.com/backstage/backstage/issues/2460) yet. + +You can find instructions on setting up a PostgreSQL for your Backstage instance in [Database](../getting-started/config/database.md). [Configuring Plugin Databases](../tutorials/configuring-plugin-databases.md) provides information on how to configure a database for a plugin. + +## Plugin architecture Architecturally, plugins can take three forms: -1. Standalone -2. Service backed -3. Third-party backed +- [Standalone](#standalone-plugins) +- [Service backend](#service-backend-plugins) +- [Third-party backend](#third-party-backend-plugins) -#### Standalone plugins +### Standalone plugins -Standalone plugins run entirely in the browser. -[The Tech Radar plugin](https://demo.backstage.io/tech-radar), for example, -simply renders hard-coded information. It doesn't make any API requests to other -services. +Standalone plugins run entirely in the browser. [The Tech Radar plugin](https://demo.backstage.io/tech-radar), for example, simply renders hard-coded information. It doesn't make any API requests to other services. + +The architecture of the Tech Radar installed into a Backstage app is very simple. You just need to add Tech Radar as a frontend plugin into your app, as shown in the following diagram. + +> **NOTE:** +> The following diagram does not show the detailed contents of the frontend and backend containers in order to highlight the changes that pertain to the addition of the specified plugin. + +![ui and tech radar plugin connected together](../assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg) + +Once the plugin has been added, then you can view the Tech Radar information in the Backstage UI. ![tech radar plugin ui](../assets/architecture-overview/tech-radar-plugin.png) -The architecture of the Tech Radar installed into a Backstage app is very -simple. +### Service backend plugins -![ui and tech radar plugin connected together](../assets/architecture-overview/tech-radar-plugin-architecture.png) +Service backend plugins make API requests to a service which is within the purview of the organization running Backstage. -#### Service backed plugins +The Lighthouse plugin, for example, makes requests to the [lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service). The `lighthouse-audit-service` is a microservice which runs a copy of Google's [Lighthouse library](https://github.com/GoogleChrome/lighthouse/) and stores the results in a PostgreSQL database. -Service backed plugins make API requests to a service which is within the -purview of the organisation running Backstage. +The Lighthouse plugin is added to the frontend. The lighthouse-audit-service container is already publicly available in Docker Hub and can be downloaded and run with -The Lighthouse plugin, for example, makes requests to the -[lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service). -The `lighthouse-audit-service` is a microservice which runs a copy of Google's -[Lighthouse library](https://github.com/GoogleChrome/lighthouse/) and stores the -results in a PostgreSQL database. +```bash +docker run spotify/lighthouse-audit-service:latest +``` -Its architecture looks like this: +> **NOTE:** +> The following diagram does not show the detailed contents of the frontend and backend, in order to highlight the changes that pertain to the addition of the specified plugin. -![lighthouse plugin backed to microservice and database](../assets/architecture-overview/lighthouse-plugin-architecture.png) +![lighthouse plugin backend to microservice and database](../assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg) -The software catalog in Backstage is another example of a service backed plugin. -It retrieves a list of services, or "entities", from the Backstage Backend -service and renders them in a table for the user. +The software catalog in Backstage is another example of a service backend plugin. It retrieves a list of services, or "entities", from the Backstage Backend service and renders them in a table for the user. -### Third-party backed plugins +### Third-party backend plugins -Third-party backed plugins are similar to service backed plugins. The main -difference is that the service which backs the plugin is hosted outside of the -ecosystem of the company hosting Backstage. +Third-party backend plugins are similar to service backend plugins. The main difference is that the service which backs the plugin is hosted outside of the ecosystem of the company hosting Backstage. -The CircleCI plugin is an example of a third-party backed plugin. CircleCI is a -SaaS service which can be used without any knowledge of Backstage. It has an API -which a Backstage plugin consumes to display content. +The CircleCI plugin is an example of a third-party backend plugin. CircleCI is a SaaS service which can be used without any knowledge of Backstage. It has an API which a Backstage plugin consumes to display content. -Requests going to CircleCI from the user's browser are passed through a proxy -service that Backstage provides. Without this, the requests would be blocked by -Cross Origin Resource Sharing policies which prevent a browser page served at -[https://example.com](https://example.com) from serving resources hosted at -https://circleci.com. +Requests going to CircleCI from the user's browser are passed through a proxy service that Backstage provides. Without this, the requests would be blocked by Cross Origin Resource Sharing policies which prevent a browser page served at [https://example.com](https://example.com) from serving resources hosted at https://circleci.com. -![CircleCI plugin talking to proxy talking to SaaS Circle CI](../assets/architecture-overview/circle-ci-plugin-architecture.png) +> **NOTE:** +> The following diagram does not show the detailed contents of the frontend and backend, in order to highlight the changes that pertain to the addition of the specified plugin. + +![CircleCI plugin talking to proxy talking to SaaS Circle CI](../assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg) ## Package Architecture -Backstage relies heavily on NPM packages, both for distribution of libraries, -and structuring of code within projects. While the way you structure your -Backstage project is up to you, there is a set of established patterns that we -encourage you to follow. These patterns can help set up a sound project -structure as well as provide familiarity between different Backstage projects. +Backstage relies heavily on NPM packages, both for distribution of libraries, and structuring of code within projects. While the way you structure your Backstage project is up to you, there is a set of established patterns that we encourage you to follow. These patterns can help set up a sound project structure as well as provide familiarity between different Backstage projects. -The following diagram shows an overview of the package architecture of -Backstage. It takes the point of view of an individual plugin and all of the -packages that it may contain, indicated by the thicker border and italic text. -Surrounding the plugin are different package groups which are the different -possible interface points of the plugin. Note that not all library package lists -are complete as packages have been omitted for brevity. +The following diagram shows an overview of the package architecture of Backstage. It takes the point of view of an individual plugin and all of the packages that it may contain, indicated by the thicker border and italic text. Surrounding the plugin are different package groups which are the different possible interface points of the plugin. Note that not all library package lists are complete as packages have been omitted for brevity. ![Package architecture](../assets/architecture-overview/package-architecture.drawio.svg) ### Overview -The arrows in the diagram above indicate a runtime dependency on the code of the -target package. This strict dependency graph only applies to runtime -`dependencies`, and there may be `devDependencies` that break the rules of this -table for the purpose of testing. While there are some arrows that show a -dependency on a collection of frontend, backend and isomorphic packages, those -still have to abide by important compatibility rules shown in the bottom left. +The arrows in the diagram above indicate a runtime dependency on the code of the target package. This strict dependency graph only applies to runtime +`dependencies`, and there may be `devDependencies` that break the rules of this table for the purpose of testing. While there are some arrows that show a dependency on a collection of frontend, backend and isomorphic packages, those still have to abide by important compatibility rules shown in the bottom left. -The `app` and `backend` packages are the entry points of a Backstage project. -The `app` package is the frontend application that brings together a collection -of frontend plugins and customizes them to fit an organization, while the -`backend` package is the backend service that powers the Backstage application. -Worth noting is that there can be more than one instance of each of these -packages within a project. Particularly the `backend` packages can benefit from -being split up into smaller deployment units that each serve their own purpose -with a smaller collection of plugins. +The `app` and `backend` packages are the entry points of a Backstage project. The `app` package is the frontend application that brings together a collection of frontend plugins and customizes them to fit an organization, while the `backend` package is the backend service that powers the Backstage application. Worth noting is that there can be more than one instance of each of these packages within a project. Particularly the `backend` packages can benefit from being split up into smaller deployment units that each serve their own purpose with a smaller collection of plugins. ### Plugin Packages -A typical plugin consists of up to five packages, two frontend ones, two -backend, and one isomorphic package. All packages within the plugin must share a -common prefix, typically of the form `@/plugin-`, but -alternatives like `backstage-plugin-` or -`@/backstage-plugin-` are also valid. Along with this prefix, -each of the packages have their own unique suffix that denotes their role. In -addition to these five plugin packages it's also possible for a plugin to have -additional frontend and backend modules that can be installed to enable optional -features. For a full list of suffixes and their roles, see the -[Plugin Package Structure ADR](../architecture-decisions/adr011-plugin-package-structure.md). +A typical plugin consists of up to five packages, two frontend ones, two backend, and one isomorphic package. All packages within the plugin must share a common prefix, typically of the form `@/plugin-`, but alternatives like `backstage-plugin-` or `@/backstage-plugin-` are also valid. Along with this prefix, each of the packages have their own unique suffix that denotes their role. In addition to these five plugin packages it's also possible for a plugin to have +additional frontend and backend modules that can be installed to enable optional features. For a full list of suffixes and their roles, see the [Plugin Package Structure ADR](../architecture-decisions/adr011-plugin-package-structure.md). -The `-react`, `-common`, and `-node` plugin packages together form the external -library of a plugin. The plugin library enables other plugins to build on top of -and extend a plugin, and likewise allows the plugin to depend on and extend -other plugins. Because of this, it is preferable that plugin library packages -allow duplicate installations of themselves, as you may end up with a mix of -versions being installed as dependencies of various plugins. It is also -forbidden for plugins to directly import non-library packages from other -plugins, all communication between plugins must be handled through libraries and -the application itself. +The `-react`, `-common`, and `-node` plugin packages together form the external library of a plugin. The plugin library enables other plugins to build on top of and extend a plugin, and likewise allows the plugin to depend on and extend other plugins. Because of this, it is preferable that plugin library packages allow duplicate installations of themselves, as you may end up with a mix of versions being installed as dependencies of various plugins. It is also forbidden for plugins to directly import non-library packages from other plugins, all communication between plugins must be handled through libraries and the application itself. ### Frontend Packages -The frontend packages are grouped into two main groups. The first one is -"Frontend App Core", which is the set of packages that are only used by the -`app` package itself. These packages help build up the core structure of the app -as well as provide a foundation for the plugin libraries to rely upon. +The frontend packages are grouped into two main groups. The first one is "Frontend App Core", which is the set of packages that are only used by the `app` package itself. These packages help build up the core structure of the app as well as provide a foundation for the plugin libraries to rely upon. -The second group is the rest of the shared packages, further divided into -"Frontend Plugin Core" and "Frontend Libraries". The core packages are -considered particularly stable and form the core of the frontend framework. -Their most important role is to form the boundary around each plugin and provide -a set of tools that helps you combine a collection of plugins into a running -application. The rest of the frontend packages are more traditional libraries -that serve as building blocks to create plugins. +The second group is the rest of the shared packages, further divided into "Frontend Plugin Core" and "Frontend Libraries". The core packages are considered particularly stable and form the core of the frontend framework. Their most important role is to form the boundary around each plugin and provide a set of tools that helps you combine a collection of plugins into a running application. The rest of the frontend packages are more traditional libraries that serve as building blocks to create plugins. ### Backend Packages -The backend library packages do not currently share a similar plugin -architecture as the frontend packages. They are instead simply a collection of -building blocks and patterns that help you build backend services. This is -however likely to change in the future. +The backend library packages do not currently share a similar plugin architecture as the frontend packages. They are instead simply a collection of building blocks and patterns that help you build backend services. This is however likely to change in the future. ### Common Packages -The common packages are the packages effectively depended on by all other pages. -This is a much smaller set of packages but they are also very pervasive. Because -the common packages are isomorphic and must execute both in the frontend and -backend, they are never allowed to depend on any of the frontend or backend -packages. +The common packages are the packages effectively depended on by all other pages. This is a much smaller set of packages but they are also very pervasive. Because the common packages are isomorphic and must execute both in the frontend and backend, they are never allowed to depend on any of the frontend or backend packages. -The Backstage CLI is in a category of its own and is depended on by virtually -all other packages. It's not a library in itself though, and must always be a -development dependency only. +The Backstage CLI is in a category of its own and is depended on by virtually all other packages. It's not a library in itself though, and must always be a development dependency only. ### Deciding where you place your code -It can sometimes be difficult to decide where to place your plugin code. For example -should it go directly in the `-backend` plugin package or in the `-node` package? -As a general guideline you should try to keep the exposure of your code as low -as possible. If it doesn't need to be public API, it's best to avoid. If you don't -need it to be used by other plugins, then keep it directly in the plugin packages. +It can sometimes be difficult to decide where to place your plugin code. For example, should it go directly in the `-backend` plugin package or in the `-node` package? As a general guideline you should try to keep the exposure of your code as low as possible. If it doesn't need to be public API, it's best to avoid. If you don't need it to be used by other plugins, then keep it directly in the plugin packages. Below is a chart to help you decide where to place your code. ![Package decision](../assets/architecture-overview/package-decision.drawio.svg) -## Databases - -As we have seen, both the `lighthouse-audit-service` and `catalog-backend` -require a database to work with. - -The Backstage backend and its built-in plugins are based on the -[Knex](http://knexjs.org/) library, and set up a separate logical database per -plugin. This gives great isolation and lets them perform migrations and evolve -separately from each other. - -The Knex library supports a multitude of databases, but Backstage at this time -of writing is tested primarily against two of them: SQLite, which is mainly used as -an in-memory mock/test database, and PostgreSQL, which is the preferred -production database. Other databases such as the MySQL variants are reported to -work but -[aren't fully tested](https://github.com/backstage/backstage/issues/2460) -yet. - ## Cache -The Backstage backend and its built-in plugins are also able to leverage cache -stores as a means of improving performance or reliability. Similar to how -databases are supported, plugins receive logically separated cache connections, -which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood. +The Backstage backend and its built-in plugins are also able to leverage cache stores as a means of improving performance or reliability. Similar to how databases are supported, plugins receive logically separated cache connections, which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood. -At this time of writing, Backstage can be configured to use one of five cache -stores: memory, which is mainly used for local testing, memcache, redis, valkey or infinispan, -which are cache stores better suited for production deployment. The right cache -store for your Backstage instance will depend on your own run-time constraints -and those required of the plugins you're running. +At this time of writing, Backstage can be configured to use one of five cache stores: + +- `memory` +- `memcache` +- `redis` +- `valkey` +- `infinispan` + +`memory` is primarily used for local development, for production deployments, we recommend using one of the other cache stores. The right cache store for your Backstage instance will depend on your own run-time constraints and those required of the plugins you're running. ### Use memory for cache @@ -351,30 +298,3 @@ backend: ``` Contributions supporting other cache stores are welcome! - -## Containerization - -The example Backstage architecture shown above would Dockerize into three -separate Docker images. - -1. The frontend container -2. The backend container -3. The Lighthouse audit service container - -![Boxes around the architecture to indicate how it is containerised](../assets/architecture-overview/containerised.png) - -The backend container can be built by running the following command: - -```bash -yarn run build -yarn run build-image -``` - -This will create a container called `example-backend`. - -The lighthouse-audit-service container is already publicly available in Docker -Hub and can be downloaded and run with - -```bash -docker run spotify/lighthouse-audit-service:latest -``` From c017220e7780f1a373d5da811a1941aaaf9b8d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 3 Oct 2025 13:25:53 +0200 Subject: [PATCH 155/193] Update .changeset/fuzzy-trams-kick.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/fuzzy-trams-kick.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fuzzy-trams-kick.md b/.changeset/fuzzy-trams-kick.md index 71c06891d0..b433eac803 100644 --- a/.changeset/fuzzy-trams-kick.md +++ b/.changeset/fuzzy-trams-kick.md @@ -2,4 +2,4 @@ '@backstage/plugin-home': patch --- -fix(home): correct clearAll logic to properly handle deletable flag +fix(home): correct `clearAll` logic to properly handle `deletable` flag From 5c21e451a733de3ff06a050622eefb385efdb47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Fri, 3 Oct 2025 15:52:46 +0200 Subject: [PATCH 156/193] add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .changeset/sad-women-rule.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/sad-women-rule.md diff --git a/.changeset/sad-women-rule.md b/.changeset/sad-women-rule.md new file mode 100644 index 0000000000..caed5bea87 --- /dev/null +++ b/.changeset/sad-women-rule.md @@ -0,0 +1,6 @@ +--- +'example-app': patch +'@backstage/ui': patch +--- + +Add react router for internal routing for ButtonLinks From 5f75636f858c9a4e122a00f3fb639c4914d92baa Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 3 Oct 2025 15:59:21 +0200 Subject: [PATCH 157/193] Update verify_chromatic.yml Signed-off-by: Charles de Dreuille --- .github/workflows/verify_chromatic.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/verify_chromatic.yml b/.github/workflows/verify_chromatic.yml index 0c9f110b4c..18511b936d 100644 --- a/.github/workflows/verify_chromatic.yml +++ b/.github/workflows/verify_chromatic.yml @@ -5,7 +5,7 @@ on: push: branches: - master - pull_request: + pull_request_target: paths: - '.github/workflows/verify_chromatic.yml' - '.storybook/**' @@ -31,6 +31,8 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: + # For pull_request_target, we need to check out the PR branch + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} fetch-depth: 10000 # Required to retrieve git history - name: Use node.js ${{ matrix.node-version }} @@ -63,7 +65,7 @@ jobs: packages/ui/src/**/*.css - name: Prepare Chromatic Message - if: github.event_name == 'pull_request' && steps.chromatic.outputs.url + if: github.event_name == 'pull_request_target' && steps.chromatic.outputs.url id: prepare-message run: | if [ "${{ steps.chromatic.outputs.changeCount }}" = "0" ] || [ -z "${{ steps.chromatic.outputs.changeCount }}" ]; then @@ -73,7 +75,7 @@ jobs: fi - name: Post Chromatic Link in PR Comment - if: github.event_name == 'pull_request' && steps.chromatic.outputs.url + if: github.event_name == 'pull_request_target' && steps.chromatic.outputs.url uses: mshick/add-pr-comment@v2 with: message: | From 722182fa90fb84834cedd298a80a41edb32e0dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Fri, 3 Oct 2025 16:28:19 +0200 Subject: [PATCH 158/193] fix changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .changeset/sad-women-rule.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/sad-women-rule.md b/.changeset/sad-women-rule.md index caed5bea87..04bbbe1eed 100644 --- a/.changeset/sad-women-rule.md +++ b/.changeset/sad-women-rule.md @@ -1,5 +1,4 @@ --- -'example-app': patch '@backstage/ui': patch --- From 97818157bba7038533d9805f16ad38ab47a1887b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Fri, 3 Oct 2025 16:39:03 +0200 Subject: [PATCH 159/193] add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .changeset/thin-hoops-bathe.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thin-hoops-bathe.md diff --git a/.changeset/thin-hoops-bathe.md b/.changeset/thin-hoops-bathe.md new file mode 100644 index 0000000000..508d548431 --- /dev/null +++ b/.changeset/thin-hoops-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Remove auto selection of tabs for tabs that all have href defined From edc15d8cd738bf9479dd382f9325ea1fe4b654b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Fri, 3 Oct 2025 16:50:39 +0200 Subject: [PATCH 160/193] Revert "chore: update package.json" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 82540078e800ebeb26d6d0f751b67019ee682b07. Signed-off-by: Sofia Sjöblad --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index babce203dd..b510a5a864 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,6 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@yarnpkg/plugin-npm@npm:^3.1.0": "patch:@yarnpkg/plugin-npm@npm%3A3.1.0#~/.yarn/patches/@yarnpkg-plugin-npm-npm-3.1.0-6533d0f5a1.patch", - "GendocuPublicApis": "npm:gendocu-public-apis@^1.0.0", "ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", From 67cf9829946b301952ec74a467578b71364e285b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Mon, 6 Oct 2025 10:54:20 +0200 Subject: [PATCH 161/193] fix: update stories to comply with react-aria MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .../HeaderPage/HeaderPage.stories.tsx | 32 +++++++++++++---- .../ui/src/components/Tabs/Tabs.stories.tsx | 36 ++++++++++++++++--- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index c4849ddc20..c3c2b59ae1 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -44,22 +44,27 @@ const tabs: HeaderTab[] = [ { id: 'overview', label: 'Overview', + href: '/overview', }, { id: 'checks', label: 'Checks', + href: '/checks', }, { id: 'tracks', label: 'Tracks', + href: '/tracks', }, { id: 'campaigns', label: 'Campaigns', + href: '/campaigns', }, { id: 'integrations', label: 'Integrations', + href: '/integrations', }, ]; @@ -89,10 +94,19 @@ const withRouter = (Story: StoryFn) => ( ); +// White background decorator for stories +const withWhiteBackground = (Story: StoryFn) => ( +

+ + + + + + + + + + + + +
Understand BackstageTry BackstageAdministrationAdditional Features
Learn about Backstage.

+ +
Build a customizable Backstage app.
+ +
Configure, Deploy, & Upgrade.

+ +
Additional Backstage Features

+ +
+ +## Plugin Development, references, and how to contribute + + + + + + + + + + + + +
Plugin DevelopmentReferencesContribute to Backstage
Creating and Configuring Plugins

+ +
Glossary, FAQ, and other reference material.
+ +
Contributions are welcome and greatly appreciated!
+ +
From ee0e5b3257b50a9548f7d37d3422f45868878346 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 8 Oct 2025 17:46:15 +0200 Subject: [PATCH 193/193] docs: fix monospace font usage Signed-off-by: Vincenzo Scamporlino --- docs-ui/src/app/theming/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-ui/src/app/theming/page.mdx b/docs-ui/src/app/theming/page.mdx index 92b5256791..455b2cd7f5 100644 --- a/docs-ui/src/app/theming/page.mdx +++ b/docs-ui/src/app/theming/page.mdx @@ -446,7 +446,7 @@ monospace font that we use for code blocks and tables. - --bui-font-mono + --bui-font-monospace The monospace font for the theme.