Merge branch 'master' into canon-field

This commit is contained in:
Charles de Dreuille
2025-01-14 10:34:40 +00:00
90 changed files with 2458 additions and 847 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch
---
Use the email from `cfIdentity` instead of `claims` when constructing user profile in order to support Cloudflare Service Tokens.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-proxy-backend': patch
'@backstage/plugin-proxy-node': minor
---
Added `proxyEndpointsExtensionPoint` to allow addition of proxy configuration through an extension point in the new backend system.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-defaults': patch
---
The `GerritUrlReader` can now read content from a commit and not only from the top of a branch. The
Gitiles URL must contain the full commit `SHA` hash like: `https://gerrit.com/gitiles/repo/+/2846e8dc327ae2f60249983b1c3b96f42f205bae/catalog-info.yaml`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
---
Added `reviewerIds` to the Gitlab Merge Request action.
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/integration': patch
---
A new Gerrit helper function (`buildGerritGitilesArchiveUrlFromLocation`) has been added. This
constructs a Gitiles URL to download an archive. It is similar to the existing
`buildGerritGitilesArchiveUrl` but also support content referenced by a full commit `SHA`.
**DEPRECATIONS**: The function `buildGerritGitilesArchiveUrl` is deprecated, use the
`buildGerritGitilesArchiveUrlFromLocation` function instead.
**DEPRECATIONS**: The function `parseGerritGitilesUrl` is deprecated, use the
`parseGitilesUrlRef` function instead.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"mode": "pre",
"mode": "exit",
"tag": "next",
"initialVersions": {
"example-app": "0.2.104",
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
---
Added action `gitlab:group:migrate` to migrate projects using `bulk_import`
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-react': patch
'@backstage/plugin-scaffolder': patch
---
Added support for `FormDecoratorBlueprint` to create form decorators in the Scaffolder plugin
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Removed the `EXPERIMENTAL_VITE` flag for using Vite as a dev server. If you were using this feature, we recommend switching to Rspack via the `EXPERIMENTAL_RSPACK` flag.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-backstage-openapi': patch
---
docs: Use a valid configuration as example
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-server': patch
---
Updated the `getFile` method in `BitbucketServerClient` to use `this.config.apiBaseUrl` directly for constructing the API request URL, removing the creation of an unnecessary `URL` object. The method now relies on REST API paths for accessing resources instead of direct access, ensuring better alignment with Bitbucket's API conventions.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
---
Updated the path field in the `gitlab:group:ensureExists` action to accept an array of either strings or objects with name and slug fields.
+1
View File
@@ -132,6 +132,7 @@ Scope: Tooling and Community Repo Maintainers for the Backstage [Community Plugi
| -------------------- | ------------ | ------------------------------------------- | ------------ |
| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` |
| Bethany Griggs | Red Hat | [BethGriggs](https://github.com/BethGriggs) | `bethgriggs` |
| Kashish Mittal | Red Hat | [04kash](https://github.com/04kash) | `kashh._.` |
| Nick Boldt | Red Hat | [nickboldt](https://github.com/nickboldt) | `nboldt` |
| Vincenzo Scamporlino | Spotify | [vinzscam](https://github.com/vinzscam) | `vinzscam` |
+4
View File
@@ -4,6 +4,10 @@ body {
background-color: var(--canon-background);
color: var(--canon-text-primary);
transition: background-color 0.2s ease-in-out;
--docs-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
iframe {
+6 -6
View File
@@ -1,8 +1,9 @@
import type { Metadata } from 'next';
import { Sidebar } from '../components/Sidebar';
import { Toolbar } from '@/components/Toolbar';
import { Providers } from './providers';
import styles from './page.module.css';
import { Providers } from './providers';
import './globals.css';
import '/public/core.css';
@@ -19,10 +20,6 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
// const cookieStore = await cookies();
// const theme = cookieStore.get('theme')?.value || 'light';
// const themeName = cookieStore.get('theme-name')?.value || 'default';
return (
<html
lang="en"
@@ -34,7 +31,10 @@ export default function RootLayout({
<Providers>
<div className={styles.global}>
<Sidebar />
<div className={styles.container}>{children}</div>
<div className={styles.container}>
<Toolbar />
{children}
</div>
</div>
</Providers>
</body>
Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

+6 -1
View File
@@ -3,10 +3,15 @@
}
.container {
padding-left: 300px;
width: 100%;
}
@media (min-width: 768px) {
.container {
padding-left: 300px;
}
}
.page {
flex: 1;
}
@@ -1,34 +1,34 @@
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100vh;
color: var(--canon-text-primary);
background-color: var(--canon-surface-1);
border-right: 1px solid var(--canon-border-base);
padding-left: 20px;
padding-right: 20px;
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
overflow: hidden;
display: none;
}
@media (min-width: 768px) {
.sidebar {
display: block;
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100vh;
color: var(--canon-text-primary);
background-color: var(--canon-surface-1);
border-right: 1px solid var(--canon-border-base);
padding-left: 20px;
padding-right: 20px;
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
overflow: hidden;
}
}
.logoContainer {
padding-left: 6px;
padding-top: 32px;
}
.logo path {
fill: var(--canon-text-primary);
}
.content {
padding-top: 20px;
}
.actions {
display: flex;
flex-direction: row;
gap: 12px;
margin-top: 20px;
margin-bottom: 12px;
}
.menu {
display: flex;
flex-direction: row;
@@ -46,6 +46,13 @@
width: 100%;
}
.sectionTitle {
font-family: var(--docs-font);
font-size: var(--canon-font-size-body);
font-weight: var(--canon-font-weight-bold);
padding: 12px 0;
}
.line {
width: 100%;
display: flex;
@@ -65,3 +72,16 @@
.line.active {
background-color: var(--canon-surface-2);
}
.lineTitle {
font-family: var(--docs-font);
font-size: var(--canon-font-size-body);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-text-primary);
}
.lineStatus {
font-family: var(--docs-font);
font-size: var(--canon-font-size-body);
color: var(--canon-text-secondary);
}
+4 -9
View File
@@ -55,9 +55,7 @@ export const Docs = () => {
return (
<Fragment key={section.title}>
<Box marginTop="lg" marginBottom="2xs">
<Text variant="body" weight="bold">
{section.title}
</Text>
<div className={styles.sectionTitle}>{section.title}</div>
</Box>
{section.content.map(item => {
const isActive = pathname === `${section.url}/${item.slug}`;
@@ -68,17 +66,14 @@ export const Docs = () => {
key={item.slug}
className={`${styles.line} ${isActive ? styles.active : ''}`}
>
<Text variant="body">{item.title}</Text>
<Text
variant="body"
style={{ color: 'var(--canon-text-secondary)' }}
>
<div className={styles.lineTitle}>{item.title}</div>
<div className={styles.lineStatus}>
{item.status === 'alpha' && 'Alpha'}
{item.status === 'beta' && 'Beta'}
{item.status === 'inProgress' && 'In Progress'}
{item.status === 'stable' && 'Stable'}
{item.status === 'deprecated' && 'Deprecated'}
</Text>
</div>
</Link>
);
})}
+5 -11
View File
@@ -1,5 +1,4 @@
import styles from './Sidebar.module.css';
import { TabsVersion, TabsPages, TabsTheme } from './tabs';
import { Docs } from './docs';
import { Playground } from './playground';
import Link from 'next/link';
@@ -7,7 +6,7 @@ import Link from 'next/link';
export const Sidebar = () => {
return (
<div className={styles.sidebar}>
<div className={styles.content}>
<div className={styles.logoContainer}>
<Link href="/">
<svg
width="89"
@@ -20,15 +19,10 @@ export const Sidebar = () => {
<path d="M14.037 16.729a4.237 4.237 0 0 0 4.234-4.24 4.237 4.237 0 0 0-4.234-4.242 4.237 4.237 0 0 0-4.234 4.241 4.237 4.237 0 0 0 4.234 4.24Z" />
</svg>
</Link>
<div className={styles.actions}>
<TabsVersion />
<TabsTheme />
</div>
<TabsPages />
<div className={styles.menu}>
<Docs />
<Playground />
</div>
</div>
<div className={styles.menu}>
<Docs />
<Playground />
</div>
</div>
);
-103
View File
@@ -1,103 +0,0 @@
'use client';
import styles from './Tabs.module.css';
import { Tabs } from '@base-ui-components/react/tabs';
import { Icon } from '../../../../packages/canon/src/components/Icon';
import { Text } from '../../../../packages/canon/src/components/Text';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/navigation';
import { usePlayground } from '@/utils/playground-context';
export const TabsVersion = () => {
const { selectedThemeName, setSelectedThemeName } = usePlayground();
return (
<Tabs.Root
className={styles.tabs}
onValueChange={setSelectedThemeName}
value={selectedThemeName}
>
<Tabs.List className={styles.list}>
<Tabs.Tab className={styles.tab} value="legacy">
<Text variant="caption" weight="bold">
Theme 1
</Text>
</Tabs.Tab>
<Tabs.Tab className={styles.tab} value="default">
<Text variant="caption" weight="bold">
Theme 2
</Text>
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
export const TabsTheme = () => {
const { selectedTheme, setSelectedTheme } = usePlayground();
return (
<Tabs.Root
className={styles.tabsTheme}
onValueChange={setSelectedTheme}
value={selectedTheme}
>
<Tabs.List className={styles.list}>
<Tabs.Tab className={styles.tab} value="light">
<Icon name="sun" />
</Tabs.Tab>
<Tabs.Tab className={styles.tab} value="dark">
<Icon name="moon" />
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
export const TabsPages = () => {
const pathname = usePathname();
const router = useRouter();
const onValueChange = (value: string) => {
if (value === 'docs') {
router.push('/');
} else {
router.push('/playground');
}
};
return (
<Tabs.Root
className={styles.tabs}
value={pathname.includes('playground') ? 'playground' : 'docs'}
onValueChange={onValueChange}
>
<Tabs.List className={styles.list}>
<Tabs.Tab
className={styles.tab}
value="docs"
onClick={() => {
router.push('/');
}}
>
<Text variant="caption" weight="bold">
Documentation
</Text>
</Tabs.Tab>
<Tabs.Tab
className={styles.tab}
value="playground"
onClick={() => {
router.push('/playground');
}}
>
<Text variant="caption" weight="bold">
Playground
</Text>
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
@@ -0,0 +1,18 @@
import { ThemeSelector } from './theme';
import { ThemeNameSelector } from './theme-name';
import styles from './styles.module.css';
import { Nav } from './nav';
export const Toolbar = () => {
return (
<div className={styles.toolbar}>
<div>
<Nav />
</div>
<div className={styles.actions}>
<ThemeNameSelector />
<ThemeSelector />
</div>
</div>
);
};
@@ -0,0 +1,79 @@
.tabs {
display: none;
}
@media (min-width: 768px) {
.tabs {
display: block;
height: 60px;
}
}
.tabsTheme {
width: 142px;
border-radius: 0.375rem;
background-color: var(--canon-surface-2);
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}
.list {
display: flex;
position: relative;
z-index: 0;
gap: var(--canon-spacing-lg);
}
.tab {
all: unset;
height: 60px;
font-family: var(--docs-font);
color: var(--canon-text-secondary);
font-size: var(--canon-font-size-body);
font-weight: var(--canon-font-weight-bold);
cursor: pointer;
transition: color 0.2s ease-in-out;
&:hover {
color: var(--canon-text-primary);
}
&[data-selected] {
color: var(--canon-text-primary);
& p {
color: var(--canon-text-primary);
}
}
&:focus-visible {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 2px solid var(--canon-surface-1);
outline-offset: -1px;
}
}
}
.tab p {
color: var(--canon-text-secondary) !important;
}
.indicator {
position: absolute;
z-index: -1;
left: 0;
bottom: -1px;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 1px;
border-radius: 0.25rem;
background-color: var(--canon-text-primary);
transition-property: translate, width, background-color;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
+49
View File
@@ -0,0 +1,49 @@
'use client';
import { Tabs } from '@base-ui-components/react/tabs';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/navigation';
import styles from './nav.module.css';
export const Nav = () => {
const pathname = usePathname();
const router = useRouter();
const onValueChange = (value: string) => {
if (value === 'docs') {
router.push('/');
} else {
router.push('/playground');
}
};
return (
<Tabs.Root
className={styles.tabs}
value={pathname.includes('playground') ? 'playground' : 'docs'}
onValueChange={onValueChange}
>
<Tabs.List className={styles.list}>
<Tabs.Tab
className={styles.tab}
value="docs"
onClick={() => {
router.push('/');
}}
>
Documentation
</Tabs.Tab>
<Tabs.Tab
className={styles.tab}
value="playground"
onClick={() => {
router.push('/playground');
}}
>
Playground
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
@@ -0,0 +1,21 @@
.toolbar {
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
background-color: var(--canon-surface-1);
border-bottom: 1px solid var(--canon-border-base);
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--canon-spacing-md);
transition: background-color 0.2s ease-in-out;
}
.actions {
display: flex;
align-items: center;
gap: var(--canon-spacing-sm);
}
@@ -0,0 +1,137 @@
.Select {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
height: 2rem;
padding-left: 0.875rem;
padding-right: 0.75rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
cursor: pointer;
user-select: none;
background-color: var(--canon-surface-2);
transition: background-color 0.2s ease-in-out;
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.SelectIcon {
display: flex;
}
.SelectValue {
font-size: var(--canon-font-size-body);
font-weight: var(--canon-font-weight-regular);
}
.Positioner {
z-index: 20;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
color: var(--color-gray-900);
border: 1px solid var(--canon-border-base);
padding-inline: 0.25rem;
transform-origin: var(--transform-origin);
transition: transform 150ms, opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side='none'] {
transition: none;
transform: none;
opacity: 1;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow: 0px 10px 15px -3px var(--color-gray-200),
0px 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Item {
box-sizing: border-box;
outline: 0;
font-size: 0.875rem;
line-height: 1rem;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 1rem;
font-size: var(--canon-font-size-body);
font-weight: var(--canon-font-weight-bold);
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
cursor: default;
user-select: none;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
[data-side='none'] & {
font-size: 1rem;
padding-right: 3rem;
min-width: calc(var(--anchor-width) + 1rem);
}
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
background-color: var(--canon-surface-2);
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.ItemIndicator {
grid-column-start: 1;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
}
.ItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.ItemText {
grid-column-start: 2;
}
@@ -0,0 +1,48 @@
'use client';
import { Select } from '@base-ui-components/react/select';
import styles from './theme-name.module.css';
import { Icon } from '@backstage/canon';
import { usePlayground } from '@/utils/playground-context';
const themes = [
{ name: 'Backstage Legacy', value: 'legacy' },
{ name: 'Backstage Default', value: 'default' },
];
export const ThemeNameSelector = () => {
const { selectedThemeName, setSelectedThemeName } = usePlayground();
return (
<Select.Root
value={selectedThemeName || 'default'}
onValueChange={setSelectedThemeName}
>
<Select.Trigger className={styles.Select}>
<Select.Value
className={styles.SelectValue}
placeholder="Select a theme"
/>
<Select.Icon className={styles.SelectIcon}>
<Icon name="chevronDown" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8}>
<Select.Popup className={styles.Popup}>
{themes.map(({ name, value }) => (
<Select.Item className={styles.Item} value={value}>
<Select.ItemIndicator className={styles.ItemIndicator}>
<Icon name="check" />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>
{name}
</Select.ItemText>
</Select.Item>
))}
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
};
@@ -6,7 +6,7 @@
}
.tabsTheme {
width: 142px;
width: 100px;
border-radius: 0.375rem;
background-color: var(--canon-surface-2);
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
@@ -29,21 +29,17 @@
outline: 0;
background: none;
appearance: none;
color: var(--canon-text-secondary) !important;
font-family: inherit;
font-size: 0.75rem;
line-height: 1.25rem;
font-weight: 500;
color: var(--canon-text-secondary);
user-select: none;
height: 2.25rem;
height: 2rem;
flex: 1;
cursor: pointer;
&[data-selected] {
color: var(--canon-text-primary) !important;
color: var(--canon-text-primary);
& p {
color: var(--canon-text-primary) !important;
color: var(--canon-text-primary);
}
}
@@ -78,7 +74,7 @@
top: 50%;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 1.8rem;
height: 1.5rem;
border-radius: 0.25rem;
background-color: var(--canon-surface-1);
transition-property: translate, width, background-color;
@@ -0,0 +1,28 @@
'use client';
import { Tabs } from '@base-ui-components/react/tabs';
import { Icon } from '@backstage/canon';
import { usePlayground } from '@/utils/playground-context';
import styles from './theme.module.css';
export const ThemeSelector = () => {
const { selectedTheme, setSelectedTheme } = usePlayground();
return (
<Tabs.Root
className={styles.tabsTheme}
onValueChange={setSelectedTheme}
value={selectedTheme}
>
<Tabs.List className={styles.list}>
<Tabs.Tab className={styles.tab} value="light">
<Icon name="sun" />
</Tabs.Tab>
<Tabs.Tab className={styles.tab} value="dark">
<Icon name="moon" />
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
+54 -8
View File
@@ -1,33 +1,64 @@
import React, { ReactNode } from 'react';
import type { MDXComponents } from 'mdx/types';
import Image, { ImageProps } from 'next/image';
import { ReactNode } from 'react';
import React from 'react';
import { CodeBlock } from '@/components/CodeBlock';
import { Heading } from '../../packages/canon/src/components/Heading';
import { Text } from '../../packages/canon/src/components/Text';
import { Box } from '../../packages/canon/src/components/Box';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// Allows customizing built-in components, e.g. to add styling.
h1: ({ children }) => (
<Box marginBottom="md" style={{ marginTop: '4rem' }}>
<Heading variant="title2">{children as ReactNode}</Heading>
<Box style={{ marginTop: '4rem' }}>
<h1
style={{
fontFamily: 'var(--docs-font)',
fontSize: '3rem',
fontWeight: 'var(--canon-font-weight-bold)',
margin: 0,
}}
>
{children as ReactNode}
</h1>
</Box>
),
h2: ({ children }) => (
<Box marginTop="2xl" marginBottom="md">
<Heading variant="title4">{children as ReactNode}</Heading>
<h2
style={{
fontFamily: 'var(--docs-font)',
fontSize: '1.5rem',
fontWeight: 'var(--canon-font-weight-bold)',
}}
>
{children as ReactNode}
</h2>
</Box>
),
h3: ({ children }) => (
<Box marginTop="xl" marginBottom="xs">
<Heading variant="title5">{children as ReactNode}</Heading>
<h3
style={{
fontFamily: 'var(--docs-font)',
fontSize: '1.25rem',
fontWeight: 'var(--canon-font-weight-bold)',
}}
>
{children as ReactNode}
</h3>
</Box>
),
p: ({ children }) => (
<Box marginBottom="sm">
<Text variant="subtitle">{children as ReactNode}</Text>
<p
style={{
fontFamily: 'var(--docs-font)',
fontSize: '1rem',
lineHeight: '1.5',
}}
>
{children as ReactNode}
</p>
</Box>
),
a: ({ children, href }) => (
@@ -42,6 +73,21 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
return <CodeBlock lang="tsx" code={codeContent} />;
},
code: ({ children }) => (
<code
style={{
fontFamily: 'var(--canon-font-monospace)',
backgroundColor: 'var(--canon-surface-1)',
padding: '0.2rem 0.375rem',
borderRadius: '0.25rem',
color: 'var(--canon-text-secondary)',
border: '1px solid var(--canon-border-base)',
fontSize: '0.875rem',
}}
>
{children}
</code>
),
img: props => (
<Image
sizes="100vw"
+1 -1
View File
@@ -61,7 +61,7 @@ More details are provided in dedicated sections of the documentation.
- [Reading Configuration](./reading.md): How to read configuration in your
plugin.
- [Writing Configuration](./writing.md): How to provide configuration for your
- [Writing Configuration](./writing.md): How to write configuration for your
Backstage deployment.
- [Defining Configuration](./defining.md): How to define a configuration schema
for users of your plugin or package.
+1 -1
View File
@@ -169,7 +169,7 @@ Kubernetes definitions in a single file and apply them at the same time.
Note the volume `type: local`; this creates a volume using local disk on
Kubernetes nodes. More likely in a production scenario, you'd want to use a more
highly available
[type of PersistentVolume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes).
[types of PersistentVolume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes).
Apply the storage volume and claim to the Kubernetes cluster:
+1 -1
View File
@@ -108,7 +108,7 @@ The following versioning policy applies to all packages:
- Breaking changes are noted in the changelog, and documentation is updated.
- Breaking changes are prefixed with `**BREAKING**: ` in the changelog.
- All public exports are considered stable and will have an entry in the
changelog
changelog.
- Breaking changes are recommended to document a clear upgrade path in the
changelog. This may be omitted for newly introduced or unstable packages.
+1 -1
View File
@@ -11,7 +11,7 @@ Backstage integrator.
## Datadog RUM Events
See how to install Datadog Events in your app
[here](../integrations/datadog-rum/installation.md)
[here](../integrations/datadog-rum/installation.md).
## Logging
+39
View File
@@ -120,3 +120,42 @@ third-parties.
The same logic applies to headers that are sent from the target back to the
frontend.
### Proxy Extension Endpoint
The proxy plugin additionally supports a `proxyExtensionEndpoint` which a proxy
plugin module can utilize in order to programmatically register additional
endpoints, whose payloads are specified exactly as in app-config (described
above). Note that endpoints configured in app-config will always override those
registered in this manner.
Example:
```ts
backend.add(
createBackendModule({
pluginId: 'proxy',
moduleId: 'demo-additional-endpoints',
register: reg => {
reg.registerInit({
deps: {
proxyEndpoints: proxyEndpointsExtensionPoint,
},
init: async ({ proxyEndpoints }) => {
let largerExampleAuth: string = /* exercise for the reader */;
proxyEndpoints.addProxyEndpoints({
"/simple-example": "http://simple.example.com:8080",
"/larger-example/v1": {
target: "http://larger.example.com:8080/svc.v1",
credentials: "require",
headers: {
Authorization: largerExampleAuth
},
},
});
},
});
},
}),
);
```
@@ -48,9 +48,9 @@ If you are encountering errors related to React, it is likely that the versions
## Generating temporary patches
!!!info
:::info
This feature is experimental and currently only supports Yarn workspaces.
!!!
:::
After making local changes to a package in an external workspace you might often want to merge and deploy these changes in your own project. You can use Yarn patches for this purpose, but it can be quite cumbersome to create these patches manually. To make this process easier, you can use `yarn backstage-repo-tools generate-patch` command from the `@backstage/repo-tools` package to generate a patch and resolution entries in the external workspace.
+9
View File
@@ -0,0 +1,9 @@
title: Crossplane
author: terasky.com
authorUrl: https://github.com/terasky-oss
category: Kubernetes
description: View Crossplane Resources in Backstage.
documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugins/crossplane-resources
iconUrl: https://github.com/cncf/artwork/blob/main/projects/crossplane/icon/color/crossplane-icon-color.png?raw=true
npmPackageName: '@terasky/backstage-plugin-crossplane-resources-frontend'
addedDate: '2024-12-30'
+10
View File
@@ -0,0 +1,10 @@
---
title: Glean
author: Mark Shields
authorUrl: https://github.com/beejiujitsu
category: Search
description: Index Backstage TechDocs-enabled Entities into Glean, the AI-powered work assistant
documentation: https://github.com/beejiujitsu/wealthsimple-backstage-plugins/tree/master/plugins/glean-backend
iconUrl: /img/glean-logo-circular-white.png
npmPackageName: '@beejiujitsu/backstage-plugin-glean-backend'
addedDate: '2024-12-30'
+14
View File
@@ -0,0 +1,14 @@
---
title: Jaeger Tracing
author: Deepankumar
authorUrl: https://github.com/deepan10
category: Observability
description: View Jaeger traces and trace details in Backstage components.
documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/jaeger/plugins/jaeger/README.md
iconUrl: https://avatars.githubusercontent.com/u/28545596?s=200&v=4
npmPackageName: '@backstage-community/plugin-jaeger'
tags:
- monitoring
- tracing
- observability
addedDate: '2024-12-21'
@@ -0,0 +1,9 @@
title: Kubernetes Ingestor
author: terasky.com
authorUrl: https://github.com/terasky-oss
category: Kubernetes
description: Automatically Ingest Kubernetes Workloads and Crossplane Claims as Components, and XRDs as Templates.
documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugins/kubernetes-ingestor
iconUrl: https://avatars.githubusercontent.com/u/13629408
npmPackageName: '@terasky/backstage-plugin-kubernetes-ingestor'
addedDate: '2024-12-30'
+9
View File
@@ -0,0 +1,9 @@
title: Kyverno Policy Reports
author: terasky.com
authorUrl: https://github.com/terasky-oss
category: Kubernetes
description: View Kyverno Policy reports for Kubernetes resources related to components in Backstage.
documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugins/kyverno-policy-reports
iconUrl: https://github.com/cncf/artwork/blob/main/projects/kyverno/icon/color/kyverno-icon-color.png?raw=true
npmPackageName: '@terasky/backstage-plugin-kyverno-policy-reports'
addedDate: '2025-01-05'
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

-5
View File
@@ -73,18 +73,13 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@octokit/rest": "^19.0.3",
"@vitejs/plugin-react": "^4.3.1",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4",
"vite": "^5.0.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-node-polyfills": "^0.22.0",
"zen-observable": "^0.10.0"
},
"devDependencies": {
@@ -20,7 +20,7 @@ import {
registerMswTestHooks,
} from '@backstage/backend-test-utils';
import { ConfigReader } from '@backstage/config';
import { NotModifiedError, NotFoundError } from '@backstage/errors';
import { NotModifiedError } from '@backstage/errors';
import {
GerritIntegration,
readGerritIntegrationConfig,
@@ -36,22 +36,11 @@ import { GerritUrlReader } from './GerritUrlReader';
import getRawBody from 'raw-body';
const mockDir = createMockDirectory({ mockOsTmpDir: true });
const env = process.env;
process.env = { ...env, DISABLE_GERRIT_GITILES_REQUIREMENT: '1' };
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
const cloneMock = jest.fn(() => Promise.resolve());
jest.mock('./git', () => ({
Git: {
fromAuth: () => ({
clone: cloneMock,
}),
},
}));
// Gerrit processor without a gitilesBaseUrl configured
const gerritProcessor = new GerritUrlReader(
new GerritIntegration(
@@ -96,12 +85,10 @@ describe.skip('GerritUrlReader', () => {
beforeEach(() => {
mockDir.clear();
process.env = { ...env, DISABLE_GERRIT_GITILES_REQUIREMENT: '1' };
});
afterAll(() => {
jest.clearAllMocks();
process.env = env;
});
describe('reader factory', () => {
@@ -186,7 +173,24 @@ describe.skip('GerritUrlReader', () => {
const buffer = await result.buffer();
expect(buffer.toString()).toBe(responseBuffer.toString());
});
it('should be able to read file contents of a commit as buffer', async () => {
worker.use(
rest.get(
'https://gerrit.com/projects/web%2Fproject/commits/f775f9119c313c7ffc890d7908a45997273434d5/files/LICENSE/content',
(_, res, ctx) => {
return res(
ctx.status(200),
ctx.body(responseBuffer.toString('base64')),
);
},
),
);
const result = await gerritProcessor.readUrl(
'https://gerrit.com/web/project/+/f775f9119c313c7ffc890d7908a45997273434d5/LICENSE',
);
const buffer = await result.buffer();
expect(buffer.toString()).toBe(responseBuffer.toString());
});
it('should be able to read file contents as stream', async () => {
worker.use(
rest.get(
@@ -258,6 +262,7 @@ describe.skip('GerritUrlReader', () => {
const treeUrlGitiles =
'https://gerrit.com/gitiles/app/web/+/refs/heads/master/';
const etag = '52432507a70b677b5674b019c9a46b2e9f29d0a1';
const sha = 'f775f9119c313c7ffc890d7908a45997273434d5';
const mkdocsContent = 'a repo fetched using git clone';
const mdContent = 'doc';
const repoArchiveBuffer = fs.readFileSync(
@@ -302,6 +307,19 @@ describe.skip('GerritUrlReader', () => {
ctx.body(repoArchiveDocsBuffer),
),
),
rest.get(
`https://gerrit.com/gitiles/app/web/\\+archive/${sha}.tar.gz`,
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
ctx.set(
'content-disposition',
'attachment; filename=web-refs/heads/master.tar.gz',
),
ctx.body(repoArchiveBuffer),
),
),
);
});
@@ -330,8 +348,6 @@ describe.skip('GerritUrlReader', () => {
const mdFile = await files[1].content();
expect(mdFile.toString()).toBe('site_name: Test\n');
expect(cloneMock).not.toHaveBeenCalled();
});
it('throws NotModifiedError for matching etags.', async () => {
@@ -346,15 +362,17 @@ describe.skip('GerritUrlReader', () => {
);
});
it('throws NotFoundError if branch info not found.', async () => {
it('throws ResponseError if branch info not found.', async () => {
worker.use(
rest.get(branchAPIUrl, (_, res, ctx) => {
return res(ctx.status(404, 'Not found.'));
}),
);
await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow(
NotFoundError,
await expect(
gerritProcessor.readTree(treeUrl),
).rejects.toMatchInlineSnapshot(
`[ResponseError: Request failed with 404 Not found.]`,
);
});
@@ -386,8 +404,20 @@ describe.skip('GerritUrlReader', () => {
const mdFile = await files[0].content();
expect(mdFile.toString()).toBe('# Test\n');
});
it('throws NotModifiedError for a known commit.', async () => {
const shaTreeUrl = `https://gerrit.com/app/web/+/${sha}/`;
expect(cloneMock).not.toHaveBeenCalled();
await expect(
gerritProcessor.readTree(shaTreeUrl, { etag: sha }),
).rejects.toThrow(NotModifiedError);
});
it('can fetch files for a specifc sha.', async () => {
const response = await gerritProcessorWithGitiles.readTree(
`https://gerrit.com/gitiles/app/web/+/${sha}/`,
);
expect(response.etag).toBe(sha);
});
});
});
@@ -28,14 +28,18 @@ import { Readable } from 'stream';
import {
GerritIntegration,
ScmIntegrations,
buildGerritGitilesArchiveUrl,
buildGerritGitilesArchiveUrlFromLocation,
getGerritBranchApiUrl,
getGerritFileContentsApiUrl,
getGerritRequestOptions,
parseGerritGitilesUrl,
parseGerritJsonResponse,
parseGitilesUrlRef,
} from '@backstage/integration';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import {
NotFoundError,
NotModifiedError,
ResponseError,
} from '@backstage/errors';
import { ReadTreeResponseFactory, ReaderFactory } from './types';
/**
@@ -135,34 +139,9 @@ export class GerritUrlReader implements UrlReaderService {
url: string,
options?: UrlReaderServiceReadTreeOptions,
): Promise<UrlReaderServiceReadTreeResponse> {
const apiUrl = getGerritBranchApiUrl(this.integration.config, url);
let response: Response;
try {
response = await fetch(apiUrl, {
method: 'GET',
...getGerritRequestOptions(this.integration.config),
});
} catch (e) {
throw new Error(`Unable to read branch state ${url}, ${e}`);
}
const urlRevision = await this.getRevisionForUrl(url, options);
if (response.status === 404) {
throw new NotFoundError(`Not found: ${url}`);
}
if (!response.ok) {
throw new Error(
`${url} could not be read as ${apiUrl}, ${response.status} ${response.statusText}`,
);
}
const branchInfo = (await parseGerritJsonResponse(response as any)) as {
revision: string;
};
if (options?.etag === branchInfo.revision) {
throw new NotModifiedError();
}
return this.readTreeFromGitiles(url, branchInfo.revision, options);
return this.readTreeFromGitiles(url, urlRevision, options);
}
async search(): Promise<UrlReaderServiceSearchResponse> {
@@ -179,16 +158,10 @@ export class GerritUrlReader implements UrlReaderService {
revision: string,
options?: UrlReaderServiceReadTreeOptions,
) {
const { branch, filePath, project } = parseGerritGitilesUrl(
const archiveUrl = buildGerritGitilesArchiveUrlFromLocation(
this.integration.config,
url,
);
const archiveUrl = buildGerritGitilesArchiveUrl(
this.integration.config,
project,
branch,
filePath,
);
const archiveResponse = await fetch(archiveUrl, {
...getGerritRequestOptions(this.integration.config),
// TODO(freben): The signal cast is there because pre-3.x versions of
@@ -217,4 +190,41 @@ export class GerritUrlReader implements UrlReaderService {
stripFirstDirectory: false,
});
}
private async getRevisionForUrl(
url: string,
options?: UrlReaderServiceReadTreeOptions,
): Promise<string> {
const { ref, refType } = parseGitilesUrlRef(this.integration.config, url);
// The url points to a static revision.
if (refType === 'sha') {
if (options?.etag === ref) {
throw new NotModifiedError();
}
return ref;
}
const apiUrl = getGerritBranchApiUrl(this.integration.config, url);
let response: Response;
try {
response = await fetch(apiUrl, {
method: 'GET',
...getGerritRequestOptions(this.integration.config),
});
} catch (e) {
throw new Error(`Unable to read branch state ${url}, ${e}`);
}
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const branchInfo = (await parseGerritJsonResponse(response as any)) as {
revision: string;
};
if (options?.etag === branchInfo.revision) {
throw new NotModifiedError();
}
return branchInfo.revision;
}
}
+2 -26
View File
@@ -192,28 +192,16 @@
"@types/terser-webpack-plugin": "^5.0.4",
"@types/webpack-sources": "^3.2.3",
"@types/yarnpkg__lockfile": "^1.1.4",
"@vitejs/plugin-react": "^4.3.1",
"del": "^8.0.0",
"msw": "^1.0.0",
"nodemon": "^3.0.1",
"vite": "^5.0.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-node-polyfills": "^0.22.0"
"nodemon": "^3.0.1"
},
"peerDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@rspack/core": "^1.0.10",
"@rspack/dev-server": "^1.0.9",
"@rspack/plugin-react-refresh": "^1.0.0",
"@vitejs/plugin-react": "^4.0.4",
"vite": "^5.0.0",
"vite-plugin-html": "^3.2.0",
"vite-plugin-node-polyfills": "^0.22.0"
"@rspack/plugin-react-refresh": "^1.0.0"
},
"peerDependenciesMeta": {
"@modyfi/vite-plugin-yaml": {
"optional": true
},
"@rspack/core": {
"optional": true
},
@@ -222,18 +210,6 @@
},
"@rspack/plugin-react-refresh": {
"optional": true
},
"@vitejs/plugin-react": {
"optional": true
},
"vite": {
"optional": true
},
"vite-plugin-html": {
"optional": true
},
"vite-plugin-node-polyfills": {
"optional": true
}
},
"configSchema": {
+74 -165
View File
@@ -27,7 +27,6 @@ import { createConfig, resolveBaseUrl, resolveEndpoint } from './config';
import { createDetectedModulesEntryPoint } from './packageDetection';
import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths';
import { ServeOptions } from './types';
import { hasReactDomClient } from './hasReactDomClient';
export async function serveBundle(options: ServeOptions) {
const paths = resolveBundlingPaths(options);
@@ -54,17 +53,11 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
const { name } = await fs.readJson(libPaths.resolveTarget('package.json'));
let webpackServer: WebpackDevServer | undefined = undefined;
// @ts-ignore
let viteServer: import('vite').ViteDevServer | undefined = undefined;
let latestFrontendAppConfigs: AppConfig[] = [];
/** Triggers a full reload of all clients */
const triggerReload = () => {
if (viteServer) {
viteServer.restart();
}
if (webpackServer) {
webpackServer.invalidate();
@@ -147,167 +140,84 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
moduleFederation: options.moduleFederation,
});
if (process.env.EXPERIMENTAL_VITE) {
const vite = require('vite') as typeof import('vite');
const { default: viteReact } =
require('@vitejs/plugin-react') as typeof import('@vitejs/plugin-react');
const { default: viteYaml } =
require('@modyfi/vite-plugin-yaml') as typeof import('@modyfi/vite-plugin-yaml');
const { nodePolyfills: viteNodePolyfills } =
require('vite-plugin-node-polyfills') as typeof import('vite-plugin-node-polyfills');
const { createHtmlPlugin: viteHtml } =
require('vite-plugin-html') as typeof import('vite-plugin-html');
const bundler = (rspack ?? webpack) as typeof webpack;
const DevServer: typeof WebpackDevServer = rspack
? require('@rspack/dev-server').RspackDevServer
: WebpackDevServer;
viteServer = await vite.createServer({
define: {
'process.argv': JSON.stringify(process.argv),
'process.env.APP_CONFIG': JSON.stringify(cliConfig.frontendAppConfigs),
// This allows for conditional imports of react-dom/client, since there's no way
// to check for presence of it in source code without module resolution errors.
'process.env.HAS_REACT_DOM_CLIENT': JSON.stringify(hasReactDomClient()),
},
optimizeDeps: {
esbuildOptions: {
plugins: [
{
name: 'custom-define',
setup(build) {
const define = (build.initialOptions.define ||= {});
define['process.env.HAS_REACT_DOM_CLIENT'] = JSON.stringify(
hasReactDomClient(),
);
define['process.env.NODE_ENV'] = JSON.stringify('development');
},
},
],
},
},
plugins: [
viteReact(),
viteNodePolyfills({
include: [
'buffer',
'events',
'fs',
'http',
'https',
'os',
'path',
'process',
'querystring',
'stream',
'url',
'util',
'zlib',
],
globals: {
global: true,
Buffer: true,
process: true,
},
}),
viteYaml(),
viteHtml({
entry: paths.targetEntry,
// todo(blam): we should look at contributing to thPe plugin here
// to support absolute paths, but works in the interim at least.
template: 'public/index.html',
inject: {
data: {
config: frontendConfig,
publicPath: config.output?.publicPath,
},
},
}),
],
server: {
host,
port,
},
publicDir: paths.targetPublic,
root: paths.targetPath,
});
} else {
const bundler = (rspack ?? webpack) as typeof webpack;
const DevServer: typeof WebpackDevServer = rspack
? require('@rspack/dev-server').RspackDevServer
: WebpackDevServer;
if (rspack) {
console.log(
chalk.yellow(`⚠️ WARNING: Using experimental RSPack dev server.`),
);
}
const publicPaths = await resolveOptionalBundlingPaths({
entry: 'src/index-public-experimental',
dist: 'dist/public',
});
if (publicPaths) {
console.log(
chalk.yellow(
`⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`,
),
);
}
const compiler = publicPaths
? bundler([config, await createConfig(publicPaths, commonConfigOptions)])
: bundler(config);
webpackServer = new DevServer(
{
hot: !process.env.CI,
devMiddleware: {
publicPath: config.output?.publicPath as string,
stats: 'errors-warnings',
},
static: paths.targetPublic
? {
publicPath: config.output?.publicPath as string,
directory: paths.targetPublic,
}
: undefined,
historyApiFallback:
options.moduleFederation?.mode === 'remote'
? false
: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
// The index needs to be rewritten relative to the new public path, including subroutes.
index: `${config.output?.publicPath}index.html`,
},
server:
url.protocol === 'https:'
? {
type: 'https',
options: {
cert: fullConfig.getString('app.https.certificate.cert'),
key: fullConfig.getString('app.https.certificate.key'),
},
}
: {},
host,
port,
proxy: targetPkg.proxy,
// When the dev server is behind a proxy, the host and public hostname differ
allowedHosts: [url.hostname],
client: {
webSocketURL: { hostname: host, port },
},
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers':
'X-Requested-With, content-type, Authorization',
},
},
compiler,
if (rspack) {
console.log(
chalk.yellow(`⚠️ WARNING: Using experimental RSPack dev server.`),
);
}
await viteServer?.listen();
const publicPaths = await resolveOptionalBundlingPaths({
entry: 'src/index-public-experimental',
dist: 'dist/public',
});
if (publicPaths) {
console.log(
chalk.yellow(
`⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`,
),
);
}
const compiler = publicPaths
? bundler([config, await createConfig(publicPaths, commonConfigOptions)])
: bundler(config);
webpackServer = new DevServer(
{
hot: !process.env.CI,
devMiddleware: {
publicPath: config.output?.publicPath as string,
stats: 'errors-warnings',
},
static: paths.targetPublic
? {
publicPath: config.output?.publicPath as string,
directory: paths.targetPublic,
}
: undefined,
historyApiFallback:
options.moduleFederation?.mode === 'remote'
? false
: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
// The index needs to be rewritten relative to the new public path, including subroutes.
index: `${config.output?.publicPath}index.html`,
},
server:
url.protocol === 'https:'
? {
type: 'https',
options: {
cert: fullConfig.getString('app.https.certificate.cert'),
key: fullConfig.getString('app.https.certificate.key'),
},
}
: {},
host,
port,
proxy: targetPkg.proxy,
// When the dev server is behind a proxy, the host and public hostname differ
allowedHosts: [url.hostname],
client: {
webSocketURL: { hostname: host, port },
},
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers':
'X-Requested-With, content-type, Authorization',
},
},
compiler,
);
await new Promise<void>(async (resolve, reject) => {
if (webpackServer) {
webpackServer.startCallback((err?: Error) => {
@@ -330,7 +240,6 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
webpackServer?.stop();
viteServer?.close();
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
process.exit();
});
+8 -2
View File
@@ -299,7 +299,7 @@ export type BitbucketServerIntegrationConfig = {
password?: string;
};
// @public
// @public @deprecated
export function buildGerritGitilesArchiveUrl(
config: GerritIntegrationConfig,
project: string,
@@ -307,6 +307,12 @@ export function buildGerritGitilesArchiveUrl(
filePath: string,
): string;
// @public
export function buildGerritGitilesArchiveUrlFromLocation(
config: GerritIntegrationConfig,
url: string,
): string;
// @public
export class DefaultAzureCredentialsManager implements AzureCredentialsManager {
static fromIntegrations(
@@ -802,7 +808,7 @@ export interface IntegrationsByType {
harness: ScmIntegrationsGroup<HarnessIntegration>;
}
// @public
// @public @deprecated
export function parseGerritGitilesUrl(
config: GerritIntegrationConfig,
url: string,
@@ -21,6 +21,7 @@ import { registerMswTestHooks } from '../helpers';
import { GerritIntegrationConfig } from './config';
import {
buildGerritGitilesArchiveUrl,
buildGerritGitilesArchiveUrlFromLocation,
buildGerritGitilesUrl,
getGerritBranchApiUrl,
getGerritCloneRepoUrl,
@@ -115,6 +116,94 @@ describe('gerrit core', () => {
});
});
describe('buildGerritGitilesArchiveUrlFromLocation', () => {
const config: GerritIntegrationConfig = {
host: 'gerrit.com',
baseUrl: 'https://gerrit.com',
gitilesBaseUrl: 'https://gerrit.com/gitiles',
};
const configWithPath: GerritIntegrationConfig = {
host: 'gerrit.com',
baseUrl: 'https://gerrit.com/gerrit',
gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles',
};
const configWithDedicatedGitiles: GerritIntegrationConfig = {
host: 'gerrit.com',
baseUrl: 'https://gerrit.com/gerrit',
gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles',
};
it('can create an archive url for a branch', () => {
expect(
buildGerritGitilesArchiveUrlFromLocation(
config,
'https://gerrit.com/gitiles/repo/+/refs/heads/dev/',
),
).toEqual(
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
);
});
it('can create an archive url for a sha', () => {
expect(
buildGerritGitilesArchiveUrlFromLocation(
config,
'https://gerrit.com/gitiles/repo/+/2846e8dc327ae2f60249983b1c3b96f42f205bae/',
),
).toEqual(
'https://gerrit.com/gitiles/repo/+archive/2846e8dc327ae2f60249983b1c3b96f42f205bae.tar.gz',
);
});
it('can create an archive url for a sha with a specific directory', () => {
expect(
buildGerritGitilesArchiveUrlFromLocation(
config,
'https://gerrit.com/gitiles/repo/+/2846e8dc327ae2f60249983b1c3b96f42f205bae/docs',
),
).toEqual(
'https://gerrit.com/gitiles/repo/+archive/2846e8dc327ae2f60249983b1c3b96f42f205bae/docs.tar.gz',
);
});
it('can create an archive url for a specific directory', () => {
expect(
buildGerritGitilesArchiveUrlFromLocation(
config,
'https://gerrit.com/gitiles/repo/+/refs/heads/dev/docs/',
),
).toEqual(
'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
);
});
it('can create an authenticated url when auth is enabled and an url-path is used', () => {
const authConfig = {
...configWithPath,
username: 'username',
password: 'password',
};
expect(
buildGerritGitilesArchiveUrlFromLocation(
authConfig,
'https://gerrit.com/gerrit/plugins/gitiles/repo/+/refs/heads/dev/docs/',
),
).toEqual(
'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
);
});
it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => {
const authConfig = {
...configWithDedicatedGitiles,
username: 'username',
password: 'password',
};
expect(() =>
buildGerritGitilesArchiveUrlFromLocation(
authConfig,
'https://gerrit.com/gitiles/repo/+/refs/heads/dev/',
),
).toThrow(
'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.',
);
});
});
describe('buildGerritGitilesUrl', () => {
it('can create an url from arguments', () => {
const config: GerritIntegrationConfig = {
@@ -426,6 +515,39 @@ describe('gerrit core', () => {
'https://gerrit.com/a/projects/web%2Fproject/branches/master/files/README.md/content',
);
});
it('can create an authenticated url for a commit.', () => {
const authConfig: GerritIntegrationConfig = {
host: 'gerrit.com',
baseUrl: 'https://gerrit.com',
gitilesBaseUrl: 'https://gerrit.com',
username: 'u',
password: 'u',
};
const authFileContentUrl = getGerritFileContentsApiUrl(
authConfig,
'https://gerrit.com/web/project/+/157f862803d45b9d269f0e390f88aece1ded51e8/README.md',
);
expect(authFileContentUrl).toEqual(
'https://gerrit.com/a/projects/web%2Fproject/commits/157f862803d45b9d269f0e390f88aece1ded51e8/files/README.md/content',
);
});
it('will throw for unsupported ref types (tag).', () => {
const authConfig: GerritIntegrationConfig = {
host: 'gerrit.com',
baseUrl: 'https://gerrit.com',
gitilesBaseUrl: 'https://gerrit.com',
username: 'u',
password: 'u',
};
expect(() =>
getGerritFileContentsApiUrl(
authConfig,
'https://gerrit.com/modules/events-broker/+/refs/tags/v3.5.6/src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApi.java',
),
).toThrow(/gitiles ref type/);
});
});
describe('parseGerritJsonResponse', () => {
+54 -7
View File
@@ -41,8 +41,9 @@ const GERRIT_BODY_PREFIX = ")]}'";
*
* @param url - An URL pointing to a file stored in git.
* @public
* @deprecated `parseGerritGitilesUrl` is deprecated. Use
* {@link parseGitilesUrlRef} instead.
*/
export function parseGerritGitilesUrl(
config: GerritIntegrationConfig,
url: string,
@@ -215,6 +216,8 @@ export function buildGerritGitilesUrl(
* @param branch - The branch we will target.
* @param filePath - The absolute file path.
* @public
* @deprecated `buildGerritGitilesArchiveUrl` is deprecated. Use
* {@link buildGerritGitilesArchiveUrlFromLocation} instead.
*/
export function buildGerritGitilesArchiveUrl(
config: GerritIntegrationConfig,
@@ -229,6 +232,38 @@ export function buildGerritGitilesArchiveUrl(
)}/${project}/+archive/refs/heads/${branch}${archiveName}`;
}
/**
* Build a Gerrit Gitiles archive url from a Gitiles url.
*
* @param config - A Gerrit provider config.
* @param url - The gitiles url
* @public
*/
export function buildGerritGitilesArchiveUrlFromLocation(
config: GerritIntegrationConfig,
url: string,
): string {
const {
path: filePath,
ref,
project,
refType,
} = parseGitilesUrlRef(config, url);
const archiveName =
filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`;
if (refType === 'branch') {
return `${getGitilesAuthenticationUrl(
config,
)}/${project}/+archive/refs/heads/${ref}${archiveName}`;
}
if (refType === 'sha') {
return `${getGitilesAuthenticationUrl(
config,
)}/${project}/+archive/${ref}${archiveName}`;
}
throw new Error(`Unsupported gitiles ref type: ${refType}`);
}
/**
* Return the authentication prefix.
*
@@ -324,13 +359,25 @@ export function getGerritFileContentsApiUrl(
config: GerritIntegrationConfig,
url: string,
) {
const { branch, filePath, project } = parseGerritGitilesUrl(config, url);
const { ref, refType, path, project } = parseGitilesUrlRef(config, url);
return `${config.baseUrl}${getAuthenticationPrefix(
config,
)}projects/${encodeURIComponent(
project,
)}/branches/${branch}/files/${encodeURIComponent(filePath)}/content`;
// https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-content
if (refType === 'branch') {
return `${config.baseUrl}${getAuthenticationPrefix(
config,
)}projects/${encodeURIComponent(
project,
)}/branches/${ref}/files/${encodeURIComponent(path)}/content`;
}
// https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-content-from-commit
if (refType === 'sha') {
return `${config.baseUrl}${getAuthenticationPrefix(
config,
)}projects/${encodeURIComponent(
project,
)}/commits/${ref}/files/${encodeURIComponent(path)}/content`;
}
throw new Error(`Unsupported gitiles ref type: ${refType}`);
}
/**
+1
View File
@@ -20,6 +20,7 @@ export {
} from './config';
export {
buildGerritGitilesArchiveUrl,
buildGerritGitilesArchiveUrlFromLocation,
getGerritBranchApiUrl,
getGerritCloneRepoUrl,
getGerritFileContentsApiUrl,
@@ -18,14 +18,14 @@ import { mockServices } from '@backstage/backend-test-utils';
import { createCloudflareAccessAuthenticator } from './authenticator';
describe('authenticator', () => {
it('createCloudflareAccessAuthenticator works', async () => {
it('works for normal users', async () => {
const auth = createCloudflareAccessAuthenticator({
cache: mockServices.cache.mock(),
});
const profile = await auth.defaultProfileTransform(
{
cfIdentity: { name: 'Name' } as any,
cfIdentity: { name: 'Name', email: 'hello@example.com' } as any,
claims: { email: 'hello@example.com' } as any,
token: 'fake',
},
@@ -38,4 +38,25 @@ describe('authenticator', () => {
},
});
});
it('works for service tokens', async () => {
const auth = createCloudflareAccessAuthenticator({
cache: mockServices.cache.mock(),
});
const profile = await auth.defaultProfileTransform(
{
cfIdentity: { name: 'Name', email: 'hello@example.com' } as any,
claims: {} as any,
token: 'fake',
},
{} as any,
);
expect(profile).toEqual({
profile: {
displayName: 'Name',
email: 'hello@example.com',
},
});
});
});
@@ -38,7 +38,7 @@ export function createCloudflareAccessAuthenticator(options?: {
async defaultProfileTransform(result: CloudflareAccessResult) {
return {
profile: {
email: result.claims.email,
email: result.cfIdentity.email,
displayName: result.cfIdentity.name,
},
};
@@ -26,14 +26,14 @@ catalog:
backstageOpenapi:
plugins:
- catalog
- todo
- events
- search
entityOverrides: # All optional
metadata:
name: 'my name'
name: 'my-name'
title: 'my title'
spec:
owner: 'my team'
owner: 'my-team'
```
We will attempt to load each plugin's OpenAPI spec hosted at `${pluginRoute}/openapi.json`. These are automatically added if you are using `@backstage/backend-openapi-utils`'s `createValidatedOpenApiRouter`.
@@ -153,7 +153,7 @@ describe('BitbucketServerClient', () => {
it('getFile', async () => {
server.use(
rest.get(
`https://${config.host}/projects/test-project/repos/test-repo/raw/catalog-info.yaml`,
`${config.apiBaseUrl}/projects/test-project/repos/test-repo/raw/catalog-info.yaml`,
(req, res, ctx) => {
if (
req.headers.get('authorization') !==
@@ -77,9 +77,8 @@ export class BitbucketServerClient {
repo: string;
path: string;
}): Promise<Response> {
const base = new URL(this.config.apiBaseUrl);
return throttledFetch(
`${base.protocol}//${base.host}/projects/${options.projectKey}/repos/${options.repo}/raw/${options.path}`,
`${this.config.apiBaseUrl}/projects/${options.projectKey}/repos/${options.repo}/raw/${options.path}`,
getBitbucketServerRequestOptions(this.config),
);
}
+28
View File
@@ -15,7 +15,35 @@
*/
import { createBackend } from '@backstage/backend-defaults';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { proxyEndpointsExtensionPoint } from '@backstage/plugin-proxy-node/alpha';
const backend = createBackend();
backend.add(import('../src/alpha'));
backend.add(
createBackendModule({
pluginId: 'proxy',
moduleId: 'demo-additional-endpoints',
register: reg => {
reg.registerInit({
deps: {
proxyEndpoints: proxyEndpointsExtensionPoint,
},
init: async ({ proxyEndpoints }) => {
proxyEndpoints.addProxyEndpoints({
...Object.fromEntries(
['foo', 'bar', 'baz'].map(msv => [
`/${msv}`,
{ target: `http://${msv}.org` },
]),
),
'/gocd': 'http://cannot-override-config.no',
});
},
});
},
}),
);
backend.start();
+1
View File
@@ -56,6 +56,7 @@
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-proxy-node": "workspace:^",
"@backstage/types": "workspace:^",
"@types/express": "^4.17.6",
"express": "^4.17.1",
+3
View File
@@ -7,6 +7,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import express from 'express';
import { Logger } from 'winston';
import { ProxyConfig } from '@backstage/plugin-proxy-node/alpha';
import { RootConfigService } from '@backstage/backend-plugin-api';
// @public @deprecated
@@ -18,6 +19,8 @@ export default proxyPlugin;
// @public @deprecated (undocumented)
export interface RouterOptions {
// (undocumented)
additionalEndpoints?: ProxyConfig;
// (undocumented)
config: RootConfigService;
// (undocumented)
+9
View File
@@ -20,6 +20,7 @@ import {
coreServices,
} from '@backstage/backend-plugin-api';
import { createRouterInternal } from './service/router';
import { proxyEndpointsExtensionPoint } from '@backstage/plugin-proxy-node/alpha';
/**
* The proxy backend plugin.
@@ -29,6 +30,13 @@ import { createRouterInternal } from './service/router';
export const proxyPlugin = createBackendPlugin({
pluginId: 'proxy',
register(env) {
const additionalEndpoints = {};
env.registerExtensionPoint(proxyEndpointsExtensionPoint, {
addProxyEndpoints(endpoints) {
Object.assign(additionalEndpoints, endpoints);
},
});
env.registerInit({
deps: {
config: coreServices.rootConfig,
@@ -42,6 +50,7 @@ export const proxyPlugin = createBackendPlugin({
discovery,
logger: loggerToWinstonLogger(logger),
httpRouterService: httpRouter,
additionalEndpoints,
});
},
});
+10 -11
View File
@@ -20,7 +20,6 @@ import Router from 'express-promise-router';
import {
createProxyMiddleware,
fixRequestBody,
Options,
RequestHandler,
} from 'http-proxy-middleware';
import { Logger } from 'winston';
@@ -31,6 +30,7 @@ import {
HttpRouterService,
RootConfigService,
} from '@backstage/backend-plugin-api';
import { ProxyConfig } from '@backstage/plugin-proxy-node/alpha';
// A list of headers that are always forwarded to the proxy targets.
const safeForwardHeaders = [
@@ -63,12 +63,7 @@ export interface RouterOptions {
discovery: DiscoveryService;
skipInvalidProxies?: boolean;
reviveConsumedRequestBodies?: boolean;
}
export interface ProxyConfig extends Options {
allowedMethods?: string[];
allowedHeaders?: string[];
reviveRequestBody?: boolean;
additionalEndpoints?: ProxyConfig;
}
// Creates a proxy middleware, possibly with defaults added on top of the
@@ -87,7 +82,7 @@ export function buildMiddleware(
fullConfig = { target: config };
credentialsPolicy = 'require';
} else {
const { credentials, ...rest } = config as any;
const { credentials, ...rest } = config;
fullConfig = rest;
credentialsPolicy = credentials ?? 'require';
}
@@ -315,7 +310,11 @@ export async function createRouterInternal(
const externalUrl = await options.discovery.getExternalBaseUrl('proxy');
const { pathname: pathPrefix } = new URL(externalUrl);
const proxyConfig = readProxyConfig(options.config, options.logger);
const proxyConfig: ProxyConfig = {
...(options.additionalEndpoints ?? {}),
...readProxyConfig(options.config, options.logger),
};
configureMiddlewares(
proxyOptions,
currentRouter,
@@ -358,10 +357,10 @@ function configureMiddlewares(
},
router: express.Router,
pathPrefix: string,
proxyConfig: any,
proxyConfig: ProxyConfig,
httpRouterService?: HttpRouterService,
) {
Object.entries<any>(proxyConfig).forEach(([route, proxyRouteConfig]) => {
Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => {
try {
router.use(
route,
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+5
View File
@@ -0,0 +1,5 @@
# backstage-plugin-proxy-node-node
Welcome to the Node.js library package for the proxy-node plugin!
_This plugin was created through the Backstage CLI_
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-proxy-node
title: '@backstage/plugin-proxy-node'
description: The plugin-proxy-node module for @backstage/plugin-proxy-backend
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
+60
View File
@@ -0,0 +1,60 @@
{
"name": "@backstage/plugin-proxy-node",
"version": "0.0.0",
"description": "The plugin-proxy-node module for @backstage/plugin-proxy-backend",
"backstage": {
"role": "node-library",
"pluginId": "proxy-backend",
"pluginPackages": [
"@backstage/plugin-proxy-node"
]
},
"publishConfig": {
"access": "public"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/proxy-node"
},
"license": "Apache-2.0",
"exports": {
"./alpha": "./src/alpha/index.ts",
"./package.json": "./package.json"
},
"main": "src/alpha/index.ts",
"types": "src/alpha/index.ts",
"typesVersions": {
"*": {
"alpha": [
"src/alpha/index.ts"
],
"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/backend-plugin-api": "workspace:^",
"http-proxy-middleware": "^2.0.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@types/http-proxy-middleware": "^1.0.0"
}
}
+31
View File
@@ -0,0 +1,31 @@
## API Report File for "@backstage/plugin-proxy-node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { Options } from 'http-proxy-middleware';
// @alpha
export interface ProxyConfig extends Options {
// (undocumented)
allowedHeaders?: string[];
// (undocumented)
allowedMethods?: string[];
// (undocumented)
credentials?: 'require' | 'forward' | 'dangerously-allow-unauthenticated';
// (undocumented)
reviveRequestBody?: boolean;
}
// @alpha
export interface ProxyEndpointsExtensionPoint {
// (undocumented)
addProxyEndpoints(endpoints: Record<string, string | ProxyConfig>): void;
}
// @alpha
export const proxyEndpointsExtensionPoint: ExtensionPoint<ProxyEndpointsExtensionPoint>;
// (No @packageDocumentation comment for this package)
```
+48
View File
@@ -0,0 +1,48 @@
/*
* 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 { createExtensionPoint } from '@backstage/backend-plugin-api';
import { Options } from 'http-proxy-middleware';
/**
* http-proxy-middleware proxy config interface.
*
* @alpha
*/
export interface ProxyConfig extends Options {
allowedMethods?: string[];
allowedHeaders?: string[];
reviveRequestBody?: boolean;
credentials?: 'require' | 'forward' | 'dangerously-allow-unauthenticated';
}
/**
* Extension point interface for managing proxy endpoints.
*
* @alpha
*/
export interface ProxyEndpointsExtensionPoint {
addProxyEndpoints(endpoints: Record<string, string | ProxyConfig>): void;
}
/**
* Extension point for managing proxy endpoints.
*
* @alpha
*/
export const proxyEndpointsExtensionPoint =
createExtensionPoint<ProxyEndpointsExtensionPoint>({
id: 'proxy.endpoints',
});
@@ -14,7 +14,13 @@ export const createGitlabGroupEnsureExistsAction: (options: {
integrations: ScmIntegrationRegistry;
}) => TemplateAction<
{
path: string[];
path: (
| string
| {
name: string;
slug: string;
}
)[];
repoUrl: string;
token?: string | undefined;
},
@@ -202,6 +208,7 @@ export const createPublishGitlabMergeRequestAction: (options: {
projectid?: string | undefined;
removeSourceBranch?: boolean | undefined;
assignee?: string | undefined;
reviewers?: string[] | undefined;
},
JsonObject
>;
@@ -196,4 +196,51 @@ describe('gitlab:group:ensureExists', () => {
expect(mockContext.output).toHaveBeenCalledWith('groupId', 42);
});
it(`Should ${examples[4].description}`, async () => {
mockGitlabClient.Groups.search.mockResolvedValue([
{
id: 1,
full_path: 'group1',
},
{
id: 2,
full_path: 'group1/group2',
},
]);
mockGitlabClient.Groups.create.mockResolvedValue({
id: 3,
full_path: 'group1/group2/group3',
});
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.com',
token: 'tokenlols',
apiBaseUrl: 'https://api.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createGitlabGroupEnsureExistsAction({ integrations });
await action.handler({
...mockContext,
input: yaml.parse(examples[4].example).steps[0].input,
});
expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith(
'Group 3',
'group3',
{
parentId: 2,
},
);
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
});
});
@@ -82,4 +82,24 @@ export const examples: TemplateExample[] = [
],
}),
},
{
description: 'Create a group nested within another group using objects',
example: yaml.stringify({
steps: [
{
id: 'gitlabGroup',
name: 'Group',
action: 'gitlab:group:ensureExists',
input: {
repoUrl: 'gitlab.com',
path: [
{ name: 'Group 1', slug: 'group1' },
{ name: 'Group 2', slug: 'group2' },
{ name: 'Group 3', slug: 'group3' },
],
},
},
],
}),
},
];
@@ -62,7 +62,7 @@ describe('gitlab:group:ensureExists', () => {
const mockContext = createMockActionContext();
it('should create a new group if it does not exists', async () => {
it('should create a new group from string if it does not exists', async () => {
mockGitlabClient.Groups.search.mockResolvedValue([
{
id: 1,
@@ -94,6 +94,45 @@ describe('gitlab:group:ensureExists', () => {
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
});
it('should create a new group from object if it does not exists', async () => {
mockGitlabClient.Groups.search.mockResolvedValue([
{
id: 1,
full_path: 'bar',
},
{
id: 2,
full_path: 'foo',
},
]);
mockGitlabClient.Groups.create.mockResolvedValue({
id: 3,
full_path: 'foo/bar',
});
await action.handler({
...mockContext,
input: {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
path: [
{ name: 'Foo', slug: 'foo' },
{ name: 'Bar is a nice name', slug: 'bar' },
],
},
});
expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith(
'Bar is a nice name',
'bar',
{
parentId: 2,
},
);
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
});
it('should return existing group if it does exists', async () => {
mockGitlabClient.Groups.search.mockResolvedValue([
{
@@ -41,9 +41,18 @@ export const createGitlabGroupEnsureExistsAction = (options: {
input: commonGitlabConfig.merge(
z.object({
path: z
.array(z.string(), {
description: 'A path of group names that is ensured to exist',
})
.array(
z.string().or(
z.object({
name: z.string(),
slug: z.string(),
}),
),
{
description:
'A path of group names or objects (name and slug) that is ensured to exist',
},
)
.min(1),
}),
),
@@ -68,9 +77,11 @@ export const createGitlabGroupEnsureExistsAction = (options: {
let currentPath: string | null = null;
let parentId: number | null = null;
for (const pathElement of path) {
const fullPath: string = currentPath
? `${currentPath}/${pathElement}`
: pathElement;
const slug =
typeof pathElement === 'string' ? pathElement : pathElement.slug;
const name =
typeof pathElement === 'string' ? pathElement : pathElement.name;
const fullPath: string = currentPath ? `${currentPath}/${slug}` : slug;
const result = (await api.Groups.search(
fullPath,
)) as unknown as Array<GroupSchema>; // recast since the return type for search is wrong in the gitbeaker typings
@@ -81,8 +92,8 @@ export const createGitlabGroupEnsureExistsAction = (options: {
ctx.logger.info(`creating missing group ${fullPath}`);
parentId = (
await api.Groups.create(
pathElement,
pathElement,
name,
slug,
parentId
? {
parentId: parentId,
@@ -42,6 +42,58 @@ const mockGitlabClient = {
default_branch: 'main',
};
}),
show: jest.fn(async (_: any) => {
return {
default_branch: 'main',
};
}),
edit: jest.fn(async (_: any) => {
return {
default_branch: 'main',
};
}),
},
MergeRequestApprovals: {
allApprovalRules: jest.fn(async (_: any) => {
return [
{
id: 123,
name: 'rule1',
rule_type: 'regular',
eligible_approvers: [
{
id: 123,
username: 'John Smith',
},
{
id: 456,
username: 'Jane Doe',
},
],
approvals_required: 1,
users: [],
contains_hidden_groups: false,
report_type: null,
section: null,
source_rule: { approvals_required: 1 },
overridden: false,
},
{
id: 456,
name: 'All Members',
rule_type: 'any_approver',
eligible_approvers: [],
approvals_required: 1,
users: [],
groups: [],
contains_hidden_groups: false,
report_type: null,
section: null,
source_rule: { approvals_required: 1 },
overridden: false,
},
];
}),
},
Projects: {
create: jest.fn(),
@@ -51,11 +51,86 @@ const mockGitlabClient = {
create: jest.fn(),
},
MergeRequests: {
create: jest.fn(async (_: any) => {
create: jest.fn(async (repoId: string) => {
if (repoId === 'owner/repo-without-approvals') {
return {
iid: 5,
};
}
return {
default_branch: 'main',
iid: 4,
};
}),
show: jest.fn(async (repoId: string, iid: number) => {
if (repoId === 'owner/repo' && iid === 4) {
return {
iid: 4,
};
} else if (repoId === 'owner/repo-without-approvals' && iid === 5) {
return {
iid: 5,
};
}
return {
default_branch: 'main',
};
}),
edit: jest.fn(async (_: any) => {
return {
default_branch: 'main',
};
}),
},
MergeRequestApprovals: {
allApprovalRules: jest.fn(
async (repoId: string, options: { mergerequestIId: number }) => {
if (
repoId === 'owner/repo-without-approvals' &&
options.mergerequestIId === 5
) {
return [];
}
return [
{
id: 123,
name: 'rule1',
rule_type: 'regular',
eligible_approvers: [
{
id: 234,
username: 'Bob Vance',
},
{
id: 345,
username: 'Dina Fox',
},
],
approvals_required: 1,
users: [],
contains_hidden_groups: false,
report_type: null,
section: null,
source_rule: { approvals_required: 1 },
overridden: false,
},
{
id: 456,
name: 'All Members',
rule_type: 'any_approver',
eligible_approvers: [],
approvals_required: 1,
users: [],
groups: [],
contains_hidden_groups: false,
report_type: null,
section: null,
source_rule: { approvals_required: 1 },
overridden: false,
},
];
},
),
},
Projects: {
create: jest.fn(),
@@ -67,13 +142,28 @@ const mockGitlabClient = {
},
Users: {
all: jest.fn(async (userOptions: { username: string }) => {
if (userOptions.username !== 'John Smith')
throw new Error('user does not exist');
return [
{
id: 123,
},
];
switch (userOptions.username) {
case 'John Smith':
return [
{
id: 123,
},
];
case 'Bob Vance':
return [
{
id: 234,
},
];
case 'Jane Doe':
return [
{
id: 456,
},
];
default:
throw new Error('user does not exist');
}
}),
},
Repositories: {
@@ -517,6 +607,236 @@ describe('createGitLabMergeRequest', () => {
});
});
describe('createGitlabMergeRequestWithReviewers', () => {
it('no reviewers are set when a no reviewer are passed in options', async () => {
const input = {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'This is an important change',
removeSourceBranch: false,
targetPath: 'Subdirectory',
assignee: 'John Smith',
};
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
);
expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled();
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
'Create my new MR',
{
description: 'This is an important change',
removeSourceBranch: false,
assigneeId: 123,
},
);
expect(
mockGitlabClient.MergeRequestApprovals.allApprovalRules,
).toHaveBeenCalled();
expect(mockGitlabClient.MergeRequests.edit).toHaveBeenCalledWith(
'owner/repo',
4,
{
reviewerIds: [234, 345], // Approval Rule Members
},
);
});
it('reviewer is set correcly when a valid reviewer username is passed in options in combination with MR approval rules', async () => {
const input = {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'This is an important change',
removeSourceBranch: false,
targetPath: 'Subdirectory',
assignee: 'John Smith',
reviewers: ['Jane Doe', 'Bob Vance'],
};
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
);
expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled();
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
'Create my new MR',
{
description: 'This is an important change',
removeSourceBranch: false,
assigneeId: 123,
reviewerIds: [456, 234], // Jane Doe and Bob Vance
},
);
expect(
mockGitlabClient.MergeRequestApprovals.allApprovalRules,
).toHaveBeenCalled();
expect(mockGitlabClient.MergeRequests.edit).toHaveBeenCalledWith(
'owner/repo',
4,
{
reviewerIds: [234, 345, 456], // Approval Rule Members + Jane Doe (individual reviewer) but no duplicates (Bob Vance)
},
);
});
it('reviewer is set correcly when a valid reviewer username is passed in options and no MR rules exist', async () => {
const input = {
repoUrl: 'gitlab.com?repo=repo-without-approvals&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'This is an important change',
removeSourceBranch: false,
targetPath: 'Subdirectory',
assignee: 'John Smith',
reviewers: ['Jane Doe', 'Bob Vance'],
};
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith(
'owner/repo-without-approvals',
'new-mr',
'main',
);
expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled();
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo-without-approvals',
'new-mr',
'main',
'Create my new MR',
{
description: 'This is an important change',
removeSourceBranch: false,
assigneeId: 123,
reviewerIds: [456, 234], // Jane Doe and Bob Vance
},
);
expect(
mockGitlabClient.MergeRequestApprovals.allApprovalRules,
).toHaveBeenCalledWith('owner/repo-without-approvals', {
mergerequestIId: 5,
});
expect(mockGitlabClient.MergeRequests.edit).not.toHaveBeenCalled();
});
it('reviewers are set correcly when valid reviewers username are passed in options', async () => {
const input = {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'This is an important change',
removeSourceBranch: false,
targetPath: 'Subdirectory',
assignee: 'John Smith',
reviewers: ['Jane Doe', 'John Smith'],
};
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
);
expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled();
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
'Create my new MR',
{
description: 'This is an important change',
removeSourceBranch: false,
assigneeId: 123,
reviewerIds: [456, 123],
},
);
});
it('assignee is not set when a valid assignee username is not passed in options', async () => {
const input = {
repoUrl: 'gitlab.com?repo=repo&owner=owner',
title: 'Create my new MR',
branchName: 'new-mr',
description: 'This is an important change',
removeSourceBranch: false,
targetPath: 'Subdirectory',
reviewers: ['John Doe'],
};
mockDir.setContent({
[workspacePath]: {
source: { 'foo.txt': 'Hello there!' },
irrelevant: { 'bar.txt': 'Nothing to see here' },
},
});
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
);
expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled();
expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith(
'owner/repo',
'new-mr',
'main',
'Create my new MR',
{
description: 'This is an important change',
removeSourceBranch: false,
assigneeId: undefined,
reviewerIds: [],
},
);
});
});
describe('createGitLabMergeRequestWithoutCommitAction', () => {
it('default commitAction is auto', async () => {
const input = {
@@ -20,7 +20,12 @@ import {
SerializedFile,
serializeDirectoryContents,
} from '@backstage/plugin-scaffolder-node';
import { Gitlab, RepositoryTreeSchema, CommitAction } from '@gitbeaker/rest';
import {
Gitlab,
RepositoryTreeSchema,
CommitAction,
SimpleUserSchema,
} from '@gitbeaker/rest';
import path from 'path';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { InputError } from '@backstage/errors';
@@ -100,6 +105,7 @@ export const createPublishGitlabMergeRequestAction = (options: {
projectid?: string;
removeSourceBranch?: boolean;
assignee?: string;
reviewers?: string[];
}>({
id: 'publish:gitlab:merge-request',
examples,
@@ -179,6 +185,14 @@ which uses additional API calls in order to detect whether to 'create', 'update'
type: 'string',
description: 'User this merge request will be assigned to',
},
reviewers: {
title: 'Merge Request Reviewers',
type: 'array',
items: {
type: 'string',
},
description: 'Users that will be assigned as reviewers',
},
},
},
output: {
@@ -207,6 +221,7 @@ which uses additional API calls in order to detect whether to 'create', 'update'
async handler(ctx) {
const {
assignee,
reviewers,
branchName,
targetBranchName,
description,
@@ -242,6 +257,27 @@ which uses additional API calls in order to detect whether to 'create', 'update'
}
}
let reviewerIds: number[] | undefined = undefined; // Explicitly set to undefined. Strangely, passing an empty array to the API will result the other options being undefined also being explicity passed to the Gitlab API call (e.g. assigneeId)
if (reviewers !== undefined) {
reviewerIds = (
await Promise.all(
reviewers.map(async reviewer => {
try {
const reviewerUser = await api.Users.all({
username: reviewer,
});
return reviewerUser[0].id;
} catch (e) {
ctx.logger.warn(
`Failed to find gitlab user id for ${reviewer}: ${e}. Proceeding with MR creation without reviewer.`,
);
return undefined;
}
}),
)
).filter(id => id !== undefined) as number[];
}
let fileRoot: string;
if (sourcePath) {
fileRoot = resolveSafeChildPath(ctx.workspacePath, sourcePath);
@@ -355,7 +391,7 @@ which uses additional API calls in order to detect whether to 'create', 'update'
}
}
try {
const mergeRequestUrl = await api.MergeRequests.create(
let mergeRequest = await api.MergeRequests.create(
repoID,
branchName,
String(targetBranch),
@@ -364,12 +400,59 @@ which uses additional API calls in order to detect whether to 'create', 'update'
description,
removeSourceBranch: removeSourceBranch ? removeSourceBranch : false,
assigneeId,
reviewerIds,
},
).then(mergeRequest => mergeRequest.web_url ?? mergeRequest.webUrl);
);
// Because we don't know the code owners before the MR is created, we can't check the approval rules beforehand.
// Getting the approval rules beforehand is very difficult, especially, because of the inheritance rules for groups.
// Code owners take a moment to be processed and added to the approval rules after the MR is created.
while (
mergeRequest.detailed_merge_status === 'preparing' ||
mergeRequest.detailed_merge_status === 'approvals_syncing' ||
mergeRequest.detailed_merge_status === 'checking'
) {
mergeRequest = await api.MergeRequests.show(repoID, mergeRequest.iid);
ctx.logger.info(`${mergeRequest.detailed_merge_status}`);
}
const approvalRules = await api.MergeRequestApprovals.allApprovalRules(
repoID,
{
mergerequestIId: mergeRequest.iid,
},
);
if (approvalRules.length !== 0) {
const eligibleApprovers = approvalRules
.filter(rule => rule.eligible_approvers !== undefined)
.map(rule => {
return rule.eligible_approvers as SimpleUserSchema[];
})
.flat();
const eligibleUserIds = new Set([
...eligibleApprovers.map(user => user.id),
...(reviewerIds ?? []),
]);
mergeRequest = await api.MergeRequests.edit(
repoID,
mergeRequest.iid,
{
reviewerIds: Array.from(eligibleUserIds),
},
);
}
ctx.output('projectid', repoID);
ctx.output('targetBranchName', targetBranch);
ctx.output('projectPath', repoID);
ctx.output('mergeRequestUrl', mergeRequestUrl);
ctx.output(
'mergeRequestUrl',
mergeRequest.web_url ?? mergeRequest.webUrl,
);
} catch (e) {
throw new InputError(
`Merge request creation failed. ${getErrorMessage(e)}`,
@@ -0,0 +1,89 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { createGitlabProjectMigrateAction } from './gitlabProjectMigrate';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { examples } from './gitlabProjectMigrate.examples';
import yaml from 'yaml';
const mockGitlabClient = {
Migrations: {
create: jest.fn(),
},
};
jest.mock('@gitbeaker/rest', () => ({
Gitlab: class {
constructor() {
return mockGitlabClient;
}
},
}));
describe('gitlab:group:migrate', () => {
let instance: TemplateAction<any>;
beforeEach(() => {
jest.clearAllMocks();
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.local.com',
token: 'token',
apiBaseUrl: 'https://api.gitlab.local.com',
},
{
host: 'hosted.gitlab.com',
apiBaseUrl: 'https://api.hosted.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
instance = createGitlabProjectMigrateAction({ integrations });
});
describe('Migrate a gitlab project to a different gitlab instance in another', () => {
const workspacePath = createMockDirectory().resolve('workspace');
it(`Execute example ${examples[0].description}`, async () => {
const input = yaml.parse(examples[0].example).steps[0].input;
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Migrations.create).toHaveBeenCalledWith(
{
url: 'https://gitlab.remote.com',
access_token: 'lolstoken',
},
[
{
sourceType: 'project_entity',
sourceFullPath: 'my/wonderful-repo',
destinationSlug: 'precious-repo',
destinationNamespace: 'my/local/site',
},
],
);
});
});
});
@@ -0,0 +1,40 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TemplateExample } from '@backstage/plugin-scaffolder-node';
import yaml from 'yaml';
export const examples: TemplateExample[] = [
{
description: 'Import repo from remote GitLab instance',
example: yaml.stringify({
steps: [
{
id: 'importGitLabRepo',
action: 'gitlab:repo:push',
name: 'Push changes to gitlab repository',
input: {
destinationAccessToken: 'tokenslols',
destinationUrl:
'gitlab.local.com?repo=precious-repo&owner=my%2Flocal%2Fsite',
sourceAccessToken: 'lolstoken',
sourceFullPath: 'my/wonderful-repo',
sourceUrl: 'https://gitlab.remote.com',
},
},
],
}),
},
];
@@ -0,0 +1,94 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { createGitlabProjectMigrateAction } from './gitlabProjectMigrate';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { createMockDirectory } from '@backstage/backend-test-utils';
const mockGitlabClient = {
Migrations: {
create: jest.fn(),
},
};
jest.mock('@gitbeaker/rest', () => ({
Gitlab: class {
constructor() {
return mockGitlabClient;
}
},
}));
describe('createGitlabRepoMigrate', () => {
let instance: TemplateAction<any>;
beforeEach(() => {
jest.clearAllMocks();
const config = new ConfigReader({
integrations: {
gitlab: [
{
host: 'gitlab.com',
token: 'token',
apiBaseUrl: 'https://api.gitlab.com',
},
{
host: 'hosted.gitlab.com',
apiBaseUrl: 'https://api.hosted.gitlab.com',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
instance = createGitlabProjectMigrateAction({ integrations });
});
describe('createGitlabRepoImportAction', () => {
const workspacePath = createMockDirectory().resolve('workspace');
it('default repo import action is created', async () => {
const input = {
destinationAccessToken: 'moreLOLsToken',
destinationUrl:
'gitlab.com?owner=migrated%2Ffoo%2Fbar&repo=migrated-go-lang',
sourceUrl: 'https://gitlab.remote.com',
sourceAccessToken: 'lolstoken',
sourceFullPath: 'foo/bar/go-lang',
};
const ctx = createMockActionContext({ input, workspacePath });
await instance.handler(ctx);
expect(mockGitlabClient.Migrations.create).toHaveBeenCalledWith(
{
url: 'https://gitlab.remote.com',
access_token: 'lolstoken',
},
[
{
sourceType: 'project_entity',
sourceFullPath: 'foo/bar/go-lang',
destinationSlug: 'migrated-go-lang',
destinationNamespace: 'migrated/foo/bar',
},
],
);
});
});
});
@@ -0,0 +1,151 @@
/*
* 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 {
createTemplateAction,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { InputError } from '@backstage/errors';
import { createGitlabApi } from './helpers';
import { examples } from './gitlabRepoPush.examples';
import { MigrationEntityOptions } from '@gitbeaker/rest';
/**
* Create a new action that imports a gitlab project into another gitlab project (potentially from another gitlab instance).
*
* @public
*/
export const createGitlabProjectMigrateAction = (options: {
integrations: ScmIntegrationRegistry;
}) => {
const { integrations } = options;
return createTemplateAction<{
destinationAccessToken: string;
destinationUrl: string;
sourceAccessToken: string;
sourceFullPath: string;
sourceUrl: string;
}>({
id: 'gitlab:group:migrate',
examples,
schema: {
input: {
required: [
'destinationAccessToken',
'destinationUrl',
'sourceAccessToken',
'sourceFullPath',
'sourceUrl',
],
type: 'object',
properties: {
destinationAccessToken: {
type: 'string',
title: 'Target Repository Access Token',
description: `The token to use for authorization to the target GitLab'`,
},
destinationUrl: {
type: 'string',
title: 'Target Project Location',
description: `Accepts the format 'gitlab.com?repo=project_name&owner=group_name' where 'project_name' is the repository name and 'group_name' is a group or username`,
},
sourceAccessToken: {
type: 'string',
title: 'Source Group Access Token',
description: `The token to use for authorization to the source GitLab'`,
},
sourceFullPath: {
type: 'string',
title: 'Group Full Path',
description:
'Full path to the project in the source Gitlab instance',
},
sourceUrl: {
type: 'string',
title: 'Source URL Location',
description: `Accepts the format 'https://gitlab.com/'`,
},
},
},
output: {
type: 'object',
properties: {
importedRepoUrl: {
title: 'URL to the newly imported repo',
type: 'string',
},
},
},
},
async handler(ctx) {
const {
destinationAccessToken,
destinationUrl,
sourceAccessToken,
sourceFullPath,
sourceUrl,
} = ctx.input;
const {
host: destinationHost,
repo: destinationSlug,
owner: destinationNamespace,
} = parseRepoUrl(destinationUrl, integrations);
if (!destinationNamespace) {
throw new InputError(
`Failed to determine target repository to migrate to. Make sure destinationUrl matches the format 'gitlab.myorg.com?repo=project_name&owner=group_name'`,
);
}
const api = createGitlabApi({
integrations,
token: destinationAccessToken,
repoUrl: destinationUrl,
});
const migrationEntity: MigrationEntityOptions[] = [
{
sourceType: 'project_entity',
sourceFullPath: sourceFullPath,
destinationSlug: destinationSlug,
destinationNamespace: destinationNamespace,
},
];
const sourceConfig = {
url: sourceUrl,
access_token: sourceAccessToken,
};
try {
await api.Migrations.create(sourceConfig, migrationEntity);
} catch (e: any) {
throw new InputError(
`Failed to transfer repo ${sourceFullPath}. Make sure that ${sourceFullPath} exists in ${sourceUrl}, and token has enough rights.\nError: ${e}`,
);
}
ctx.output(
'importedRepoUrl',
`${destinationHost}/${destinationNamespace}/${destinationSlug}`,
);
},
});
};
@@ -34,6 +34,7 @@ import {
createTriggerGitlabPipelineAction,
editGitlabIssueAction,
} from './actions';
import { createGitlabProjectMigrateAction } from './actions/gitlabProjectMigrate';
import { createHandleAutocompleteRequest } from './autocomplete/autocomplete';
/**
@@ -55,6 +56,7 @@ export const gitlabModule = createBackendModule({
scaffolder.addActions(
createGitlabGroupEnsureExistsAction({ integrations }),
createGitlabProjectMigrateAction({ integrations }),
createGitlabIssueAction({ integrations }),
createGitlabProjectAccessTokenAction({ integrations }),
createGitlabProjectDeployTokenAction({ integrations }),
+1
View File
@@ -358,6 +358,7 @@ export const createPublishGitlabMergeRequestAction: (options: {
projectid?: string | undefined;
removeSourceBranch?: boolean | undefined;
assignee?: string | undefined;
reviewers?: string[] | undefined;
},
JsonObject
>;
@@ -132,6 +132,30 @@ export const Form: (
props: PropsWithChildren<ScaffolderRJSFFormProps>,
) => React_2.JSX.Element;
// @alpha
export const FormDecoratorBlueprint: ExtensionBlueprint<{
kind: 'scaffolder-form-decorator';
name: undefined;
params: {
decorator: ScaffolderFormDecorator;
};
output: ConfigurableExtensionDataRef<
ScaffolderFormDecorator,
'scaffolder.form-decorator-loader',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
formDecoratorLoader: ConfigurableExtensionDataRef<
ScaffolderFormDecorator,
'scaffolder.form-decorator-loader',
{}
>;
};
}>;
// @alpha
export const FormFieldBlueprint: ExtensionBlueprint<{
kind: 'scaffolder-form-field';
@@ -0,0 +1,41 @@
/*
* 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 {
createExtensionBlueprint,
createExtensionDataRef,
} from '@backstage/frontend-plugin-api';
import { ScaffolderFormDecorator } from '../extensions';
const formDecoratorExtensionDataRef =
createExtensionDataRef<ScaffolderFormDecorator>().with({
id: 'scaffolder.form-decorator-loader',
});
/**
* @alpha
* Creates extensions that are Field Extensions for the Scaffolder
* */
export const FormDecoratorBlueprint = createExtensionBlueprint({
kind: 'scaffolder-form-decorator',
attachTo: { id: 'api:scaffolder/form-decorators', input: 'formDecorators' },
dataRefs: {
formDecoratorLoader: formDecoratorExtensionDataRef,
},
output: [formDecoratorExtensionDataRef],
*factory(params: { decorator: ScaffolderFormDecorator }) {
yield formDecoratorExtensionDataRef(params.decorator);
},
});
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export * from './FormDecoratorBlueprint';
export * from './FormFieldBlueprint';
export * from './types';
+52
View File
@@ -80,6 +80,33 @@ const _default: FrontendPlugin<
factory: AnyApiFactory;
};
}>;
'api:scaffolder/form-decorators': ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {
formDecorators: ExtensionInput<
ConfigurableExtensionDataRef<
ScaffolderFormDecorator,
'scaffolder.form-decorator-loader',
{}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: 'api';
name: 'form-decorators';
params: {
factory: AnyApiFactory;
};
}>;
'page:scaffolder': ExtensionDefinition<{
kind: 'page';
name: undefined;
@@ -173,6 +200,31 @@ export class DefaultScaffolderFormDecoratorsApi
getFormDecorators(): Promise<ScaffolderFormDecorator[]>;
}
// @alpha (undocumented)
export const formDecoratorsApi: ExtensionDefinition<{
config: {};
configInput: {};
output: ConfigurableExtensionDataRef<AnyApiFactory, 'core.api.factory', {}>;
inputs: {
formDecorators: ExtensionInput<
ConfigurableExtensionDataRef<
ScaffolderFormDecorator,
'scaffolder.form-decorator-loader',
{}
>,
{
singleton: false;
optional: false;
}
>;
};
kind: 'api';
name: 'form-decorators';
params: {
factory: AnyApiFactory;
};
}>;
// @alpha (undocumented)
export const formDecoratorsApiRef: ApiRef<ScaffolderFormDecoratorsApi>;
@@ -14,8 +14,15 @@
* limitations under the License.
*/
import {
ApiBlueprint,
createApiFactory,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import { ScaffolderFormDecoratorsApi } from './types';
import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
import { formDecoratorsApiRef } from './ref';
import { FormDecoratorBlueprint } from '@backstage/plugin-scaffolder-react/alpha';
/** @alpha */
export class DefaultScaffolderFormDecoratorsApi
@@ -37,3 +44,29 @@ export class DefaultScaffolderFormDecoratorsApi
return this.options.decorators;
}
}
/** @alpha */
export const formDecoratorsApi = ApiBlueprint.makeWithOverrides({
name: 'form-decorators',
inputs: {
formDecorators: createExtensionInput([
FormDecoratorBlueprint.dataRefs.formDecoratorLoader,
]),
},
factory(originalFactory, { inputs }) {
const formDecorators = inputs.formDecorators.map(e =>
e.get(FormDecoratorBlueprint.dataRefs.formDecoratorLoader),
);
return originalFactory({
factory: createApiFactory({
api: formDecoratorsApiRef,
deps: {},
factory: () =>
DefaultScaffolderFormDecoratorsApi.create({
decorators: formDecorators,
}),
}),
});
},
});
+1 -1
View File
@@ -16,4 +16,4 @@
export { formDecoratorsApiRef } from './ref';
export type { ScaffolderFormDecoratorsApi } from './types';
export { DefaultScaffolderFormDecoratorsApi } from './FormDecoratorsApi';
export * from './FormDecoratorsApi';
+2
View File
@@ -33,6 +33,7 @@ import {
scaffolderApi,
} from './extensions';
import { formFieldsApi } from '@backstage/plugin-scaffolder-react/alpha';
import { formDecoratorsApi } from './api';
/** @alpha */
export default createFrontendPlugin({
@@ -53,6 +54,7 @@ export default createFrontendPlugin({
scaffolderApi,
scaffolderPage,
scaffolderNavItem,
formDecoratorsApi,
formFieldsApi,
repoUrlPickerFormField,
],
+4 -4
View File
@@ -2689,15 +2689,15 @@ __metadata:
linkType: hard
"eslint-plugin-storybook@npm:^0.11.0":
version: 0.11.1
resolution: "eslint-plugin-storybook@npm:0.11.1"
version: 0.11.2
resolution: "eslint-plugin-storybook@npm:0.11.2"
dependencies:
"@storybook/csf": ^0.1.11
"@typescript-eslint/utils": ^8.8.1
ts-dedent: ^2.2.0
peerDependencies:
eslint: ">=6"
checksum: 8b8eb30b598f3c44c2bbf921e318215338f1159c1fba2d2f6cd5bc0b2ec14515655cf1760b5e11355baddabc2ac8446d4dc18ee6df6d2252555b126ff649a421
eslint: ">=8"
checksum: 04dab47b676db57aa6c2325d4ab66283f5970133a2615cc68a97e42cb4ae1c79317e6db73bcfc24468e146bdff4c9ffe90a368bf2bcb67593954485f058b33ca
languageName: node
linkType: hard
+55 -358
View File
@@ -1872,7 +1872,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.19.6, @babel/core@npm:^7.24.7, @babel/core@npm:^7.26.0":
"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.19.6, @babel/core@npm:^7.24.7":
version: 7.26.0
resolution: "@babel/core@npm:7.26.0"
dependencies:
@@ -2956,28 +2956,6 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-react-jsx-self@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.25.9"
dependencies:
"@babel/helper-plugin-utils": ^7.25.9
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 41c833cd7f91b1432710f91b1325706e57979b2e8da44e83d86312c78bbe96cd9ef778b4e79e4e17ab25fa32c72b909f2be7f28e876779ede28e27506c41f4ae
languageName: node
linkType: hard
"@babel/plugin-transform-react-jsx-source@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-react-jsx-source@npm:7.25.9"
dependencies:
"@babel/helper-plugin-utils": ^7.25.9
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: a3e0e5672e344e9d01fb20b504fe29a84918eaa70cec512c4d4b1b035f72803261257343d8e93673365b72c371f35cf34bb0d129720bf178a4c87812c8b9c662
languageName: node
linkType: hard
"@babel/plugin-transform-react-jsx@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/plugin-transform-react-jsx@npm:7.22.5"
@@ -4013,7 +3991,6 @@ __metadata:
"@types/yarnpkg__lockfile": ^1.1.4
"@typescript-eslint/eslint-plugin": ^8.17.0
"@typescript-eslint/parser": ^8.16.0
"@vitejs/plugin-react": ^4.3.1
"@yarnpkg/lockfile": ^1.1.0
"@yarnpkg/parsers": ^3.0.0
bfj: ^8.0.0
@@ -4086,9 +4063,6 @@ __metadata:
terser-webpack-plugin: ^5.1.3
ts-morph: ^24.0.0
util: ^0.12.3
vite: ^5.0.0
vite-plugin-html: ^3.2.2
vite-plugin-node-polyfills: ^0.22.0
webpack: ^5.94.0
webpack-dev-server: ^5.0.0
yaml: ^2.0.0
@@ -4097,31 +4071,16 @@ __metadata:
yn: ^4.0.0
zod: ^3.22.4
peerDependencies:
"@modyfi/vite-plugin-yaml": ^1.1.0
"@rspack/core": ^1.0.10
"@rspack/dev-server": ^1.0.9
"@rspack/plugin-react-refresh": ^1.0.0
"@vitejs/plugin-react": ^4.0.4
vite: ^5.0.0
vite-plugin-html: ^3.2.0
vite-plugin-node-polyfills: ^0.22.0
peerDependenciesMeta:
"@modyfi/vite-plugin-yaml":
optional: true
"@rspack/core":
optional: true
"@rspack/dev-server":
optional: true
"@rspack/plugin-react-refresh":
optional: true
"@vitejs/plugin-react":
optional: true
vite:
optional: true
vite-plugin-html:
optional: true
vite-plugin-node-polyfills:
optional: true
bin:
backstage-cli: bin/backstage-cli
languageName: unknown
@@ -7511,6 +7470,7 @@ __metadata:
"@backstage/config": "workspace:^"
"@backstage/config-loader": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-proxy-node": "workspace:^"
"@backstage/types": "workspace:^"
"@types/express": ^4.17.6
"@types/http-proxy-middleware": ^1.0.0
@@ -7528,6 +7488,19 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-proxy-node@workspace:^, @backstage/plugin-proxy-node@workspace:plugins/proxy-node":
version: 0.0.0-use.local
resolution: "@backstage/plugin-proxy-node@workspace:plugins/proxy-node"
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@types/http-proxy-middleware": ^1.0.0
http-proxy-middleware: ^2.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-azure@workspace:^, @backstage/plugin-scaffolder-backend-module-azure@workspace:plugins/scaffolder-backend-module-azure":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-azure@workspace:plugins/scaffolder-backend-module-azure"
@@ -12174,19 +12147,6 @@ __metadata:
languageName: node
linkType: hard
"@modyfi/vite-plugin-yaml@npm:^1.1.0":
version: 1.1.0
resolution: "@modyfi/vite-plugin-yaml@npm:1.1.0"
dependencies:
"@rollup/pluginutils": 5.1.0
js-yaml: 4.1.0
tosource: 2.0.0-alpha.3
peerDependencies:
vite: ^3.2.7 || ^4.0.5 || ^5.0.5
checksum: 6989cde89323321b714b69d11b9125c8d3483a8c6ace536df313edd653180e85e6013effbc6cb8b89e6f9d6e107a10f7a82fadf6a44e81255b437702d5dd1db0
languageName: node
linkType: hard
"@motionone/animation@npm:^10.12.0":
version: 10.16.3
resolution: "@motionone/animation@npm:10.16.3"
@@ -15828,22 +15788,6 @@ __metadata:
languageName: node
linkType: hard
"@rollup/plugin-inject@npm:^5.0.5":
version: 5.0.5
resolution: "@rollup/plugin-inject@npm:5.0.5"
dependencies:
"@rollup/pluginutils": ^5.0.1
estree-walker: ^2.0.2
magic-string: ^0.30.3
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
checksum: 22cb772fd6f7178308b2ece95cdde5f8615f6257197832166294552a7e4c0d3976dc996cbfa6470af3151d8b86c00091aa93da5f4db6ec563f11b6db29fd1b63
languageName: node
linkType: hard
"@rollup/plugin-json@npm:^6.0.0":
version: 6.1.0
resolution: "@rollup/plugin-json@npm:6.1.0"
@@ -15892,7 +15836,17 @@ __metadata:
languageName: node
linkType: hard
"@rollup/pluginutils@npm:5.1.0, @rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0":
"@rollup/pluginutils@npm:^4.2.1":
version: 4.2.1
resolution: "@rollup/pluginutils@npm:4.2.1"
dependencies:
estree-walker: ^2.0.1
picomatch: ^2.2.2
checksum: 6bc41f22b1a0f1efec3043899e4d3b6b1497b3dea4d94292d8f83b4cf07a1073ecbaedd562a22d11913ff7659f459677b01b09e9598a98936e746780ecc93a12
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0":
version: 5.1.0
resolution: "@rollup/pluginutils@npm:5.1.0"
dependencies:
@@ -15908,16 +15862,6 @@ __metadata:
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^4.2.0, @rollup/pluginutils@npm:^4.2.1":
version: 4.2.1
resolution: "@rollup/pluginutils@npm:4.2.1"
dependencies:
estree-walker: ^2.0.1
picomatch: ^2.2.2
checksum: 6bc41f22b1a0f1efec3043899e4d3b6b1497b3dea4d94292d8f83b4cf07a1073ecbaedd562a22d11913ff7659f459677b01b09e9598a98936e746780ecc93a12
languageName: node
linkType: hard
"@rollup/rollup-android-arm-eabi@npm:4.27.4":
version: 4.27.4
resolution: "@rollup/rollup-android-arm-eabi@npm:4.27.4"
@@ -19037,7 +18981,7 @@ __metadata:
languageName: node
linkType: hard
"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0, @types/babel__core@npm:^7.20.5":
"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0":
version: 7.20.5
resolution: "@types/babel__core@npm:7.20.5"
dependencies:
@@ -21379,21 +21323,6 @@ __metadata:
languageName: node
linkType: hard
"@vitejs/plugin-react@npm:^4.3.1":
version: 4.3.4
resolution: "@vitejs/plugin-react@npm:4.3.4"
dependencies:
"@babel/core": ^7.26.0
"@babel/plugin-transform-react-jsx-self": ^7.25.9
"@babel/plugin-transform-react-jsx-source": ^7.25.9
"@types/babel__core": ^7.20.5
react-refresh: ^0.14.2
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
checksum: d417f40d9259a1d5193152f7d9fee081d5bf41cbeef9662ae1123ccc1e26aa4b6b04bc82ebb8c4fbfde9516a746fb3af7da19fdd449819c30f0631daaa10a44b
languageName: node
linkType: hard
"@vitest/expect@npm:2.0.5":
version: 2.0.5
resolution: "@vitest/expect@npm:2.0.5"
@@ -23101,19 +23030,6 @@ __metadata:
languageName: node
linkType: hard
"assert@npm:^2.0.0":
version: 2.1.0
resolution: "assert@npm:2.1.0"
dependencies:
call-bind: ^1.0.2
is-nan: ^1.3.2
object-is: ^1.1.5
object.assign: ^4.1.4
util: ^0.12.5
checksum: 1ed1cabba9abe55f4109b3f7292b4e4f3cf2953aad8dc148c0b3c3bd676675c31b1abb32ef563b7d5a19d1715bf90d1e5f09fad2a4ee655199468902da80f7c2
languageName: node
linkType: hard
"assertion-error@npm:^2.0.1":
version: 2.0.1
resolution: "assertion-error@npm:2.0.1"
@@ -24000,15 +23916,6 @@ __metadata:
languageName: node
linkType: hard
"browser-resolve@npm:^2.0.0":
version: 2.0.0
resolution: "browser-resolve@npm:2.0.0"
dependencies:
resolve: ^1.17.0
checksum: 69225e73b555bd6d2a08fb93c7342cfcf3b5058b975099c52649cd5c3cec84c2066c5385084d190faedfb849684d9dabe10129f0cd401d1883572f2e6650f440
languageName: node
linkType: hard
"browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4":
version: 1.2.0
resolution: "browserify-aes@npm:1.2.0"
@@ -24177,7 +24084,7 @@ __metadata:
languageName: node
linkType: hard
"buffer@npm:^5.5.0, buffer@npm:^5.7.1":
"buffer@npm:^5.5.0":
version: 5.7.1
resolution: "buffer@npm:5.7.1"
dependencies:
@@ -24428,7 +24335,7 @@ __metadata:
languageName: node
linkType: hard
"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8":
"call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8":
version: 1.0.8
resolution: "call-bind@npm:1.0.8"
dependencies:
@@ -25204,7 +25111,7 @@ __metadata:
languageName: node
linkType: hard
"colorette@npm:^2.0.10, colorette@npm:^2.0.16, colorette@npm:^2.0.20":
"colorette@npm:^2.0.10, colorette@npm:^2.0.20":
version: 2.0.20
resolution: "colorette@npm:2.0.20"
checksum: 0c016fea2b91b733eb9f4bcdb580018f52c0bc0979443dad930e5037a968237ac53d9beb98e218d2e9235834f8eebce7f8e080422d6194e957454255bde71d3d
@@ -25506,13 +25413,6 @@ __metadata:
languageName: node
linkType: hard
"connect-history-api-fallback@npm:^1.6.0":
version: 1.6.0
resolution: "connect-history-api-fallback@npm:1.6.0"
checksum: 804ca2be28c999032ecd37a9f71405e5d7b7a4b3defcebbe41077bb8c5a0a150d7b59f51dcc33b2de30bc7e217a31d10f8cfad27e8e74c2fc7655eeba82d6e7e
languageName: node
linkType: hard
"connect-history-api-fallback@npm:^2.0.0":
version: 2.0.0
resolution: "connect-history-api-fallback@npm:2.0.0"
@@ -25542,7 +25442,7 @@ __metadata:
languageName: node
linkType: hard
"consola@npm:^2.15.0, consola@npm:^2.15.3":
"consola@npm:^2.15.0":
version: 2.15.3
resolution: "consola@npm:2.15.3"
checksum: 8ef7a09b703ec67ac5c389a372a33b6dc97eda6c9876443a60d76a3076eea0259e7f67a4e54fd5a52f97df73690822d090cf8b7e102b5761348afef7c6d03e28
@@ -25878,7 +25778,7 @@ __metadata:
languageName: node
linkType: hard
"create-require@npm:^1.1.0, create-require@npm:^1.1.1":
"create-require@npm:^1.1.0":
version: 1.1.1
resolution: "create-require@npm:1.1.1"
checksum: a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff
@@ -26089,7 +25989,7 @@ __metadata:
languageName: node
linkType: hard
"css-select@npm:^4.1.3, css-select@npm:^4.2.1":
"css-select@npm:^4.1.3":
version: 4.3.0
resolution: "css-select@npm:4.3.0"
dependencies:
@@ -27162,13 +27062,6 @@ __metadata:
languageName: node
linkType: hard
"domain-browser@npm:^4.22.0":
version: 4.23.0
resolution: "domain-browser@npm:4.23.0"
checksum: 95b772f5fa88300240694380e06e03868573acdf86ca392a58c78602d6536dca2097ad2469a1500bd23a1329b09992de846e0b66c364cbf5711a7fee3ee5dac9
languageName: node
linkType: hard
"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0":
version: 2.3.0
resolution: "domelementtype@npm:2.3.0"
@@ -27267,14 +27160,7 @@ __metadata:
languageName: node
linkType: hard
"dotenv-expand@npm:^8.0.2":
version: 8.0.3
resolution: "dotenv-expand@npm:8.0.3"
checksum: 128ce90ac825b543de3ece0154a51b056ab0dc36bb26d97a68cd0b8707327ecd3c182fb6ac63b26a0fcdfa85064419906a1065cb634f1f9dc08ad311375f1fc0
languageName: node
linkType: hard
"dotenv@npm:^16.0.0, dotenv@npm:^16.3.1":
"dotenv@npm:^16.3.1":
version: 16.4.5
resolution: "dotenv@npm:16.4.5"
checksum: 301a12c3d44fd49888b74eb9ccf9f07a1f5df43f489e7fcb89647a2edcd84c42d6bc349dc8df099cd18f07c35c7b04685c1a4f3e6a6a9e6b30f8d48c15b7f49c
@@ -27984,7 +27870,7 @@ __metadata:
languageName: node
linkType: hard
"esbuild@npm:^0.21.0, esbuild@npm:^0.21.3":
"esbuild@npm:^0.21.0":
version: 0.21.5
resolution: "esbuild@npm:0.21.5"
dependencies:
@@ -28343,15 +28229,15 @@ __metadata:
linkType: hard
"eslint-plugin-storybook@npm:^0.11.1":
version: 0.11.1
resolution: "eslint-plugin-storybook@npm:0.11.1"
version: 0.11.2
resolution: "eslint-plugin-storybook@npm:0.11.2"
dependencies:
"@storybook/csf": ^0.1.11
"@typescript-eslint/utils": ^8.8.1
ts-dedent: ^2.2.0
peerDependencies:
eslint: ">=6"
checksum: 8b8eb30b598f3c44c2bbf921e318215338f1159c1fba2d2f6cd5bc0b2ec14515655cf1760b5e11355baddabc2ac8446d4dc18ee6df6d2252555b126ff649a421
eslint: ">=8"
checksum: 04dab47b676db57aa6c2325d4ab66283f5970133a2615cc68a97e42cb4ae1c79317e6db73bcfc24468e146bdff4c9ffe90a368bf2bcb67593954485f058b33ca
languageName: node
linkType: hard
@@ -28774,7 +28660,6 @@ __metadata:
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@material-ui/lab": 4.0.0-alpha.61
"@modyfi/vite-plugin-yaml": ^1.1.0
"@octokit/rest": ^19.0.3
"@playwright/test": ^1.32.3
"@testing-library/dom": ^10.0.0
@@ -28785,7 +28670,6 @@ __metadata:
"@types/react": "*"
"@types/react-dom": "*"
"@types/zen-observable": ^0.8.0
"@vitejs/plugin-react": ^4.3.1
axios: ^1.7.7
cross-env: ^7.0.0
history: ^5.0.0
@@ -28795,9 +28679,6 @@ __metadata:
react-router: ^6.3.0
react-router-dom: ^6.3.0
react-use: ^17.2.4
vite: ^5.0.0
vite-plugin-html: ^3.2.2
vite-plugin-node-polyfills: ^0.22.0
zen-observable: ^0.10.0
languageName: unknown
linkType: soft
@@ -29215,7 +29096,7 @@ __metadata:
languageName: node
linkType: hard
"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2":
"fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2":
version: 3.3.2
resolution: "fast-glob@npm:3.3.2"
dependencies:
@@ -29976,7 +29857,7 @@ __metadata:
languageName: node
linkType: hard
"fs-extra@npm:10.1.0, fs-extra@npm:^10.0.0, fs-extra@npm:^10.0.1":
"fs-extra@npm:10.1.0, fs-extra@npm:^10.0.0":
version: 10.1.0
resolution: "fs-extra@npm:10.1.0"
dependencies:
@@ -30074,7 +29955,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
dependencies:
@@ -30093,7 +29974,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.3#~builtin<compat/fsevents>":
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
dependencies:
@@ -31292,7 +31173,7 @@ __metadata:
languageName: node
linkType: hard
"html-minifier-terser@npm:^6.0.2, html-minifier-terser@npm:^6.1.0":
"html-minifier-terser@npm:^6.0.2":
version: 6.1.0
resolution: "html-minifier-terser@npm:6.1.0"
dependencies:
@@ -32422,16 +32303,6 @@ __metadata:
languageName: node
linkType: hard
"is-nan@npm:^1.3.2":
version: 1.3.2
resolution: "is-nan@npm:1.3.2"
dependencies:
call-bind: ^1.0.0
define-properties: ^1.1.3
checksum: 5dfadcef6ad12d3029d43643d9800adbba21cf3ce2ec849f734b0e14ee8da4070d82b15fdb35138716d02587c6578225b9a22779cab34888a139cc43e4e3610a
languageName: node
linkType: hard
"is-network-error@npm:^1.0.0":
version: 1.0.1
resolution: "is-network-error@npm:1.0.1"
@@ -32893,13 +32764,6 @@ __metadata:
languageName: node
linkType: hard
"isomorphic-timers-promises@npm:^1.0.1":
version: 1.0.1
resolution: "isomorphic-timers-promises@npm:1.0.1"
checksum: 16ef59f0fbcceba1a037c74b5f7195d252ae058724ccd3e53b37ad034e8498f5532084e8ab18e7940ba3fa8fca2f21403d00eed15802ab1f7cab7c099cba62a8
languageName: node
linkType: hard
"isomorphic-ws@npm:5.0.0, isomorphic-ws@npm:^5.0.0":
version: 5.0.0
resolution: "isomorphic-ws@npm:5.0.0"
@@ -33646,7 +33510,7 @@ __metadata:
languageName: node
linkType: hard
"js-yaml@npm:4.1.0, js-yaml@npm:=4.1.0, js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0":
"js-yaml@npm:=4.1.0, js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0":
version: 4.1.0
resolution: "js-yaml@npm:4.1.0"
dependencies:
@@ -37359,16 +37223,6 @@ __metadata:
languageName: node
linkType: hard
"node-html-parser@npm:^5.3.3":
version: 5.4.2
resolution: "node-html-parser@npm:5.4.2"
dependencies:
css-select: ^4.2.1
he: 1.2.0
checksum: 2d2391147c83b402786eeab95d23ea4e24ca8608e0e70a2823bfd4f2a248be13a8cc31acfd55a0109e051131e4f0c17d7ada8d999ce70ff2e342ab0110f5da59
languageName: node
linkType: hard
"node-html-parser@npm:^6.1.1":
version: 6.1.5
resolution: "node-html-parser@npm:6.1.5"
@@ -37478,41 +37332,6 @@ __metadata:
languageName: node
linkType: hard
"node-stdlib-browser@npm:^1.2.0":
version: 1.2.0
resolution: "node-stdlib-browser@npm:1.2.0"
dependencies:
assert: ^2.0.0
browser-resolve: ^2.0.0
browserify-zlib: ^0.2.0
buffer: ^5.7.1
console-browserify: ^1.1.0
constants-browserify: ^1.0.0
create-require: ^1.1.1
crypto-browserify: ^3.11.0
domain-browser: ^4.22.0
events: ^3.0.0
https-browserify: ^1.0.0
isomorphic-timers-promises: ^1.0.1
os-browserify: ^0.3.0
path-browserify: ^1.0.1
pkg-dir: ^5.0.0
process: ^0.11.10
punycode: ^1.4.1
querystring-es3: ^0.2.1
readable-stream: ^3.6.0
stream-browserify: ^3.0.0
stream-http: ^3.2.0
string_decoder: ^1.0.0
timers-browserify: ^2.0.4
tty-browserify: 0.0.1
url: ^0.11.0
util: ^0.12.4
vm-browserify: ^1.0.1
checksum: fe491f0839319fd9bb95964c6f7da81fc7fde4c3ac9062aa367f19bc5a6060d0d9e423d3de4196cb51f8259d6aaf6cf380048c48a86eb3721c6223dd0dcc5bfd
languageName: node
linkType: hard
"node-watch@npm:0.7.3":
version: 0.7.3
resolution: "node-watch@npm:0.7.3"
@@ -37888,16 +37707,6 @@ __metadata:
languageName: node
linkType: hard
"object-is@npm:^1.1.5":
version: 1.1.6
resolution: "object-is@npm:1.1.6"
dependencies:
call-bind: ^1.0.7
define-properties: ^1.2.1
checksum: 3ea22759967e6f2380a2cbbd0f737b42dc9ddb2dfefdb159a1b927fea57335e1b058b564bfa94417db8ad58cddab33621a035de6f5e5ad56d89f2dd03e66c6a1
languageName: node
linkType: hard
"object-keys@npm:^1.1.1":
version: 1.1.1
resolution: "object-keys@npm:1.1.1"
@@ -38977,13 +38786,6 @@ __metadata:
languageName: node
linkType: hard
"pathe@npm:^0.2.0":
version: 0.2.0
resolution: "pathe@npm:0.2.0"
checksum: 9a8149ce152088f30d15b0b03a7c128ba21f16b4dc1f3f90fe38eee9f6d0f1d6da8e4e47bd2a4f9e14aaac7c30ed01cfc86216479011de2bdc598b65e6f19f41
languageName: node
linkType: hard
"pathval@npm:^2.0.0":
version: 2.0.0
resolution: "pathval@npm:2.0.0"
@@ -39309,15 +39111,6 @@ __metadata:
languageName: node
linkType: hard
"pkg-dir@npm:^5.0.0":
version: 5.0.0
resolution: "pkg-dir@npm:5.0.0"
dependencies:
find-up: ^5.0.0
checksum: b167bb8dac7bbf22b1d5e30ec223e6b064b84b63010c9d49384619a36734caf95ed23ad23d4f9bd975e8e8082b60a83395f43a89bb192df53a7c25a38ecb57d9
languageName: node
linkType: hard
"pkg-up@npm:^3.1.0":
version: 3.1.0
resolution: "pkg-up@npm:3.1.0"
@@ -39837,7 +39630,7 @@ __metadata:
languageName: node
linkType: hard
"postcss@npm:^8.1.0, postcss@npm:^8.4.33, postcss@npm:^8.4.43":
"postcss@npm:^8.1.0, postcss@npm:^8.4.33":
version: 8.4.49
resolution: "postcss@npm:8.4.49"
dependencies:
@@ -40355,7 +40148,7 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:^1.2.4, punycode@npm:^1.3.2, punycode@npm:^1.4.1":
"punycode@npm:^1.2.4, punycode@npm:^1.3.2":
version: 1.4.1
resolution: "punycode@npm:1.4.1"
checksum: fa6e698cb53db45e4628559e557ddaf554103d2a96a1d62892c8f4032cd3bc8871796cae9eabc1bc700e2b6677611521ce5bb1d9a27700086039965d0cf34518
@@ -40429,7 +40222,7 @@ __metadata:
languageName: node
linkType: hard
"querystring-es3@npm:^0.2.0, querystring-es3@npm:^0.2.1":
"querystring-es3@npm:^0.2.0":
version: 0.2.1
resolution: "querystring-es3@npm:0.2.1"
checksum: 691e8d6b8b157e7cd49ae8e83fcf86de39ab3ba948c25abaa94fba84c0986c641aa2f597770848c64abce290ed17a39c9df6df737dfa7e87c3b63acc7d225d61
@@ -41105,7 +40898,7 @@ __metadata:
languageName: node
linkType: hard
"react-refresh@npm:^0.14.0, react-refresh@npm:^0.14.2":
"react-refresh@npm:^0.14.0":
version: 0.14.2
resolution: "react-refresh@npm:0.14.2"
checksum: d80db4bd40a36dab79010dc8aa317a5b931f960c0d83c4f3b81f0552cbcf7f29e115b84bb7908ec6a1eb67720fff7023084eff73ece8a7ddc694882478464382
@@ -42032,7 +41825,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:1.22.8, resolve@npm:^1.1.6, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1, resolve@npm:~1.22.2":
"resolve@npm:1.22.8, resolve@npm:^1.1.6, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1, resolve@npm:~1.22.2":
version: 1.22.8
resolution: "resolve@npm:1.22.8"
dependencies:
@@ -42058,7 +41851,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@1.22.8#~builtin<compat/resolve>, resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.17.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.4#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.8#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.2#~builtin<compat/resolve>":
"resolve@patch:resolve@1.22.8#~builtin<compat/resolve>, resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.4#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.8#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.2#~builtin<compat/resolve>":
version: 1.22.8
resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin<compat/resolve>::version=1.22.8&hash=c3c19d"
dependencies:
@@ -42369,7 +42162,7 @@ __metadata:
languageName: node
linkType: hard
"rollup@npm:^4.20.0, rollup@npm:^4.27.3":
"rollup@npm:^4.27.3":
version: 4.27.4
resolution: "rollup@npm:4.27.4"
dependencies:
@@ -43827,7 +43620,7 @@ __metadata:
languageName: node
linkType: hard
"stream-browserify@npm:3.0.0, stream-browserify@npm:^3.0.0":
"stream-browserify@npm:3.0.0":
version: 3.0.0
resolution: "stream-browserify@npm:3.0.0"
dependencies:
@@ -43883,18 +43676,6 @@ __metadata:
languageName: node
linkType: hard
"stream-http@npm:^3.2.0":
version: 3.2.0
resolution: "stream-http@npm:3.2.0"
dependencies:
builtin-status-codes: ^3.0.0
inherits: ^2.0.4
readable-stream: ^3.6.0
xtend: ^4.0.2
checksum: c9b78453aeb0c84fcc59555518ac62bacab9fa98e323e7b7666e5f9f58af8f3155e34481078509b02929bd1268427f664d186604cdccee95abc446099b339f83
languageName: node
linkType: hard
"stream-json@npm:^1.7.4":
version: 1.8.0
resolution: "stream-json@npm:1.8.0"
@@ -45161,7 +44942,7 @@ __metadata:
languageName: node
linkType: hard
"tosource@npm:2.0.0-alpha.3, tosource@npm:^2.0.0-alpha.3":
"tosource@npm:^2.0.0-alpha.3":
version: 2.0.0-alpha.3
resolution: "tosource@npm:2.0.0-alpha.3"
checksum: bc03a7571de8ed4306e6721283fa891f2adcab9dd80c46f6f177d4259b34bb192fe3a2cb3e1e2ce16f9db0bc7e534acfcb5478ab094b0ba255f98abfce6dab46
@@ -45501,13 +45282,6 @@ __metadata:
languageName: node
linkType: hard
"tty-browserify@npm:0.0.1":
version: 0.0.1
resolution: "tty-browserify@npm:0.0.1"
checksum: 93b745d43fa5a7d2b948fa23be8d313576d1d884b48acd957c07710bac1c0d8ac34c0556ad4c57c73d36e11741763ef66b3fb4fb97b06b7e4d525315a3cd45f5
languageName: node
linkType: hard
"tunnel-agent@npm:^0.6.0":
version: 0.6.0
resolution: "tunnel-agent@npm:0.6.0"
@@ -46739,83 +46513,6 @@ __metadata:
languageName: node
linkType: hard
"vite-plugin-html@npm:^3.2.2":
version: 3.2.2
resolution: "vite-plugin-html@npm:3.2.2"
dependencies:
"@rollup/pluginutils": ^4.2.0
colorette: ^2.0.16
connect-history-api-fallback: ^1.6.0
consola: ^2.15.3
dotenv: ^16.0.0
dotenv-expand: ^8.0.2
ejs: ^3.1.6
fast-glob: ^3.2.11
fs-extra: ^10.0.1
html-minifier-terser: ^6.1.0
node-html-parser: ^5.3.3
pathe: ^0.2.0
peerDependencies:
vite: ">=2.0.0"
checksum: 2fd6e1f91f74a4432222ed28e68d5f27e58ccbc9ad44e71ff9d02b684b358b0c634bdb4dd32e9d93d09e88d83c3b7b74b89698e25510bc5b94173cdc067b3ac2
languageName: node
linkType: hard
"vite-plugin-node-polyfills@npm:^0.22.0":
version: 0.22.0
resolution: "vite-plugin-node-polyfills@npm:0.22.0"
dependencies:
"@rollup/plugin-inject": ^5.0.5
node-stdlib-browser: ^1.2.0
peerDependencies:
vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
checksum: c08d3df0d5cc3102280483d3f7b216f92a18e0708fcb9f67f78f01ab865474254756bacee17caa90b3f46afe5834cb9d8de0dd0e58c1bbbdae1b949edc1e6b57
languageName: node
linkType: hard
"vite@npm:^5.0.0":
version: 5.4.11
resolution: "vite@npm:5.4.11"
dependencies:
esbuild: ^0.21.3
fsevents: ~2.3.3
postcss: ^8.4.43
rollup: ^4.20.0
peerDependencies:
"@types/node": ^18.0.0 || >=20.0.0
less: "*"
lightningcss: ^1.21.0
sass: "*"
sass-embedded: "*"
stylus: "*"
sugarss: "*"
terser: ^5.4.0
dependenciesMeta:
fsevents:
optional: true
peerDependenciesMeta:
"@types/node":
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
bin:
vite: bin/vite.js
checksum: 8c5b31d17487b69c40a30419dc0ade9f33360eb6893dbfa33a90980271bd74d35ae550b5cbb2a9e640f0df41ea36fd1bb4f222c98f6d02e607080f20832e69e8
languageName: node
linkType: hard
"vm-browserify@npm:^1.0.1":
version: 1.1.2
resolution: "vm-browserify@npm:1.1.2"
@@ -47650,7 +47347,7 @@ __metadata:
languageName: node
linkType: hard
"xtend@npm:^4.0.0, xtend@npm:^4.0.2":
"xtend@npm:^4.0.0":
version: 4.0.2
resolution: "xtend@npm:4.0.2"
checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a