From c59167016378de2b5206737ab283b43d5b77887a Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Wed, 26 Jun 2024 16:07:06 -0600 Subject: [PATCH 01/77] fix: enable support for account and org level urls Signed-off-by: Calvin Lee --- .changeset/witty-bears-behave.md | 5 ++ packages/integration/src/harness/core.ts | 71 +++++++++++++++++------- 2 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 .changeset/witty-bears-behave.md diff --git a/.changeset/witty-bears-behave.md b/.changeset/witty-bears-behave.md new file mode 100644 index 0000000000..33b4c0b187 --- /dev/null +++ b/.changeset/witty-bears-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Updated function for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls diff --git a/packages/integration/src/harness/core.ts b/packages/integration/src/harness/core.ts index 81281eaa6e..1887a5c0d8 100644 --- a/packages/integration/src/harness/core.ts +++ b/packages/integration/src/harness/core.ts @@ -34,7 +34,12 @@ export function getHarnessEditContentsUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/ng/account/${parsedUrl.accountId}/module/code/orgs/${parsedUrl.orgName}/projects/${parsedUrl.projectName}/repos/${parsedUrl.repoName}/files/${parsedUrl.branch}/~/${parsedUrl.path}`; + + return `${parsedUrl.baseUrl}/ng/account/${parsedUrl.accountId}/module/code${ + parsedUrl.orgName !== '' ? `/orgs/${parsedUrl.orgName}` : '' + }${ + parsedUrl.projectName !== '' ? `/projects/${parsedUrl.projectName}` : '' + }/repos/${parsedUrl.repoName}/files/${parsedUrl.branch}/~/${parsedUrl.path}`; } /** @@ -44,6 +49,7 @@ export function getHarnessEditContentsUrl( * * Converts * from: https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml + * https://qa.harness.io/ng/account/bDCAuAjFSJCLFj_0ug3lCg/module/code/orgs/HiteshTest/repos/impoorter/files/main/~/catalog.yaml * to: https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/content/all-apis.yaml?routingId=accountId&include_commit=false&ref=refMain * * @param url - A URL pointing to a file @@ -55,7 +61,14 @@ export function getHarnessFileContentsUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/raw/${parsedUrl.path}?routingId=${parsedUrl.accountId}&git_ref=refs/heads/${parsedUrl.refString}`; + + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/raw/${parsedUrl.path}?routingId=${ + parsedUrl.accountId + }&git_ref=refs/heads/${parsedUrl.refString}`; } /** @@ -77,7 +90,13 @@ export function getHarnessArchiveUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/archive/${parsedUrl.branch}.zip?routingId=${parsedUrl.accountId}`; + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/archive/${parsedUrl.branch}.zip?routingId=${ + parsedUrl.accountId + }`; } /** @@ -99,7 +118,13 @@ export function getHarnessLatestCommitUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/content?routingId=${parsedUrl.accountId}&include_commit=true&git_ref=refs/heads/${parsedUrl.branch}`; + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/content?routingId=${ + parsedUrl.accountId + }&include_commit=true&git_ref=refs/heads/${parsedUrl.branch}`; } /** @@ -153,27 +178,30 @@ export function parseHarnessUrl( .split('/') .filter(segment => segment !== ''); const urlParts = pathUrl.pathname.split('/'); - const [ - _ng, - _account, - accountId, - _module, - _moduleName, - _org, - orgName, - _projects, - projectName, - _repos, - repoName, - _files, - _ref, - _branch, - ..._path - ] = pathSegments; + + const accountIdIndex = + pathSegments.findIndex(segment => segment === 'account') + 1; + const accountId = pathSegments[accountIdIndex]; + + const orgNameIndex = pathSegments.findIndex(segment => segment === 'orgs'); + const orgName = orgNameIndex !== -1 ? pathSegments[orgNameIndex + 1] : ''; + + const projectNameIndex = pathSegments.findIndex( + segment => segment === 'projects', + ); + + const projectName = + projectNameIndex !== -1 ? pathSegments[projectNameIndex + 1] : ''; + + const repoNameIndex = + pathSegments.findIndex(segment => segment === 'repos') + 1; + const repoName = pathSegments[repoNameIndex]; + const refAndPath = urlParts.slice( urlParts.findIndex(i => i === 'files' || i === 'edit') + 1, ); const refIndex = refAndPath.findIndex(item => item === '~'); + const refString = refAndPath.slice(0, refIndex).join('/'); const pathWithoutSlash = refIndex !== -1 @@ -182,6 +210,7 @@ export function parseHarnessUrl( .join('/') .replace(/^\//, '') : ''; + return { baseUrl: baseUrl, accountId: accountId, From a840355a10f9ad1b47975b65b30e89a4e7181876 Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Fri, 28 Jun 2024 11:41:43 -0600 Subject: [PATCH 02/77] fix: update changeset Signed-off-by: Calvin Lee --- .changeset/witty-bears-behave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/witty-bears-behave.md b/.changeset/witty-bears-behave.md index 33b4c0b187..7514759189 100644 --- a/.changeset/witty-bears-behave.md +++ b/.changeset/witty-bears-behave.md @@ -2,4 +2,4 @@ '@backstage/integration': patch --- -Updated function for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls +Updated functions for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls From e6e293ca30256d8d7641f5c9ff27e29b76f149de Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 10:22:21 +0200 Subject: [PATCH 03/77] chore: extract BaseRepoUrlPickerProps Signed-off-by: Benjamin Janssens --- .../fields/RepoUrlPicker/AzureRepoPicker.tsx | 15 +++++++-------- .../RepoUrlPicker/BitbucketRepoPicker.tsx | 17 ++++++++--------- .../fields/RepoUrlPicker/GerritRepoPicker.tsx | 8 ++------ .../fields/RepoUrlPicker/GiteaRepoPicker.tsx | 15 +++++++-------- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 13 ++++++------- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 15 +++++++-------- .../components/fields/RepoUrlPicker/types.ts | 6 ++++++ 7 files changed, 43 insertions(+), 46 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx index 833a0849fb..48fcb4f6af 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -19,16 +19,15 @@ import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; -import { RepoUrlPickerState } from './types'; +import { BaseRepoUrlPickerProps } from './types'; import { Select, SelectItem } from '@backstage/core-components'; -export const AzureRepoPicker = (props: { - allowedOrganizations?: string[]; - allowedProject?: string[]; - rawErrors: string[]; - state: RepoUrlPickerState; - onChange: (state: RepoUrlPickerState) => void; -}) => { +export const AzureRepoPicker = ( + props: BaseRepoUrlPickerProps<{ + allowedOrganizations?: string[]; + allowedProject?: string[]; + }>, +) => { const { allowedOrganizations = [], allowedProject = [], diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 10d4a6e6c3..5d627e9512 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -17,7 +17,7 @@ import React, { useEffect, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import { Select, SelectItem } from '@backstage/core-components'; -import { RepoUrlPickerState } from './types'; +import { BaseRepoUrlPickerProps } from './types'; import Autocomplete from '@material-ui/lab/Autocomplete'; import TextField from '@material-ui/core/TextField'; import useDebounce from 'react-use/esm/useDebounce'; @@ -33,14 +33,13 @@ import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; * @param allowedProjects - Allowed projects for the Bitbucket cloud repository * */ -export const BitbucketRepoPicker = (props: { - allowedOwners?: string[]; - allowedProjects?: string[]; - onChange: (state: RepoUrlPickerState) => void; - state: RepoUrlPickerState; - rawErrors: string[]; - accessToken?: string; -}) => { +export const BitbucketRepoPicker = ( + props: BaseRepoUrlPickerProps<{ + allowedOwners?: string[]; + allowedProjects?: string[]; + accessToken?: string; + }>, +) => { const { allowedOwners = [], allowedProjects = [], diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx index 13af966c25..1b13c5eb29 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx @@ -18,13 +18,9 @@ import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; -import { RepoUrlPickerState } from './types'; +import { BaseRepoUrlPickerProps } from './types'; -export const GerritRepoPicker = (props: { - onChange: (state: RepoUrlPickerState) => void; - state: RepoUrlPickerState; - rawErrors: string[]; -}) => { +export const GerritRepoPicker = (props: BaseRepoUrlPickerProps) => { const { onChange, rawErrors, state } = props; const { workspace, owner } = state; return ( diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GiteaRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GiteaRepoPicker.tsx index 566a2732e3..9fb3af211f 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GiteaRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GiteaRepoPicker.tsx @@ -19,15 +19,14 @@ import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; -import { RepoUrlPickerState } from './types'; +import { BaseRepoUrlPickerProps } from './types'; -export const GiteaRepoPicker = (props: { - allowedOwners?: string[]; - allowedRepos?: string[]; - state: RepoUrlPickerState; - onChange: (state: RepoUrlPickerState) => void; - rawErrors: string[]; -}) => { +export const GiteaRepoPicker = ( + props: BaseRepoUrlPickerProps<{ + allowedOwners?: string[]; + allowedRepos?: string[]; + }>, +) => { const { allowedOwners = [], state, onChange, rawErrors } = props; const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index ec59d89ef8..5d46dc4333 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -19,14 +19,13 @@ import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; -import { RepoUrlPickerState } from './types'; +import { BaseRepoUrlPickerProps } from './types'; -export const GithubRepoPicker = (props: { - allowedOwners?: string[]; - rawErrors: string[]; - state: RepoUrlPickerState; - onChange: (state: RepoUrlPickerState) => void; -}) => { +export const GithubRepoPicker = ( + props: BaseRepoUrlPickerProps<{ + allowedOwners?: string[]; + }>, +) => { const { allowedOwners = [], rawErrors, state, onChange } = props; const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index fbcc13c846..12ae37bf15 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -19,15 +19,14 @@ import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; -import { RepoUrlPickerState } from './types'; +import { BaseRepoUrlPickerProps } from './types'; -export const GitlabRepoPicker = (props: { - allowedOwners?: string[]; - allowedRepos?: string[]; - state: RepoUrlPickerState; - onChange: (state: RepoUrlPickerState) => void; - rawErrors: string[]; -}) => { +export const GitlabRepoPicker = ( + props: BaseRepoUrlPickerProps<{ + allowedOwners?: string[]; + allowedRepos?: string[]; + }>, +) => { const { allowedOwners = [], state, onChange, rawErrors } = props; const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts index 1430b10b84..9489901d61 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts @@ -22,3 +22,9 @@ export interface RepoUrlPickerState { project?: string; availableRepos?: string[]; } + +export type BaseRepoUrlPickerProps = T & { + onChange: (state: RepoUrlPickerState) => void; + state: RepoUrlPickerState; + rawErrors: string[]; +}; From 6b8bab93beaee731a7db18b818cb13af21e848c3 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 10:40:19 +0200 Subject: [PATCH 04/77] refactor: use useCallback for autocompletion updates in BitbucketRepoPicker Signed-off-by: Benjamin Janssens --- .../RepoUrlPicker/BitbucketRepoPicker.tsx | 153 +++++++++--------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 5d627e9512..0b7ee49525 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import { Select, SelectItem } from '@backstage/core-components'; @@ -68,93 +68,90 @@ export const BitbucketRepoPicker = ( const [availableProjects, setAvailableProjects] = useState([]); // Update available workspaces when client is available - useDebounce( - () => { - const updateAvailableWorkspaces = async () => { - if ( - host === 'bitbucket.org' && - accessToken && - scaffolderApi.autocomplete - ) { - const { results } = await scaffolderApi.autocomplete({ - token: accessToken, - resource: 'workspaces', - context: {}, - provider: 'bitbucket-cloud', - }); + const updateAvailableWorkspaces = useCallback(() => { + if ( + !scaffolderApi.autocomplete || + !accessToken || + host !== 'bitbucket.org' + ) { + setAvailableWorkspaces([]); + return; + } - setAvailableWorkspaces(results.map(r => r.title)); - } else { - setAvailableWorkspaces([]); - } - }; + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'workspaces', + provider: 'bitbucket-cloud', + }) + .then(({ results }) => { + setAvailableWorkspaces(results.map(r => r.title)); + }) + .catch(() => { + setAvailableWorkspaces([]); + }); + }, [scaffolderApi, accessToken, host]); - updateAvailableWorkspaces().catch(() => setAvailableWorkspaces([])); - }, - 500, - [host, accessToken], - ); + useDebounce(updateAvailableWorkspaces, 500, [updateAvailableWorkspaces]); // Update available projects when client is available and workspace changes - useDebounce( - () => { - const updateAvailableProjects = async () => { - if ( - host === 'bitbucket.org' && - accessToken && - workspace && - scaffolderApi.autocomplete - ) { - const { results } = await scaffolderApi.autocomplete({ - token: accessToken, - resource: 'projects', - context: { workspace }, - provider: 'bitbucket-cloud', - }); + const updateAvailableProjects = useCallback(() => { + if ( + !scaffolderApi.autocomplete || + !accessToken || + host !== 'bitbucket.org' || + !workspace + ) { + setAvailableProjects([]); + return; + } - setAvailableProjects(results.map(r => r.title)); - } else { - setAvailableProjects([]); - } - }; + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'projects', + context: { workspace }, + provider: 'bitbucket-cloud', + }) + .then(({ results }) => { + setAvailableProjects(results.map(r => r.title)); + }) + .catch(() => { + setAvailableProjects([]); + }); + }, [scaffolderApi, accessToken, host, workspace]); - updateAvailableProjects().catch(() => setAvailableProjects([])); - }, - 500, - [host, accessToken, workspace], - ); + useDebounce(updateAvailableProjects, 500, [updateAvailableProjects]); // Update available repositories when client is available and workspace or project changes - useDebounce( - () => { - const updateAvailableRepositories = async () => { - if ( - host === 'bitbucket.org' && - accessToken && - workspace && - project && - scaffolderApi.autocomplete - ) { - const { results } = await scaffolderApi.autocomplete({ - token: accessToken, - resource: 'repositories', - context: { workspace, project }, - provider: 'bitbucket-cloud', - }); + const updateAvailableRepositories = useCallback(() => { + if ( + !scaffolderApi.autocomplete || + !accessToken || + host !== 'bitbucket.org' || + !workspace || + !project + ) { + onChange({ availableRepos: [] }); + return; + } - onChange({ availableRepos: results.map(r => r.title) }); - } else { - onChange({ availableRepos: [] }); - } - }; + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'repositories', + context: { workspace, project }, + provider: 'bitbucket-cloud', + }) + .then(({ results }) => { + onChange({ availableRepos: results.map(r => r.title) }); + }) + .catch(() => { + onChange({ availableRepos: [] }); + }); + }, [scaffolderApi, accessToken, host, workspace, project, onChange]); - updateAvailableRepositories().catch(() => - onChange({ availableRepos: [] }), - ); - }, - 500, - [host, accessToken, workspace, project], - ); + useDebounce(updateAvailableRepositories, 500, [updateAvailableRepositories]); return ( <> From 7f153f94c1c3d1849e418109e097b03914941113 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 10:44:22 +0200 Subject: [PATCH 05/77] test: simplify checking for the secret in the document; remove SecretsComponent from test that didn't use it Signed-off-by: Benjamin Janssens --- .../RepoUrlPicker/RepoUrlPicker.test.tsx | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx index 002652cfab..f0e47a2504 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx @@ -214,13 +214,14 @@ describe('RepoUrlPicker', () => { describe('requestUserCredentials', () => { it('should call the scmAuthApi with the correct params', async () => { + const secretsKey = 'testKey'; + const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); + const secret = secrets[secretsKey]; + return secret ?
{secret}
: null; }; - const { getByTestId } = await renderInTestApp( + const { getByText } = await renderInTestApp( { 'ui:field': 'RepoUrlPicker', 'ui:options': { requestUserCredentials: { - secretsKey: 'testKey', + secretsKey, additionalScopes: { github: ['workflow'] }, }, }, @@ -265,21 +266,9 @@ describe('RepoUrlPicker', () => { }, }); - const currentSecrets = JSON.parse( - getByTestId('current-secrets').textContent!, - ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, - }); + expect(getByText('abc123')).toBeInTheDocument(); }); it('should call the scmAuthApi with the correct params if workspace is nested', async () => { - const SecretsComponent = () => { - const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); - }; await renderInTestApp( { RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField, }} /> - , ); @@ -324,13 +312,14 @@ describe('RepoUrlPicker', () => { }); it('should not call the scmAuthApi if secret is available in the state', async () => { + const secretsKey = 'testKey'; + const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); + const secret = secrets[secretsKey]; + return secret ?
{secret}
: null; }; - const { getByTestId } = await renderInTestApp( + const { getByText } = await renderInTestApp( { [scaffolderApiRef, mockScaffolderApi], ]} > - +
{ 'ui:field': 'RepoUrlPicker', 'ui:options': { requestUserCredentials: { - secretsKey: 'testKey', + secretsKey, additionalScopes: { github: ['workflow'] }, }, }, @@ -368,13 +357,7 @@ describe('RepoUrlPicker', () => { // as we already have a secret in the state, getCredentials should not be called again. expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); - const currentSecrets = JSON.parse( - getByTestId('current-secrets').textContent!, - ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, - }); + expect(getByText('abc123')).toBeInTheDocument(); }); }); }); From bbd9f56c764b3fc22c79a279bca48f70d6c83dea Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 10:49:56 +0200 Subject: [PATCH 06/77] chore: add changeset Signed-off-by: Benjamin Janssens --- .changeset/spotty-planets-accept.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spotty-planets-accept.md diff --git a/.changeset/spotty-planets-accept.md b/.changeset/spotty-planets-accept.md new file mode 100644 index 0000000000..db262dd8b5 --- /dev/null +++ b/.changeset/spotty-planets-accept.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Cleaned up codebase From 9a5e7f1c36934b6eca39c8c13fbc35bcc43fefb2 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 10:51:24 +0200 Subject: [PATCH 07/77] chore: update changeset Signed-off-by: Benjamin Janssens --- .changeset/spotty-planets-accept.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spotty-planets-accept.md b/.changeset/spotty-planets-accept.md index db262dd8b5..7342f89bd1 100644 --- a/.changeset/spotty-planets-accept.md +++ b/.changeset/spotty-planets-accept.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': patch --- -Cleaned up codebase +Cleaned up codebase of RepoUrlPicker From c4a4a69912e004dbfa622d9f2107410af8fffd34 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 11:15:21 +0200 Subject: [PATCH 08/77] test: fix BitbucketRepoPicker tests Signed-off-by: Benjamin Janssens --- .../fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx index 2ae89233ff..570c5bd664 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx @@ -27,9 +27,11 @@ import { act } from 'react-dom/test-utils'; describe('BitbucketRepoPicker', () => { const scaffolderApiMock: Partial = { - autocomplete: jest.fn().mockImplementation(opts => ({ - results: [{ title: `${opts.resource}_example` }], - })), + autocomplete: jest.fn().mockImplementation(opts => + Promise.resolve({ + results: [{ title: `${opts.resource}_example` }], + }), + ), }; it('renders a select if there is a list of allowed owners', async () => { From 46e949f5ed570aefcc6917f4e771f633aef714e3 Mon Sep 17 00:00:00 2001 From: Francisco Meneses Date: Wed, 17 Jul 2024 17:16:29 -0400 Subject: [PATCH 09/77] fix Spanish welcomeTitle message Signed-off-by: Francisco Meneses --- .../WelcomeTitle/locales/goodAfternoon.locales.json | 1 + .../WelcomeTitle/locales/goodEvening.locales.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json index 9010ba4359..ddb51654fa 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json +++ b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json @@ -103,6 +103,7 @@ "Sinhala": "සුභ දවාලක්", "Slovenian": "Dober dan", "Somali": "Galab wanaagsan", + "Spanish": "Buenas tardes", "Southern Sámi": "Buerie biejjie", "Sumerian": "𒄭𒌓𒁀", "Swahili": "Habari ya mchana", diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json index 5a98abafaa..c8d8c7334e 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json +++ b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json @@ -85,7 +85,7 @@ "Slovak": "Dober vecher", "Slovenian": "Dober vecher", "Somali": "Habeen wanaagsan", - "Spanish": "Buenas tardes", + "Spanish": "Buenas noches", "Sumerian": "𒄭𒈪", "Swedish": "God afton", "Tagalog": "Magandang gabi", From fdcc05947fc7447a28d3090235d882896e37d10a Mon Sep 17 00:00:00 2001 From: Francisco Meneses Date: Wed, 17 Jul 2024 17:34:17 -0400 Subject: [PATCH 10/77] add changeset Signed-off-by: Francisco Meneses --- .changeset/slow-toes-jog.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slow-toes-jog.md diff --git a/.changeset/slow-toes-jog.md b/.changeset/slow-toes-jog.md new file mode 100644 index 0000000000..245feab544 --- /dev/null +++ b/.changeset/slow-toes-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Fixed a bug on the WelcomeTitle component where the welcome message wasn't correct when the language was set to Spanish From ec950c58caac142a311225af4463e50982f7ee11 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 18 Jul 2024 12:35:16 +0200 Subject: [PATCH 11/77] chore: remove app-api dependency Signed-off-by: blam --- plugins/search-backend-module-pg/package.json | 1 - yarn.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 8bdcf14306..1693c14c8a 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -49,7 +49,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", diff --git a/yarn.lock b/yarn.lock index c6a1d2139a..9261d70f21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7271,7 +7271,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-search-backend-module-pg@workspace:plugins/search-backend-module-pg" dependencies: - "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" From d3d6f5e9ec07a34dc9d6c66c84493629d75ea0a4 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 18 Jul 2024 12:42:16 +0200 Subject: [PATCH 12/77] chore: remove app-api dep Signed-off-by: blam --- packages/backend-dynamic-feature-service/package.json | 3 ++- .../src/schemas/rootLoggerServiceFactory.ts | 4 ++-- yarn.lock | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 0d05b97bd9..c693135f30 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -41,8 +41,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/cli-common": "workspace:^", @@ -70,6 +70,7 @@ "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "wait-for-expect": "^3.0.2" diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts index 6681a65a75..bf3df3ffe7 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts @@ -18,9 +18,9 @@ import { createServiceFactory, coreServices, } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; +import { WinstonLogger } from '@backstage/backend-defaults/rootLogger'; import { transports, format } from 'winston'; -import { createConfigSecretEnumerator } from '@backstage/backend-app-api'; +import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootLogger'; import { loadConfigSchema } from '@backstage/config-loader'; import { getPackages } from '@manypkg/get-packages'; import { dynamicPluginsSchemasServiceRef } from './schemas'; diff --git a/yarn.lock b/yarn.lock index 9261d70f21..53bef5284a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3708,6 +3708,7 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" From 7251567926578e77bddd0f87ec668b91209826e8 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 18 Jul 2024 12:43:24 +0200 Subject: [PATCH 13/77] chore: changeset Signed-off-by: blam --- .changeset/nine-glasses-nail.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/nine-glasses-nail.md diff --git a/.changeset/nine-glasses-nail.md b/.changeset/nine-glasses-nail.md new file mode 100644 index 0000000000..0d68da9f2b --- /dev/null +++ b/.changeset/nine-glasses-nail.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +'@backstage/plugin-search-backend-module-pg': patch +--- + +Removing `@backstage/backend-app-api` dependency From 0a40f13580e9b6af226ab6bdbd49e2505d988a2c Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 18 Jul 2024 13:05:32 +0200 Subject: [PATCH 14/77] chore: wip Signed-off-by: blam --- .../src/components/TabbedLayout/RoutedTabs.tsx | 3 +++ .../src/layout/HeaderTabs/HeaderTabs.test.tsx | 9 --------- .../core-components/src/layout/HeaderTabs/HeaderTabs.tsx | 7 +------ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx index 2c727efe1b..6a5bf2970f 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx @@ -96,6 +96,9 @@ export function RoutedTabs(props: { routes: SubRoute[] }) { tabs={headerTabs} selectedIndex={index} onChange={onTabChange} + tabProps={{ + component: Link, + }} /> diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx index dad40c4615..5aec83bc2b 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx @@ -95,13 +95,4 @@ describe('', () => { await user.click(rendered.getByText('Docs')); expect(mockOnChange).toHaveBeenCalledTimes(1); }); - - it('should render 2 nav tabs', async () => { - const rendered = await renderInTestApp(); - const tabs = rendered.queryAllByTestId(id => id.startsWith('header-tab')); - expect(tabs).toHaveLength(2); - tabs.forEach(tab => { - expect(tab.tagName.toLocaleLowerCase('en-US')).toBe('a'); - }); - }); }); diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx index 9ee9c5525f..002c9fe1ea 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx @@ -18,7 +18,6 @@ import { makeStyles } from '@material-ui/core/styles'; import TabUI, { TabProps } from '@material-ui/core/Tab'; import Tabs from '@material-ui/core/Tabs'; import React, { useCallback, useEffect, useState } from 'react'; -import { Link } from '../../components/Link'; // TODO(blam): Remove this implementation when the Tabs are ready // This is just a temporary solution to implementing tabs for now @@ -96,9 +95,7 @@ export function HeaderTabs(props: HeaderTabsProps) { setSelectedTab(selectedIndex); } }, [selectedIndex]); - function removeLeadingSlash(path: string) { - return path.replace(/^\//, ''); - } + return ( Date: Thu, 18 Jul 2024 16:58:03 +0200 Subject: [PATCH 15/77] chore: skip backend dynamic Signed-off-by: blam --- .changeset/nine-glasses-nail.md | 1 - packages/backend-dynamic-feature-service/package.json | 3 +-- .../src/schemas/rootLoggerServiceFactory.ts | 4 ++-- yarn.lock | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.changeset/nine-glasses-nail.md b/.changeset/nine-glasses-nail.md index 0d68da9f2b..138acce3dd 100644 --- a/.changeset/nine-glasses-nail.md +++ b/.changeset/nine-glasses-nail.md @@ -1,5 +1,4 @@ --- -'@backstage/backend-dynamic-feature-service': patch '@backstage/plugin-search-backend-module-pg': patch --- diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index c693135f30..0d05b97bd9 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -41,8 +41,8 @@ "start": "backstage-cli package start" }, "dependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", - "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/cli-common": "workspace:^", @@ -70,7 +70,6 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "wait-for-expect": "^3.0.2" diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts index bf3df3ffe7..6681a65a75 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts @@ -18,9 +18,9 @@ import { createServiceFactory, coreServices, } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-defaults/rootLogger'; +import { WinstonLogger } from '@backstage/backend-app-api'; import { transports, format } from 'winston'; -import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootLogger'; +import { createConfigSecretEnumerator } from '@backstage/backend-app-api'; import { loadConfigSchema } from '@backstage/config-loader'; import { getPackages } from '@manypkg/get-packages'; import { dynamicPluginsSchemasServiceRef } from './schemas'; diff --git a/yarn.lock b/yarn.lock index 53bef5284a..9261d70f21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3708,7 +3708,6 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" - "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" From 1eb5e9f686bd52bf99af3f611415ce2ad8bd7dc7 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 19 Jul 2024 14:06:17 +0200 Subject: [PATCH 16/77] chore: attempt to fix the tabs Signed-off-by: blam --- .../TabbedLayout/RoutedTabs.test.tsx | 12 +++++ .../components/TabbedLayout/RoutedTabs.tsx | 51 ++++++++----------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx index ec91208eee..991276c5d3 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx @@ -160,4 +160,16 @@ describe('RoutedTabs', () => { rendered.queryByText('tabbed-test-content-2'), ).not.toBeInTheDocument(); }); + + it('should render the tabs as links', async () => { + const routes = [testRoute2, testRoute1, testRoute3]; + const rendered = await renderInTestApp(); + + const tabs = rendered.queryAllByRole('tab'); + + for (const [k, v] of Object.entries(tabs)) { + expect(v.tagName).toBe('A'); + expect(v).toHaveAttribute('href', routes[Number(k)].path); + } + }); }); diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx index 6a5bf2970f..d6f6eab144 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx @@ -15,15 +15,11 @@ */ import React, { useMemo } from 'react'; import { Helmet } from 'react-helmet'; -import { - matchRoutes, - useNavigate, - useParams, - useRoutes, -} from 'react-router-dom'; +import { matchRoutes, useParams, useRoutes } from 'react-router-dom'; import { Content } from '../../layout/Content'; import { HeaderTabs } from '../../layout/HeaderTabs'; import { SubRoute } from './types'; +import { Link } from '../Link'; export function useSelectedSubRoute(subRoutes: SubRoute[]): { index: number; @@ -68,38 +64,33 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): { export function RoutedTabs(props: { routes: SubRoute[] }) { const { routes } = props; - const navigate = useNavigate(); + const { index, route, element } = useSelectedSubRoute(routes); const headerTabs = useMemo( () => - routes.map(t => ({ - id: t.path, - label: t.title, - tabProps: t.tabProps, - })), + routes.map(t => { + const { path, title, tabProps } = t; + let to = path; + // Remove trailing /* + to = to.replace(/\/\*$/, ''); + // And remove leading / for relative navigation + to = to.replace(/^\//, ''); + return { + id: path, + label: title, + tabProps: { + ...tabProps, + component: Link, + to, + }, + }; + }), [routes], ); - const onTabChange = (tabIndex: number) => { - let { path } = routes[tabIndex]; - // Remove trailing /* - path = path.replace(/\/\*$/, ''); - // And remove leading / for relative navigation - path = path.replace(/^\//, ''); - // Note! route resolves relative to the position in the React tree, - // not relative to current location - navigate(path); - }; return ( <> - + {element} From 678971ad41380d5b958ed4b9f72e9372385d9380 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 19 Jul 2024 14:07:17 +0200 Subject: [PATCH 17/77] chore: added changeset Signed-off-by: blam --- .changeset/eighty-mirrors-flow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eighty-mirrors-flow.md diff --git a/.changeset/eighty-mirrors-flow.md b/.changeset/eighty-mirrors-flow.md new file mode 100644 index 0000000000..57e978a78c --- /dev/null +++ b/.changeset/eighty-mirrors-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Move the `Link` component to the `RoutedTabs` instead of the `HeaderTabs` component From b8600fe1650507c7b0e443ba58d8183e4a7413a8 Mon Sep 17 00:00:00 2001 From: Adam Letizia Date: Fri, 19 Jul 2024 09:48:20 -0500 Subject: [PATCH 18/77] fix(scaffolder): refetch user credentials when selected host is changed in RepoUrlPicker Signed-off-by: Adam Letizia --- .changeset/tough-lies-repair.md | 5 ++ .../RepoUrlPicker/RepoUrlPicker.test.tsx | 56 +++++++++++++++---- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 11 +++- 3 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 .changeset/tough-lies-repair.md diff --git a/.changeset/tough-lies-repair.md b/.changeset/tough-lies-repair.md new file mode 100644 index 0000000000..25b1884bbc --- /dev/null +++ b/.changeset/tough-lies-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +RepoUrlPicker: refetch user credentials when a new host is selected diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx index 002652cfab..fce9d9517c 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx @@ -33,6 +33,7 @@ import { ScaffolderRJSFField, } from '@backstage/plugin-scaffolder-react'; import { act, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; describe('RepoUrlPicker', () => { const mockScaffolderApi: Partial = { @@ -323,14 +324,29 @@ describe('RepoUrlPicker', () => { }); }); - it('should not call the scmAuthApi if secret is available in the state', async () => { + it('should call the scmAuthApi with the new host when the host is changed', async () => { const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); return (
{JSON.stringify({ secrets })}
); }; - const { getByTestId } = await renderInTestApp( + const allowedHosts = ['github.com', 'gitlab.example.com']; + + (mockScmAuthApi.getCredentials as jest.Mock).mockImplementation( + ({ url }) => { + let token = ''; + if (url === `https://${allowedHosts[0]}`) { + token = 'abc123'; + } else if (url === `https://${allowedHosts[1]}`) { + token = 'def456'; + } + return Promise.resolve({ token }); + }, + ); + const secondHost = allowedHosts[1]; + + const { getAllByRole, getByTestId } = await renderInTestApp( { [scaffolderApiRef, mockScaffolderApi], ]} > - + { ); await act(async () => { + // need to wait for the debounce to finish to fetch credentials for the first selected host + await new Promise(resolve => setTimeout(resolve, 600)); + }); + const firstHostSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + expect(firstHostSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + + await act(async () => { + // Select the second host + const hostInput = getAllByRole('combobox')[0]; + await userEvent.selectOptions(hostInput, secondHost); + // need to wait for the debounce to finish await new Promise(resolve => setTimeout(resolve, 600)); }); - - // as we already have a secret in the state, getCredentials should not be called again. - expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); - - const currentSecrets = JSON.parse( + const secondHostSecrets = JSON.parse( getByTestId('current-secrets').textContent!, ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, + expect(secondHostSecrets).toEqual({ + secrets: { testKey: 'def456' }, + }); + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: `https://${secondHost}`, + additionalScope: { + repoWrite: true, + }, }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index e0d714ffda..e653be9178 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -49,6 +49,9 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { const [state, setState] = useState( parseRepoPickerUrl(formData), ); + const [credentialsHost, setCredentialsHost] = useState( + undefined, + ); const integrationApi = useApi(scmIntegrationsApiRef); const scmAuthApi = useApi(scmAuthApiRef); const { secrets, setSecrets } = useTemplateSecrets(); @@ -128,8 +131,11 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { return; } - // don't show login prompt if secret value is already in state - if (secrets[requestUserCredentials.secretsKey]) { + // don't show login prompt if secret value is already in state for selected host + if ( + secrets[requestUserCredentials.secretsKey] && + credentialsHost === state.host + ) { return; } @@ -147,6 +153,7 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { // set the secret using the key provided in the ui:options for use // in the templating the manifest with ${{ secrets[secretsKey] }} setSecrets({ [requestUserCredentials.secretsKey]: token }); + setCredentialsHost(state.host); }, 500, [state, uiSchema], From 924c0a6379fa9a2c521622259126125b0bc2ddc0 Mon Sep 17 00:00:00 2001 From: rghara Date: Fri, 19 Jul 2024 12:53:18 -0400 Subject: [PATCH 19/77] Updated document getting-started/logging-in with missing login screen screenshot Signed-off-by: rghara --- docs/assets/getting-started/login-screen.png | Bin 0 -> 108521 bytes docs/getting-started/logging-in.md | 2 ++ 2 files changed, 2 insertions(+) create mode 100755 docs/assets/getting-started/login-screen.png diff --git a/docs/assets/getting-started/login-screen.png b/docs/assets/getting-started/login-screen.png new file mode 100755 index 0000000000000000000000000000000000000000..70b56f399a22d19ad863f77db22739f67a352501 GIT binary patch literal 108521 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�)jB)zjg-BkI2qsI)C>@1lMVynsHA}KMv8q775kN5y{sWPCbNovk zy;YvmMm@e8`US+0M}?{m{i-;nqYGtQ@Dc;kCl#fs6mN*}?X)e0&^?7ZI;EvyDtZRhAY!x+7v*-=<2 z-25t!#`y|D$u)AqqtD3kI79N3BQAT&5hpJyT--5+1+gX_QAc5|97T}csKB$y5U$P5 zAO!Cr2sReqXx1~3xx{#o%D?pA{+@;nECUcIW_K~~Jf7&YW(U%slHW~sB$>XH_%>*1 zivt?=&xQ<5mLR~?NZPWQtr@hG4_FwSov$2uL$6KA==o%f*oVf+Z?}TI&O@btz}ud%65D2Ou6hY1^Ub z&->wJ(WBp!<4XeDbSN#{SM^uwNn{KitF_+(%PazK8LVOckQ?Dw1>&QBK7oEmv<6+~ z_2=TX@V^fWz4D;$Nve6?5>Q!^_Y~b0*HlE~LWA0)&t7m3b12Q_k@@%&k|=TRfu1$q zq2G`vbaL~D-uBTRp#R5}=Q);NaTR3+vK)rkA}zDIn^zz0;T!ws>kt|TY@#*&&v5!ilfmm%evBYP%eRNMTn$o1=bgU4f({H1EHxZN2~@UVDLy=KDb;^Q zRl3Ms!N##b*N~BF0>1A3J{ok0pC@kzc79EKckW%+dtD6vT8 z1URm{u`}?s(f+2bsrrw@OXr&=e!h0mr3&|ANqULoqGhLf-=7*f0SklFxBoVDdlpIl ztPGWZGV{X)%TOaQUX`_W#5nm*n@FQzNw@=vOFcAzX6vg*)@G#%MN6qv#YWDUH(GJ? z3bA*rg`%eU#*Hy-^JG_)d+NdE%GnsQj_2jbV_RLyz-I zQIbK1v%2WAzde1uP~=H|)vkWU_!E*mfX2xy+l)wRp{ovT#E|UK)ecyBpF@8tF)*lJ z){gZj7(zeZp0L^Mwu8A~W!35ON{}q~W(jFuQactwynk!A=p#I9;L~Igh3s_4@niuh zcl3H*3*(!PaeMy?wt@Za!c5ch-@pGOYk~0XNzPiVTwLs4cYa}G&bG#`>7Nzj6{!JY z;hgPFKS`7;N6oShC}oS-jg$8l3QA}y;z|X@6}CkD=BNNIYyA0!3CVBW>JcsHt!F?A z#I@lfG(~56ul18%_z1>h5JRFcg71TAWoqUx7IgCJGBP(50s9rkg{v2TwTWP#bdT$6 zwX)sP(!W9TUwnN_>zk5ASEv90V#jX`p}f7~@zR!N*wQz`5N>cDG^{NAh1~DP<148& z-is=naes2;!g>Loat~vl07X5|uzesS0W7p_kb14`s(L23dXAybgv0kB&T|7PND%jZ z3bz@Al;T7Aahi1+@ta{6!-}bS*r}70+3qe0VHlRBmc=C`h|}{Sy5u6CLRUkeMLj4t z(tg)bMaH(hd{jOFbQ1l3+Kcv1`7=UtA+l)YS;G~ zL68Hw3h!U|Qk3LVTXqU2uQSUf2YPz!LJ8SNF>q!=G0oDVHHJ}=_Z=?!-L?I%H*6)c zds`K}sh`)5IjqVSvg~#DG7N`aC8wCKZf11sU>OGn4hv(ViE!-03~qftaHZbH!-I}W zg|n@Lnl6qZMmh0S<>>)Bxx}krYFJ?!duP1D99lHU+7GFd}7%V{_Lyk)C z@CFP{c)*NM*<~r=-NxFs_4~6go~sMqrmDn3d+vOX{kJayK4A#m_GxnL#$&Ca+v}fR z9&p$V$UFQIX_TZC+Hh7%t`&m3DZ_x!Gi~11#Lv??Z-}JEP%47%$2Ub!ToItf?VBh3cfYtP5-ynF$6^0;X_J z+aVi22_i8`FjvbX7kG83XIXN4{AT~pJ`%4=3%1G@)2Bl41JAtE^u9xe&D|#3;fC)3 zd@!aU<`B%+o_Hx9sgwU~xyO1*wM+=3_EDHu_Y<)Ijxkxi+Xa7?bM5rz7hvdq_pgjf zoO$Su$moE$8Gm_+X7Z(W@{X-9qB4>KWA&mmSR~B=I$7re559Upy*gikwbR65)2pOE zFZ0f;b*3dw&lvDCF>h*hB?Dh(dV|DNaVQk5za1v1E?39<2AZ@SF}Tmpo4$d*gVoQD zl+5=4b3ioE`a??xrJeg}Y#Pkf-=5;uh4^!uNO_bFy_qL3xC-&%*N*B@O6#3s9L7|Hmm%V9>*lPv|b4JB%0@>cI*&cilx0)Me?Zb$r|OxCmSN zT0jvvd-yX}b5QB3o$Ts(k$Wum?i0%1Nvug|wD8gyUMorgxe)e6SqYq@r2|fnJ2WWD zbo->sVb@%734_00E9*||zrm{4D&Lzfk4Ap__-9fNz z+X7$+Sdy6)1mi|ADgbR%*ayUo>KMfWdQu+TX$dm;uDW`E&Rd8ff$rRXs5=va-M8MCov!fr65?UtI)T?GJoSq zf|1b)ABZ2?0q6RZZ1%6XN5)@YG?t<%_i zpSKdTZXb2pO&(T*5@XKW%E089yIDvRq(VcNcanlb@ZD|qL|~*Lm_2X!(OFO#`W;DX z{o_#iwF%1-hUN*=8IEI`?_$wW1pAcuC9~teq_Y)!q-@E|;)CR}feGX*_oGFCc#yNn z<7n*BlEU^@NE4rTN0-BS7JTNRN>I7E599#TYmeud3YpN07L8N>yZf{#zth-&AyuPL z8Z1&2cmc&=3#%`Q3GX*!f7`r6V0R%#E;!rAyeaYSvGX=9v-J(`S?m`<_U|!#27?tV z4xN~pADygNIy9fWD<8P#U!uJlyx#1%zCA$nzB>_di?zUe=^nbjS`RB2ZgE(l@)BEx=q_a#z}--~P~Cl;x481mI8d8o5;RnC z_dI};CF8=sur`KMZ8?mlLc;V%bK9pT55-GY;M>hcTS78fguihv8h|LduFRo8X-ILf&QUO3-!DfMHsen5`mmV?FmFQrDC1nk}P(YyNs(H^G>uDZkAb_dhp z(vf#@bIjy|I%31=CIK`-WC*Z7>z~~>Cb_A#K4Mb5A&J~7LO43TTuOL6fR8e9f=jJD zEG@T=kTZ87Qc0gfx%hMX>gr;;1>H#G-E&2e=u-vkX7HQq9SmV8ZKzJuOYaBoah|R5 zoPyJK5UB${IOH8usmUZkTBZd4*p&mEuS#%zB;fCIl$c(JvVW_tvT@U=b;fTEJr)_^ z`;Aruc@F~nb^na7p+Q&lb}qO37=h^&N|p}$N#pCpZ8zt;>v|jujc&mg?(LXw$ZX;f7lz5_!%I2gAv!MM}dyAg02(Q8Y$zszhWoA?x0m zQN50y%eK9Ec>%BIFh=!#yzN!U&cafaW((5vdCyTOsmLr+QR5`X`?A*NM|XFdJ5Gl$ zMYFM3My(FWN+P%DS0q90P>#~e3=6zz)WTgiu6Y4V*n9#CX8rYo`p=QNEdz%RqSttp z=D>h=78(%Uwb-IzxL!W!^*!oNlkI;21o(dkhi`W57c0l(PI6rRK6Xer>GYNzj$QiY z@44xUMQ@>3jXiX)cu?!)dL~N#;BYgFbJEuCF#E0mI^D^)Zm;#r(6a*tMzDOJea%>` z4|s)DeEXGP-bSftLL&NnvM51#v|+CJlj>@EQ+z0@NA z4XJKBduz}-LigM0CWmqUxlT%S0!ObcQY1-}+I8nw@e7wh_8H)p^Hd#TORmlk0qr)G zG(kb5>lse$()rK2{H5i~u=1l1{Ftg8V^K=>2WNi%QG7~~;YYvkcJl-lE;7(XMG6x+ zTR&>THBveg3Z43|g4~v8?I{0T`oS_BpOcZLu0ON73_#gb4mP;xDTzN+>)X0s_$S<9 zJE`#UzG35^ZMk3)9rRJs%qkd17#0U09%TVL4X#TmT-iHdEM~+(q1`nJA7{!xgrAPO z*<^+CwLW%cGG{0D1Qa##{2=0z)n>9paC}!&F zQB>|R;T7$~<+md7%UsC3c?WER6~=^IQziUXA74VSHP&W7ZLDs0SOKHjYiUQZCx2e- zcRbPlOG7M=o?m7aCvH#tqh9b`25^~}$kzTNPu*qKKhcsuEfC}GDvWR1wC*RxGUHD% zh|WS7mJB&R=7ymsx8bPjZiDQ|)u>b513`uF2Qe~ocQ&PScT;jDgfrnSsU)))LEy0 zisnEz&($%1pGrXP1?^?<#Zn>6@d2I5VhsB@oNg6bBB{;pawNHD_k4ZnfH=Rd?zkcS z;n3Uki>xq~G;fXzK-JBozKVMmr-?CV+O=8KS=)#Onv+2;zxMJjqLjg|(A=RKy%eO@ z0;L%p-e?(q>luo=(tEppoLaM!c=YK4^D_wK;2Yk$jh4h;!-}$({@Xd2;qgclj+-z;n@d|4eF8~BD!uUdU*Vykg(*-0sn!O&-qY6CTR1EYLpy=SRojvaR{19elR z=|w^r)`23vzhW5A#|h4jGuRc}>$}x74xIq+R>6aScaK-ZJR56FKOM;6MQ2&O8lxxT zU*Pm~bAfMO8&!>Kdxa(U4 z>5y&^R2mt&K}0~hLmCn3ZWuZR0g>+RR=S1|knWD5Q@REinCHUZeZSB1$NMLykzk7f7>O#^=0P6KtvrYBWeb}i(u}k-Qy8DS{qgU~jD1&VGLelD$5p=h3u!=T} z(TxAc$8b=(3w%$B>?8^lyk5l0*WVi>tj076U3o-u$n+h>t8dQiPQ#h$N1)B%*l42Ria<)--eon+hB|}kZo%P zz8Z%bZ^M~C-F>2+H1CMw@cVp}TRHeP+2s~vf|>mSd<0D3AMp%443rIE>qkG&qrJz% z@~P3gjtIKnx-a@ z5*jMLHI^6V9DAcEH#&4r)LmYNdDBc#cmrS(PNe~@bnHmQ^;bYw8Nf(T(b1en0~|PM zM4X!OyAkqv>uoC|b<5f2NtH_Ozxf;`6z;VDn`GFiTq~DSOuBu&!0L@R-}EFbzR5kE z)Ly$4whL)GGd;Y0Sg5;0#fI`;2e@wBjdlrMi(z7kyJ%ZnuTg^T*0Q={v{GqqwcZ-U z2sy6z6Ml868V3zrfZ&r!CIf%dj0c>nx>?M-&`!FXue<(yRPKzh9SLS*aTk5=`~AMa zh4w1wfJ2m4X=T0|ItmRVuZ`!WD!tq!wX)&3C zlbR_0`cTFWOkEcmOis^=j2D6+{>(9`zN_8gy1T|m!ZirHCi_EsJ>>JFCc-cG%Q%WH zZq`x@!(laeRZk`jS%t5M4nH|m5w4MSbN0zagzXqT+o1KOFm&KUFthpH&4wua2hxQ?tnj~1(lMCc7AM1FC8u4g2S-s zcc*Ju4V~$JDH9kiNiC>HLK9Og6&N$+I_R^FjPGy>uQ_)D?v98(R{@O$MZ|BryMg3( z3(1p4{&J(+4^9xB?jyd`upp|c)pjNzosJ=>iy+P0TDYHbuq`DHZB zTv7RIaaIB`V>nW7p?|I2cu}agx!~BBT;8}th!%aL~B~a8pvu?07f81WrBZG z!yE?3Um9=PEiYlPWIa%Bx4-V=7Qq~?P}fsJT>qIq~y6)Qn}OzwZV&qQ9p9#e_Fob{>#kQm>i8Hvr+wJZiUjyag&1EwF3 z6N*N&w7X9Ca<;RlRg)wB6y4S3ZuL8G_XKQ=vkWcLcaC}8Lq;~v+R;bix_Q@SX*A>+;8 zXuRL{*o$&;r`F|cn~0 z3Ov(K#5p5&KTNy{ZEQ;Y+l>~6=j-QbLi(>q0uB&xdN#v*r1A$FZXP@6lR-l-BC+S^ zr%}X31=0Gx{fK*UkS5-=oA8*beyGdT+I1oaZ5&Bw=AcK{6ogOy*2_6;>f?ja zxyj}<;c5r15yngNWlV7o7-u7b)-Ht9wo3C}HK`Bjj->6d0pkBPM(i0L;@no}vLAJB z4twn6=p6Jb=dwkWT(61Z+a*75ox?YD?l&8ovN>Tix39Y$k#t(lOp=?`{u;0)*&cWQ zeKtzTdZhm|>DYNn4D@10XbgqbFdgH%lF(%A>>h1&H0)Vu*n(>d?Njsq^QR4w-$@Ah zMVdl~?x(v?0!yAL@2vFN3-kKI`C)s3{Q0xZ)Dw#|t%MAsSG8$qGyNjfUKmRZ)>czn z?&Z20DQM9oSgUVumVYJB*Q3%rEK`m+eb|P3Y}uaIKclVm!v5mJk~D~I-w}ZtL%dKn zIKJ9>g=f6h3hf}^t&(4?aT0@HD5@*kvt!R)v(hVq+V|7Uo$8goi0R_NjtoP8nJywa zTa6wt#Nyx7n93jzdBa}oBskkmb!8*RsMV)ghnE7L8ln5kbsWuY$u$}3FL0XCJ46Fn z;(%j9O4)<$U){<@ny`*nIZp26F3{fx94pvK@JZ^TEdv9jB>0j2NjvxJvMEBQ3v`rzZzB({MC^_t$^092>Q5&%x5_tQz@w{&#V zqeq=05WJXEUx?~8-ONEYR>M}fK5A#f8A<$p`S&Gmi`LcvJ+LKl&jo&QHhl@Mzj3y( zvEoWB8!=9w zBg8@kOV^JLP<(3tLLp73r)QeHIELW{o5e{t0VdPapC!TzoHS_R*#?h;a;9>}y61hf zcuQfG4C}LHhXh2LBZ#DxT8MBP&oCQTkDUr%6WL(;wYa&0;8>wW zsd^8zY~E7QSQjuc@G&uHbw6@#EXx4PbbBa(Sy>b>ax?8VK;sxW4;!i%c!*PN@lXdc zGdp_VS|jcr7{&21pQc8~(|18lZz#G;^|BuJ(7BW*lK3e17k0s}17FVrnLSj1zVs41 z>;enP`kc!s+r55S2qr0?ja$ZD`#lg?c`2Yr|_ZBP%bhsfVq0o&OXCk2y5v}*WA z!8bWzXytMdXagaZs7_q!9?)qNyH@B@gq-*k#8lceX&4wuQu(LkJ!k@s1i#`xH)(Q- zojLq)f4q3|S3agQhPDw$scDM^wjUq48h-bX*pjZn;gsn9Iw_Rbq2akc?z!ohTaD{{ zsrE|Z#{7_;{a+%5#7Z}wN*_Ys(kX=TLjjWKO-h4F6nCQH^2l8il<&vjJU10;a`WjO z%$CvM8@TzoAsMFq5b0@1kfaq23-^nGjzhy7DX#)ZGi{h8du zkT$~1Mzq%3$I^{`76@4=dEDHn1s`p+0xhU{$nt^Wf_Z56O>JD#>CXr)VeG*fj_N*x2v#1FQ=qnK1F(~ zIO@$ zKpGTLQB}t7)bJ4)9f5i{BDT(9p`1dMmcs2Or#2Czu%YmH~YR^-NtzNja^zp0q_0(9W^>(yC>4nT_=yn4a}!Q zWh)ND+ZwA_LdCqeC?E<4q>cV2?E8eBZG3NqB@_q11aTdtV%s-AG=mkbXC2AkFZi4~ zG{OT9Otx?uI=BfLJ8hu?xY0+8lQ%V)mLKWRmhGv3`!3-y^hRfTcXP^QVm25!@QR#o z5Smoym|?DkKDm6pYkr$Nkf?0Z1JO<5=DdD68r?%AQV*SopIk5J9mTqXe(ApUvdWpQ z)TRA>HjH6l*&5u2?!HtC(SzJcdU)+yjEa2!m6=}o%W*Jth%3BO&<2Wa&8~OZq#;?Au~qyG z2Zv*TlQdR$zPrf2TUTA4-G-A9B}w8#(46BKX?9Z11$3r%Ue(7t|Dc|XMM-$J>xr$P z*JNM*KDbqTPxq1?=GB%%yxHv6dH<90oyeQdl?Wy_J~wu4q`PBpxtX(O^7(4qBYw6; ztAkaJtmINVq3)48%fSJ`d7IE{=y~xj{R7x6a>erBKv_AD^C4X0 zhv}QgA46Y=M@iD{P1mKJs~Rs=IUC5n-J=>PT1O`PThw0obHY+Kl06bcGq=SulD8!% z`?YomuwL3470sQebd9yP^_0B0ZFMYTN^6V3pytrj{1uY;9$*ky31~=Npw%(-Y`Gfs z%xD8={ofOUa*|{gNu{Ca(hNhTp;1`x*4#|Liv|a{m9f&}rB!`ubd?H>2Mq(K_=u{~ z+vlhYuS>1!i%(zFJwB=lSo1kUf~H(Dy_hRQIFK$^N`x^$`uN_U|04(#o|N^1 zB#3IA{)FNjzw!5BFk00?3^HymTqaBB&aI_<_@~6_^KApM}#{UwxkLS7DNywB->Y z23ed4kg^lw#3p-0GeR1VET2So79vsN%^(g;1 zf$*j}zjs3YEq4r&B7I@bI*#2oI<8xjn4%c z$CS@{F{!9y4z-Vwlb|l7k|nw;b?b}5f!V8T**YhJ75@$^n6YHUH>11 ziMX>^@zTbVMy+VQ$)Ce>w&??1ilHP)xkD9PZB)j&Ql*TDpizYoCn<9>7Ma0kZ6YV< zZ39JMz+J^%5N4=!dsDxqI{^$?ZHZfD_LnPK)SKRINinl(p_>fSf5X!vGC^GGSO!B? z_g}Gov#D2C_J5|evKV= zTK`JXyW3bgI-H9=;^y#t(W#zLmD5jPr-JNQOC8y#`P$`Z&&IJ~o#6Gy>64RVO$N-v zY5wg$&t03dddY`sNBVp==bAN7`m_@}Gh&7U?G5=Uj}U+0SFnx%#x7$=wN)Vxr|(BX zfNtqJKe|@I$`N2GLyg-FnOgLm(@zPr;#x8y%5!}=?kb+_udQ zrMQ@|xV!5R-(~9|Vi3*P)d|i^?9e3_+#|SFH=W+_L9={g5cDcpnY<1TP0S3EtmHt06QdEbY==$Jo-s->Rv)yfsv}laSlccw9 zRk*;gTe{%8(2wr=L!1$=+Vn{Rxl~f&O|ZU{!HPGIyPz)3wEai(_N@M`pSv2!qFeA8 zxJ|F3eL9DRl02%a820zP#KxSC9JKVyp5@J{U<-s4nU~zvAd*XZki{_nm*Ik^&8`g} zN2Cm**#_N3a2%bRAV0x+Kh>my4gdFi39lE4XSr?q4_FC~Gnu8HvqjS?96MvF2VRjw zqwdYQ2jK?eO%;)2>Bl5rEvXmX9TLsRLYdvGMV5ZrQray2>?vWJ7?d}--`+TWp_X1) zCaldy`_A=T5-;YLa?t-6VS;qH@(TasK;B5KX=vg=DoRHQ9a59j(ru=!Pb5(p4NmPih6Tp@&89Qp)L=7+P zR+dt=POcW^rzw_3Yc7J}EsT;*Q^d*^O`qu0gt=i7=a*Z7GrzW1 zk2`BQyc;IUmRtpf#5`#?9ysyF(r6A3qC>nnMEd%X4Mfj6q=%60At=r?z7&bxfcjv} zncz{(rr*q)8?0D9uk~E5?C&uj?`3D6xATYLQEVyXr6eNJxR(J@R{V-<`{jz?q zineNXqk;V4yk^*mKC6FGR21@T8={!iNiR4MkE+vN0(b<}Ud18u|Hl~p{hx%Lj>Uoq z9FVm6_{;fFN$DAW&JkM8C2)n=B=kM)<5#O;6|zyBDj2wTQ=Kifnr|-7C2gN?&Lrf)b64Tb1y4*-!ID4mnA!;mC%JM3H4_fLt z%^Q&gocb7rNskX z3(bVJx}Vck45mz1p{84Pwpn=Ws0v&bTsyaf9W+v=gU>ie+@2l=6*wFK?!t>02;SY2 zoA8kSEeqRSHWD8hOUBRLB~m5CZU2~zjijwqz&6?ux88MtrFjOrq|>dgbzmLhntmw# zPJP#JtH)s%XSS=++ZUNTsRjBTW1HKwhuq}|^=0ttk!XcpRLucD%1&8oHzP-vKPv+Y zgoPW-{D{OCbUz$1y`wMXVziyY_w;A#@Op~N_r-W;9??! zd0NNK$9?!{LH#+M(@>;jwsrzl6gq0a&;gIZ`G8b4RP(5zNFf56H!wZ8x&&!F7_`tm z$a#4c(0*j`a|$o8?3OY*VXkV)m5*1rGdbPtm#u>Nx%@L>-JeO^>d3;bNqLzrVHpa` z2OnKLwXMSA|DEIL5<;tenXwi4Zaw$^N7k*)-vu}?8rW;dn68Vr?Q-Lc7qmALzI{M$ zCGkD1jo#*CFtN1NL2mWp`I+nM2;QuOggQ`ZlDg%XE&DK#86p9+vecsxOCK&u3ufS8 z*JF*a5@1@!h^wk1)!Li?vA^N*rO97s9IOe@a+`$zIcde0ZL&dSrMMDfb{el=W{u&R zC&sKvWpz<&PH#kN?QXET#}zcsf5)g&iJPZS98smP$@VGPCq~`4Vd+yH?;}lrG)z)| zGx)G=*YnL1QOc>HwyXJCnrSjet@m0DfD=n+%<}uC=BWD8zf0!>c>%rq$#H-e7K4Ioo$>aL+cltiWGbv(HHR{yZqq<6yZHygRcFo9!kRw!&Gqj`*>LM_q z5@Ns?r1EhD56(AP;?5DO86%1*|G>(~GIivMvJ_2;)s1G~Vc?gHW4?U#Qed+b%Q%bD zJ|r>=Op^$%?u7Z|4j5bqVclD!I{oSAM~m@)>9yl4{P|FPr-Re|0rg82{H+qk;9zy{ z#ai}$ro-5Z(^8Q0?H@c=@@$*s%H>b6v8goOSs?+L=-Zqu8pC$kJ&Noy40BIT59alP z;3U5hcjZtM!#WEVV8~cZI1+3!F}#+I4_Zn_I|?;@)REuQWb=#Y*75x5KORBo@9$qZ z>ehPxqP*a8{dhND1X7L;%~_sMHp!sU-kYXoP0;I(w1*Y%Z&IdEuOEmxEYPg%=!$rC z(itslN9(C9VufD)*r53fHDwem{7JKdh@-+WzFt=PZc2l_LfmfLNbgtw0Cg${k3r|o z&J_1G4SO`{%K>zRMms+6f1kcvdj-VA7g=DE&6U;_+|hjj-|lT_arE%lki#pdrtZR0D{rp*PD^<_O+X>uhGb*=RRgz+yL<{OPqdwVTVRJ^a;9wj~ z@_(JWF^ShTevwS`X>h`M+=n_fhGNH82bGbJsH(X5ln#fxnB6*E!X|WRKC>X}&RaOO zw2!nCRI=NSJ1i6B1U3V4Q4sSQIu1=uFb%Dib4-6Uny-&KuCI4GM+-!ZpJcktW_Sry zKx|bSMnO0Eq8V2VuO%&(EK6QsPtRD~*tqgpqwFkxR2FdmD>0^AnbdQI=xnK~eIuMd za<7`wlZXoIORIpos{%U6az}M&2abT<d2qNVpZa*9J z9E&C);1lGN*c-_!xF5x)NP-ZV!*2V>L9>seaV8fuG|RXVH&$3BeF}?JD>$J(iox1L zq>*S=L|8$V_b>7;!I#!a9UYxfg}2i z-`@*Fy(KgH>WFsCOU}hfMP{NS==9ngCC8e<2gcIDTEUXsf{&7YXH0wG>EsArNlRuU z9yUmWiU)qpZr*N{`I-HitK5*Ih}C7;)kr|^O_1l*=ZZxKoSJDiTrmn-T*LpHeG&cqd_^`Ep{QzNZw)aK)TBl8ZXB3(<6;mvCs39L$KhAaZA zJ?3~4|M;PNSa)4p?IDZM!*`CZk?5r1J6d+P?MeS)Up>kJq@~GG|zcU#20RZ>9 z^(~N;sWWBjS=#bau{QzjE%^t1fD1Y&G);qF+@k$af@Dj`yZsN07nogLTG}f?%};HF z$yjDH&OOF62d^eQMT(;EeQ$kOD?mwBhjD{JFFs!1J9XKBh-Vx5Q3(>%eGh1#s{NNs&I zyiE5u3ZnZeWaFbP1U27{pPM<{6GQM_r^I?IuV)_44U=2fO-<#0-3Ik`vZWl=irRzl z{+iDrd_k8(lA`BN+EQXwcl_Qw@P|Km>?{gQbydg4tuAHSHBO(S>??ptTXIms+>a;8mo=;YkiQa}c#S9a;ostoz5NesLMhiLe58Ngd_$(AM+m0x|( zRC+i&>V1QuU17-7=Tvf}fdRPE14@-Dw#JPT9~PGmbw}G)yE>hg0y7{YG&D4^dWKKI z_?5!PALgLnQ&#-$WIc2&uFhyQ!1yCAyI=w7Z|hE0?Th!{GxBh8y`jE5YLhm2mPlvb`L+R00c*k0V%_X)fltCdDM_9^$)uubgl4OCW0$?Pt2>0 zAdFjId@Zsy%JMl;>N*f${b43f)zvkgoM9iE`zwa0Iw;E}ZghXYtKclcDP9;Vg`-V$ zarx3-ruXeMQ&3!{TE-zdhUpQ%m5b}}awbHXU8Z;|tmL(<4iG@%(jmve>XFyhV_GVe zCZF0!Rjui-E~GWunfu_}MVh_usHtf~_wzBc;8VI<;8*OOJDQ|7w86x$7ZR#@q%iQu|r0=WmqN%=D%UGaBUard=1jT zwY@=qxOE3>l;9$`{SJx_j(}r~-KPgTkj*atYHHP>eYpouL9zI%h{sv2X?nH50hKF* zh{q)g8KkV_M=7zsP~Nc!W=vYNe4l~w-v;|oG-Yxm$@HSh7Qw!)&Jsf7zatNeqg~c_ z8P@@LhgJ=a^#VH)+ov~cXbIcD73NnhBk2|wcl~1h7sW^n zMfh3+P26hL8rq7Kn0b#Q;Dx7Oj~j|B`iutFU*`~80=K^9XY+zb4JGQK*|T`lA~_cs z;D}QSsp#XNcw-03_d=g2XZVC4ScA{5oE^%j?B&N!0YmvC=QT^qv~^q^0I3xIp!4mQGxmB81}#Fjcqq+x5H>$=eO$jsVJUL~0=Fi7-4#?I$Q87hFF z&%`-?TIIGzF}5bJLX(_)soa;K*@ynFT)Qw0KjXuqPgR2LfW*&&!>1CjIX#T3E>!#l z>k)`zlWkBUSNJkcG-%&6dAz8)%t|QQFk+1X`d+0K*qZ?2(y11p(*l@r5>!n7M?~wL zdV+$od7QHMyTkDU*2mDa`p5T+#>wv{E1?BYX0;JvtOt)ll>@C8CKm9|%wWG;pOe;$ z*xR_mE@w%_80fpRHSX%!Q9lmqkY>}hSwiK2D(S4nSueSTd zvU)3lltoKVuRkOS9Cs~#V=vKyfU&5q$Mhx$2Mt~%E-sdR8ct-;X@k27JX}{SzOHP( zuoIY_dKT9e>h6b!HfqQ=amWMfCYI^yXq=8;6!-V8N=BV5NHaMGV(dFKcCZx<4=u#u zQ-L+Qz2~l`Yq9+=%k?O?CQw*2+i-jD$ETiUQhS`j_K2O0%S)6?b>QMUpc`FS?8W!x z=vE?<=%v1~t#wqLvliCXr&fQsP82-W6`Y&-%i%cK8F#rCD6SjEYg;m}exPj|-c0@# z0sSTL@N}{B(1vg!N#838ef;L%G@FsG=h?Dl+ql=)(51ik<+Q@uWV*AhU^2pymS!-t z@W#}`boy1slgd*;~TddmbnWCIIJ+MD`VU6oD2gh!rc?>Y;STs;B5$j0_jjW9~k`1N@*r-3Ff{&tq2=MF) zw#!mcJ`(Y$jKu2aiyh}10p0ode3YA=j+_Boc$?#g=@aXj@>tW(i^E0I1$$Ed%WzrF zxP37*eTo>fkwk*+^M=)(>m@Y5#6@>Kf+wwz6MR@(4Q~*sVs-r*w_xR%sk?wSx*hi* zMP>jutuAKSp`ph39JZg2SE4#+@Z!4<-43J2NnLW?4|miqnEC}5P?ynkqfR)UBJ}xB z^JQT_|6{maLR@=z>G>>~u;)N-{mfy#eP1)$>I?)SZp#1JkcW?F(^moFqfBNzC8Fm~b%doV@#*TqJ+{4uR!_m|Ilc*VaY7P=E%`XL@p zklKt7I%HDsdEOD~o*8g%M6yTMwK+th%?C)&_aKMg@Y$OfGjv|sYK(T z-ZfvDD=4t`+lgI!`19!OEH{1OhRCu)xxszC56yBrQsVt7E^Iu_1MZB2#$9nP3-zTJB3Dm(4b4YyH+i^arR_kQ7|nIF z7r3zyJnC!aU*kyIYM?{K3cImgmczeqJw1!enX5qcElz?vX|z7jKjQYUZ4X|~MHH*|VeZ^67L+b< z{=J&^dA``Ww-A!}rB0om{T$=!7c=kU6bVaKUH;eP^wLbBZOR$eAQuosZ*t@<6S-6g z?lR2-&c2Dj2>t)pMI2s`vQY@z&%O<(p*qvN{PlvV-HW1lJ4}aOMlp!A)>9M6p<@=& z*ez>wHVnlcyv`pv=kI>IyQbEyxNpWXXVsz{lR0u z$(69&=K$}dBI_P$Wr>N=tT;gQnw+Iiuk+9k1|MBZ!(wM~c)z5D@Igwym21=zu{KJ@ zk-7KcWc$^x7yQR`Fq8D)ep_(EJr;fcPJ`OQtA0C8DA{F!e`uM`&J*iMw^2u^g3a?! znRgQfnbU=tgHnb!_&WrCL<*V|!V|&VC=~xPg6!Sm`Bog^+@Bd!n&doRJZM&xgc5~l zBkpAb^9sb+-H?&ouQ3SR-7JYjNMK;%4%<(odKfC%HHt#OWsHBT5s&@Acu4Wwc)Z!)StlO< zm@NyfZ`5nzW+XNf4}R8-c<>ZVZ-W_~eAVOFm)$6`@5+FrW5+%0-QHgERm>Xo(ugKK zCcFkTy^b&+=kOdwj}b554OH=g#frjT(`N9=ZyZpxsrIA+PQ0lD1?j$P%*Z9rHg!ENEp&Ju+Ka!qcruR&g1qs5qSSg8=CHzjj0C@D_i&F) z&&r_$yY4cpRM}}yUn8)SR^Z5F_p5{_b1ewG8C!glDH>EhKD$snF%K+X$S#S%x{UhT zk0grCv}g2WPQshsYrk=(UAZ?P-ZspwRvul3EjzIeeeD&4hyJLrVdU9v?W$H9Us%!B z$t>QGi4>^yK~1)%o_;m+>7WkxsAk8Yp_G07^tV#Y_aKx<7I*X)BiWFWx4!LqxNBnH#TX5Z zAC8E4I?Lq!kgs`E{!BSQaJ4yf?d%k1J!u z$lB1wn-1?BE~+wv6NHC6v+YJ-u43*8D_4AB@Fv$CvG?TUHWxhJ>i%_A<-D4#cN8*j z4}O9}kNE5`P#9w}e}=Y}Z(SMEPeTkKANumEB+o7Nr(Z|fh2U!=hzj91nmVWbq3IXy z19~E;Wk#X%kUM57hwci8Nx9?08_WAKUr>jG{WNU+z@bd%30e`=smiNaKP(zpNBWzD z7HagQ)w7AAaL+6_{z#?}`5TXQGY_A(MzLbE-`j{zVS8~jZ6$F$+_XCx59N8o%KikG z-`{%hjF6FwMni7w{m`>YWZ5RJe<>rLMP@c4*J)n7x_I<|g=ZPD^wtLM z)I|K(i6Gy3LdpM_Fy~FM^HxhwbOU-4#*tPO00l5-{|j=+tUUfbEH%1p|0Up@Y%eYBB*EkZkPXQb692OC`$0f$ zfop-IoO{<`Uy|L)Rtbm8Vt-TCr)2QNk$l|9r0kemIxM z*6#=1YfHtG{q^I{62EvmxlTYM;n27AU-`|X`7Wyr+Dx({^!vmo7ozAN+iU+~D+Wx= z%^4wP)D=n!3Gx?z+F+w@JvbVc^o2I%Za60U=y)GG9{Yywc905#5(N&xCY)Z&y?TRTpdII-75iRg!YNBdX|J=7FYQ&<;9A@G?I^!sFUaus?e=ps zZ4TF!F_JH8T+4X6%G2Pq1+IY^3sK6;p90DCq`elDeET6{kIgyJf_?nC^oIKnfR*+1 zynEp)qlLV^>=(J19%FP1dYB!Vx!!PtAbSM%q$YqYjbeyG4{$uOVh%xmCnhR#qNXcG z8xcf^8_QP=Ky=KU?)7zN)p@f*q4GaDYl`b~=^F3za}F0N9$?-4@il4mU`avjPnT@1 zV2(?Ai#E`|6U#85_rZY^%Rh_Zquv(=5YB3(`bX-71&Jkh< znG8L5im%E6NS@El4Z53sG?eX~b4y{FTjd&ThOM3bzy&La<8A-eBVQ+v>7n=040Fy- zKM4j}0AFjomi!Pqqqg+S2XF~~pt=MmDL@zKBY*+OM707w!hJJez}L?E@%g6`9l2Tl zX>GJK4ABn&Y0ivKCH>On#eaGs08@DP$i^`NCdMuMW!H)yW_;qw#$;ZLGf#)T>h#`e zuB=mm=8ZIoe@f1e6&0vAtOlT{dIvyr(O{e)W4_9(50g(xo_~HMb9Gb~15v3dET8tI ztl;(JMgEd9bmkHFvm}k*@dypBhw8yqT7D>o{lljDN#9^qpizb|iN25v2<)R=aoohV zzq+vO7Q2BNg;RZY?4<9X^HS1=mtA1*O%y;ICAxiZ$)X`pfmRvrZ

yjD;9z)BowS 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 From aab708eb27b0a3809685d7d3e1057ac0f9ea8b54 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 21 Jul 2024 10:14:29 +0530 Subject: [PATCH 27/77] Added test cases for gitlab:issue:edit examples Signed-off-by: parmar-abhinav --- .changeset/plenty-tools-exist.md | 5 + .../actions/gitlabIssueEdit.examples.test.ts | 817 ++++++++++++++++++ .../src/actions/gitlabIssueEdit.examples.ts | 177 +++- 3 files changed, 997 insertions(+), 2 deletions(-) create mode 100644 .changeset/plenty-tools-exist.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts diff --git a/.changeset/plenty-tools-exist.md b/.changeset/plenty-tools-exist.md new file mode 100644 index 0000000000..0ce22a74f2 --- /dev/null +++ b/.changeset/plenty-tools-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:issue:edit examples diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts new file mode 100644 index 0000000000..7e50eac66c --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts @@ -0,0 +1,817 @@ +/* + * 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 { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { editGitlabIssueAction } from './gitlabIssueEdit'; +import { examples } from './gitlabIssueEdit.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + Issues: { + edit: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:issue:edit', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(1999, 6, 14, 12, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'myIntegrationsToken', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = editGitlabIssueAction({ integrations }); + + it(`Should ${examples[0].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[1].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: false, + discussionLocked: true, + epicId: undefined, + labels: 'phase1:label1,phase2:label2', + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: false, + discussionLocked: true, + epicId: undefined, + labels: 'phase1:label1,phase2:label2', + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[3].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: 'Computer banks to rule the world', + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: 'close', + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[4].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: 'Computer banks to rule the world', + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: 'reopen', + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[5].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 20], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[6].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[7].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: input.confidential, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[8].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: input.discussionLocked, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[9].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + it(`Should ${examples[10].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + it(`Should ${examples[11].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: input.issueType, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts index 861c0736f0..8686a66250 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts @@ -50,7 +50,7 @@ export const examples: TemplateExample[] = [ title: 'Test Issue', assignees: [18], description: 'This is the edited description of the issue', - updatedAt: '2024-05-10 18:00:00.000', + updatedAt: '2024-05-10T18:00:00.000Z', dueDate: '2024-09-28', }, }, @@ -72,7 +72,7 @@ export const examples: TemplateExample[] = [ assignees: [18, 15], description: 'This is the description of the issue', confidential: false, - updatedAt: '2024-05-10 18:00:00.000', + updatedAt: '2024-05-10T18:00:00.000Z', dueDate: '2024-09-28', discussionLocked: true, epicId: 1, @@ -82,4 +82,177 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Edit a gitlab issue to change its state to close', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + stateEvent: 'close', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to change its state to reopened', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + stateEvent: 'reopen', + }, + }, + ], + }), + }, + { + description: + 'Edit a gitlab issue to assign it to multiple users and set milestone', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test issue with milestone', + assignees: [18, 20], + description: 'This issue has milestone set', + milestoneId: 5, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to add weight and update labels', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with weight and labels', + description: 'This issue has weight and new labels', + weight: 3, + labels: 'bug,urgent', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to make it confidential', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Issue', + description: 'This issue is confidential', + confidential: true, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to lock the discussion', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Locked Discussion Issue', + description: 'This discussion on this issue is locked', + discussionLocked: true, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to remove labels and update milestone', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with labels removed and milestone updated', + description: 'This issue has labels removed and milestone updated', + removeLabels: 'phase1:label1', + milestoneId: 6, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to remove some labels and new ones', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with labels updated', + description: 'This issue has labels removed and new ones added', + removeLabels: 'bug,urgent', + labels: 'enhancement:documentation', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to change issue type and add labels', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with type and labels', + description: 'This issue has been changes and new labels added', + labels: 'task,high-priority', + issueType: 'task', + }, + }, + ], + }), + }, ]; From 1ba4c2f6da8412660a236fa3588c4fa129cdf92e Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 21 Jul 2024 10:59:49 +0530 Subject: [PATCH 28/77] Added test cases for gitlab:pipeline:trigger examples Signed-off-by: parmar-abhinav --- .changeset/strange-papayas-beg.md | 5 + .../gitlabPipelineTrigger.examples.test.ts | 320 ++++++++++++++++++ .../actions/gitlabPipelineTrigger.examples.ts | 63 ++++ 3 files changed, 388 insertions(+) create mode 100644 .changeset/strange-papayas-beg.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts diff --git a/.changeset/strange-papayas-beg.md b/.changeset/strange-papayas-beg.md new file mode 100644 index 0000000000..6ce33fb029 --- /dev/null +++ b/.changeset/strange-papayas-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:pipeline:trigger examples diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts new file mode 100644 index 0000000000..53cbfb0b87 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts @@ -0,0 +1,320 @@ +/* + * 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 { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createTriggerGitlabPipelineAction } from './gitlabPipelineTrigger'; +import { examples } from './gitlabPipelineTrigger.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + PipelineTriggerTokens: { + create: jest.fn(), + trigger: jest.fn(), + remove: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:pipeline:trigger', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(1988, 5, 3, 12, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'glpat-abcdef', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createTriggerGitlabPipelineAction({ integrations }); + + it(`should ${examples[0].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + + it(`should ${examples[1].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + it(`should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + it(`should ${examples[3].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts index d0840030df..f213fcd83f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts @@ -39,4 +39,67 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Trigger a GitLab Project Pipeline with No Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: {}, + }, + }, + ], + }), + }, + { + description: 'Trigger a GitLab Project Pipeline with Single Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: { var_one: 'one' }, + }, + }, + ], + }), + }, + { + description: 'Trigger a GitLab Project Pipeline with Multiple Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: { var_one: 'one', var_two: 'two', var_three: 'three' }, + }, + }, + ], + }), + }, ]; From e53074fb0ec07d93025beefa783f52de2ce21b4c Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Sun, 21 Jul 2024 01:44:13 -0400 Subject: [PATCH 29/77] Update default backend plugin to use RootConfigService Signed-off-by: Stephen Glass --- .changeset/heavy-numbers-love.md | 5 +++++ .../cli/templates/default-backend-plugin/package.json.hbs | 1 - .../templates/default-backend-plugin/src/service/router.ts | 5 ++--- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 .changeset/heavy-numbers-love.md diff --git a/.changeset/heavy-numbers-love.md b/.changeset/heavy-numbers-love.md new file mode 100644 index 0000000000..c18d144636 --- /dev/null +++ b/.changeset/heavy-numbers-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated default backend plugin to use `RootConfigService` instead of `Config`. This also removes the dependency on `@backstage/config` as it's no longer used. diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 3b808fc5e5..8dd31cbc38 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -31,7 +31,6 @@ "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", "@backstage/backend-defaults": "{{versionQuery '@backstage/backend-defaults'}}", "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}", - "@backstage/config": "{{versionQuery '@backstage/config'}}", "express": "{{versionQuery 'express' '4.17.1'}}", "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", "node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}" diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.ts b/packages/cli/templates/default-backend-plugin/src/service/router.ts index 312d7e252e..ced654aa82 100644 --- a/packages/cli/templates/default-backend-plugin/src/service/router.ts +++ b/packages/cli/templates/default-backend-plugin/src/service/router.ts @@ -1,12 +1,11 @@ import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; -import { LoggerService } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; +import { LoggerService, RootConfigService } from '@backstage/backend-plugin-api'; import express from 'express'; import Router from 'express-promise-router'; export interface RouterOptions { logger: LoggerService; - config: Config; + config: RootConfigService; } export async function createRouter( From ef742dc56e889f254983cd1a0575c0880a104aa0 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Sun, 21 Jul 2024 12:29:54 +0530 Subject: [PATCH 30/77] Added test cases for gitlab:projectAccessToken:create example Signed-off-by: parmar-abhinav --- .changeset/six-rats-kick.md | 5 + ...bProjectAccessTokenCreate.examples.test.ts | 256 ++++++++++++++++++ ...gitlabProjectAccessTokenCreate.examples.ts | 158 +++++++++++ .../gitlabProjectAccessTokenCreate.test.ts | 221 +++++++++++++++ 4 files changed, 640 insertions(+) create mode 100644 .changeset/six-rats-kick.md create mode 100644 plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts diff --git a/.changeset/six-rats-kick.md b/.changeset/six-rats-kick.md new file mode 100644 index 0000000000..23958a2502 --- /dev/null +++ b/.changeset/six-rats-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:projectAccessToken:create example diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts index 811926cc82..0762212809 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts @@ -51,6 +51,10 @@ describe('gitlab:projectAccessToken:create examples', () => { host: 'hosted.gitlab.com', apiBaseUrl: 'https://api.hosted.gitlab.com', }, + { + host: 'gitlab.example.com', + apiBaseUrl: 'https://api.gitlab.example.com', + }, ], }, }); @@ -192,4 +196,256 @@ describe('gitlab:projectAccessToken:create examples', () => { expect(mockContext.output).toHaveBeenCalledWith('access_token', 'TOKEN'); }); + + it(`should ${examples[5].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[5].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '456', + 'tokenname', + ['read_repository'], + { + accessLevel: 30, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[6].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[6].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '456', + 'full-access-token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2024-12-31', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[7].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[7].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[8].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[8].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository', 'read_api'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[9].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[9].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 10, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[10].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[10].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[11].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[11].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 50, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[12].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[12].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'no-expiry-token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[13].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[13].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2025-07-21', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts index 8d4f335a46..89f8ae6269 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts @@ -102,4 +102,162 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitLab project access token with an access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '456', + accessLevel: 30, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with multiple options', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '456', + accessLevel: 40, + name: 'full-access-token', + expiresAt: '2024-12-31', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with a token for authorization', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + token: 'personal-access-token', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with read-only scopes', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + scopes: ['read_repository', 'read_api'], + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with guest access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 10, + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with maintainer access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 40, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with owner access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 50, + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with a specified name and no expiration date', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + name: 'no-expiry-token', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token for a specific gitlab instance', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.example.com?repo=repo&owner=owner', + projectId: '101112', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts new file mode 100644 index 0000000000..34ce8878f2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts @@ -0,0 +1,221 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createGitlabProjectAccessTokenAction } from './gitlabProjectAccessTokenCreate'; // Adjust the import based on your project structure + +import { DateTime } from 'luxon'; + +jest.mock('node-fetch'); + +const mockGitlabClient = { + ProjectAccessTokens: { + create: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:projectAccessToken:create examples', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'gitlab-token', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createGitlabProjectAccessTokenAction({ integrations }); + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + }, + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should create a GitLab project access token with minimal options.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '987', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '987', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with custom scopes.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '987', + scopes: ['read_registry', 'write_repository'], + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '987', + 'tokenname', + ['read_registry', 'write_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a specified name.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '2110', + name: 'token', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '2110', + 'token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a numeric project ID.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 23, + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + 23, + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a specified expired Date.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '123', + expiresAt: '1999-07-14', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '123', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '1999-07-14', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); +}); From fba7537ad1fd83d9578d71ea546b36b3cf1be9f3 Mon Sep 17 00:00:00 2001 From: Jordan Slott Date: Sun, 21 Jul 2024 15:31:35 -0400 Subject: [PATCH 31/77] Fixes #25709 Memoize filtered entity graph nodes Signed-off-by: Jordan Slott --- .changeset/light-pianos-exercise.md | 5 +++++ .../EntityRelationsGraph/useEntityRelationGraph.ts | 12 ++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .changeset/light-pianos-exercise.md diff --git a/.changeset/light-pianos-exercise.md b/.changeset/light-pianos-exercise.md new file mode 100644 index 0000000000..4bf161eb06 --- /dev/null +++ b/.changeset/light-pianos-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Memoize entity graph nodes when applying an `entityFilter` to prevent repeated redraws diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index af2a491ada..07e3085f1f 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useEntityStore } from './useEntityStore'; import { pickBy } from 'lodash'; @@ -104,9 +104,13 @@ export function useEntityRelationGraph({ requestEntities, ]); - const filteredEntities = entityFilter - ? pickBy(entities, (value, _key) => entityFilter(value)) - : entities; + const filteredEntities = useMemo( + () => + entityFilter + ? pickBy(entities, (value, _key) => entityFilter(value)) + : entities, + [entities, entityFilter], + ); return { entities: filteredEntities, From 4e79d199ccb03c1b57dd4b889b83c058b13fd399 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jul 2024 19:59:42 +0200 Subject: [PATCH 32/77] backend-defaults,test-utils: use, export, and test health service Signed-off-by: Patrik Oldsberg --- .changeset/gentle-dryers-smile.md | 5 ++ .changeset/soft-gorillas-refuse.md | 5 ++ .../api-report-rootHttpRouter.md | 8 ++ .../rootHttpRouter/createHealthRouter.ts | 6 +- .../src/entrypoints/rootHttpRouter/index.ts | 1 + .../rootHttpRouterServiceFactory.test.ts | 74 +++++++++++++++++++ .../src/next/wiring/TestBackend.test.ts | 9 +++ .../src/next/wiring/TestBackend.ts | 7 +- 8 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 .changeset/gentle-dryers-smile.md create mode 100644 .changeset/soft-gorillas-refuse.md create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts diff --git a/.changeset/gentle-dryers-smile.md b/.changeset/gentle-dryers-smile.md new file mode 100644 index 0000000000..b061461da1 --- /dev/null +++ b/.changeset/gentle-dryers-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +The `createHealthRouter` utility that allows you to create a health check router is now exported via `@backstage/backend-defaults/rootHttpRouter`. diff --git a/.changeset/soft-gorillas-refuse.md b/.changeset/soft-gorillas-refuse.md new file mode 100644 index 0000000000..323c4d20d2 --- /dev/null +++ b/.changeset/soft-gorillas-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +The default services for `startTestBackend` and `ServiceFactoryTester` now includes the Root Health Service. diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index 329398a7b8..36f3aa3ab6 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// /// import { Config } from '@backstage/config'; @@ -17,10 +18,17 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; import { RequestListener } from 'http'; import { RootConfigService } from '@backstage/backend-plugin-api'; +import { RootHealthService } from '@backstage/backend-plugin-api'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; +import { Router } from 'express'; import type { Server } from 'node:http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +// @public (undocumented) +export function createHealthRouter(options: { + health: RootHealthService; +}): Router; + // @public export function createHttpServer( listener: RequestListener, diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts index 007d1dd9cd..3772ab0fab 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts @@ -1,5 +1,3 @@ -import { RootHealthService } from '@backstage/backend-plugin-api'; - /* * Copyright 2024 The Backstage Authors * @@ -16,9 +14,13 @@ import { RootHealthService } from '@backstage/backend-plugin-api'; * limitations under the License. */ +import { RootHealthService } from '@backstage/backend-plugin-api'; import Router from 'express-promise-router'; import { Request, Response } from 'express'; +/** + * @public + */ export function createHealthRouter(options: { health: RootHealthService }) { const router = Router(); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts index 9c2d96dab2..e9aee8a0e0 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts @@ -18,6 +18,7 @@ export { DefaultRootHttpRouter, type DefaultRootHttpRouterOptions, } from './DefaultRootHttpRouter'; +export { createHealthRouter } from './createHealthRouter'; export * from './http'; export { rootHttpRouterServiceFactory, diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts new file mode 100644 index 0000000000..dea0159dff --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ServiceFactoryTester, + mockServices, +} from '@backstage/backend-test-utils'; +import { Express } from 'express'; +import request from 'supertest'; +import { rootHttpRouterServiceFactory } from './rootHttpRouterServiceFactory'; +import { coreServices } from '@backstage/backend-plugin-api'; + +describe('rootHttpRouterServiceFactory', () => { + it('should make the health endpoints available', async () => { + let app: Express | undefined = undefined; + + const tester = ServiceFactoryTester.from( + rootHttpRouterServiceFactory({ + configure(options) { + options.applyDefaults(); + app = options.app; + }, + }), + { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + app: { baseUrl: 'http://localhost' }, + backend: { + baseUrl: 'http://localhost', + listen: { host: '', port: 0 }, + }, + }, + }), + ], + }, + ); + + // Trigger creation of the http service, accessing the app instance through the configure callback + await tester.getSubject(); + + await request(app!) + .get('/.backstage/health/v1/liveness') + .expect(200, { status: 'ok' }); + + await request(app!).get('/.backstage/health/v1/readiness').expect(503, { + message: 'Backend has not started yet', + status: 'error', + }); + + const lifecycle = await tester.getService(coreServices.rootLifecycle); + + await (lifecycle as any).startup(); // Trigger startup by calling the private startup method + + await request(app!).get('/.backstage/health/v1/readiness').expect(200, { + status: 'ok', + }); + + expect('test').toBe('test'); + }); +}); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index ecba34290d..a01d4bebf5 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -242,6 +242,15 @@ describe('TestBackend', () => { expect(res.body).toEqual({ message: 'pong' }); }); + it('should expose health check endpoints', async () => { + const { server } = await startTestBackend({ features: [] }); + + const res = await request(server).get('/.backstage/health/v1/liveness'); + + expect(res.status).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + }); + it('should provide extension point implementations', async () => { expect.assertions(3); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 16ee6b78d2..3d662a8c12 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -37,6 +37,7 @@ import express from 'express'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter'; /** @public */ export interface TestBackendOptions { @@ -73,6 +74,7 @@ export const defaultServiceFactories = [ mockServices.lifecycle.factory(), mockServices.logger.factory(), mockServices.permissions.factory(), + mockServices.rootHealth.factory(), mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), mockServices.scheduler.factory(), @@ -251,15 +253,18 @@ export async function startTestBackend( config: coreServices.rootConfig, lifecycle: coreServices.rootLifecycle, rootLogger: coreServices.rootLogger, + health: coreServices.rootHealth, }, - async factory({ config, lifecycle, rootLogger }) { + async factory({ config, lifecycle, rootLogger, health }) { const router = DefaultRootHttpRouter.create(); const logger = rootLogger.child({ service: 'rootHttpRouter' }); const app = express(); const middleware = MiddlewareFactory.create({ config, logger }); + const healthRouter = createHealthRouter({ health }); + app.use(healthRouter); app.use(router.handler()); app.use(middleware.notFound()); app.use(middleware.error()); From 31bfc4417f7a4d04d5899ce1d638505c30f9f627 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 20 Jul 2024 12:47:09 +0200 Subject: [PATCH 33/77] frontend-plugin-api: update extension data ref declaration to allow embedding of ID in type Signed-off-by: Patrik Oldsberg --- .changeset/dull-ghosts-double.md | 7 ++ .changeset/tiny-oranges-pretend.md | 19 ++++++ .../architecture/03-extensions.md | 4 +- .../architecture/08-naming-patterns.md | 20 +++--- .../src/tree/instantiateAppNodeTree.test.ts | 8 ++- packages/frontend-plugin-api/api-report.md | 52 +++++++++++--- .../src/extensions/createApiExtension.ts | 5 +- .../createAppRootWrapperExtension.tsx | 7 +- .../extensions/createComponentExtension.tsx | 2 +- .../src/extensions/createNavItemExtension.tsx | 2 +- .../src/extensions/createNavLogoExtension.tsx | 2 +- .../src/extensions/createRouterExtension.tsx | 7 +- .../extensions/createSignInPageExtension.tsx | 2 +- .../src/extensions/createThemeExtension.ts | 5 +- .../extensions/createTranslationExtension.ts | 2 +- .../src/wiring/coreExtensionData.ts | 8 ++- .../src/wiring/createExtension.test.ts | 2 +- .../src/wiring/createExtension.ts | 2 +- .../src/wiring/createExtensionDataRef.test.ts | 9 +++ .../src/wiring/createExtensionDataRef.ts | 68 +++++++++++++------ .../src/wiring/createPlugin.test.ts | 4 +- plugins/catalog-react/api-report-alpha.md | 13 +++- plugins/catalog-react/src/alpha.tsx | 18 ++--- plugins/home/api-report-alpha.md | 6 +- plugins/home/src/alpha.tsx | 4 +- plugins/search-react/api-report-alpha.md | 1 + plugins/search-react/src/alpha.tsx | 2 +- 27 files changed, 204 insertions(+), 77 deletions(-) create mode 100644 .changeset/dull-ghosts-double.md create mode 100644 .changeset/tiny-oranges-pretend.md diff --git a/.changeset/dull-ghosts-double.md b/.changeset/dull-ghosts-double.md new file mode 100644 index 0000000000..0bc5a09791 --- /dev/null +++ b/.changeset/dull-ghosts-double.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-home': patch +--- + +Updated alpha definitions of extension data references. diff --git a/.changeset/tiny-oranges-pretend.md b/.changeset/tiny-oranges-pretend.md new file mode 100644 index 0000000000..87227fb962 --- /dev/null +++ b/.changeset/tiny-oranges-pretend.md @@ -0,0 +1,19 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Extension data references can now be defined in a way that encapsulates the ID string in the type, in addition to the data type itself. The old way of creating extension data references is deprecated and will be removed in a future release. + +For example, the following code: + +```ts +export const myExtension = createExtensionDataRef('my-plugin.my-data'); +``` + +Should be updated to the following: + +```ts +export const myExtension = createExtensionDataRef().with({ + id: 'my-plugin.my-data', +}); +``` diff --git a/docs/frontend-system/architecture/03-extensions.md b/docs/frontend-system/architecture/03-extensions.md index cdc73a17ec..2f3daba6f8 100644 --- a/docs/frontend-system/architecture/03-extensions.md +++ b/docs/frontend-system/architecture/03-extensions.md @@ -91,7 +91,9 @@ To create a new extension data reference to represent a type of shared extension ```ts export const reactElementExtensionDataRef = - createExtensionDataRef('my-plugin.reactElement'); + createExtensionDataRef().with({ + id: 'my-plugin.reactElement', + }); ``` The `ExtensionDataRef` can then be used to describe an output property of the extension. This will enforce typing on the return value of the extension factory: diff --git a/docs/frontend-system/architecture/08-naming-patterns.md b/docs/frontend-system/architecture/08-naming-patterns.md index a17525f3ce..f031b80d84 100644 --- a/docs/frontend-system/architecture/08-naming-patterns.md +++ b/docs/frontend-system/architecture/08-naming-patterns.md @@ -98,9 +98,9 @@ export interface SearchResultItemExtensionData { } export const searchResultItemExtensionDataRef = - createExtensionDataRef( - 'search.search-result-item', - ); + createExtensionDataRef().with({ + id: 'search.search-result-item', + }); ``` #### Grouped Extension Data @@ -109,8 +109,12 @@ This way of defining extension data is similar to the standalone way, but it use ```ts export const coreExtensionData = { - reactElement: createExtensionDataRef('core.react-element'), - routePath: createExtensionDataRef('core.route-path'), + reactElement: createExtensionDataRef().with({ + id: 'core.react-element', + }), + routePath: createExtensionDataRef().with({ + id: 'core.route-path', + }), }; ``` @@ -125,9 +129,9 @@ export function createGraphiQLEndpointExtension(options) { // Use a TypeScript namespace to merge the extension data references with the extension creator export namespace createGraphiQLEndpointExtension { - export const endpointDataRef = createExtensionDataRef( - 'graphiql.graphiql-endpoint.endpoint', - ); + export const endpointDataRef = createExtensionDataRef().with({ + id: 'graphiql.graphiql-endpoint.endpoint', + }); } ``` diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 3330b9add8..c5c66e8319 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -32,9 +32,11 @@ import { resolveAppTree } from './resolveAppTree'; import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; import { withLogCollector } from '@backstage/test-utils'; -const testDataRef = createExtensionDataRef('test'); -const otherDataRef = createExtensionDataRef('other'); -const inputMirrorDataRef = createExtensionDataRef('mirror'); +const testDataRef = createExtensionDataRef().with({ id: 'test' }); +const otherDataRef = createExtensionDataRef().with({ id: 'other' }); +const inputMirrorDataRef = createExtensionDataRef().with({ + id: 'mirror', +}); const simpleExtension = resolveExtensionDefinition( createExtension({ diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 29c165aaaa..65445b5c4d 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -151,6 +151,7 @@ export { AnyApiRef }; export type AnyExtensionDataMap = { [name in string]: ExtensionDataRef< unknown, + string, { optional?: true; } @@ -317,13 +318,15 @@ export { configApiRef }; // @public (undocumented) export interface ConfigurableExtensionDataRef< + TId extends string, TData, TConfig extends { optional?: true; } = {}, -> extends ExtensionDataRef { +> extends ExtensionDataRef { // (undocumented) optional(): ConfigurableExtensionDataRef< + TId, TData, TData & { optional: true; @@ -347,9 +350,17 @@ export type CoreErrorBoundaryFallbackProps = { // @public (undocumented) export const coreExtensionData: { - reactElement: ConfigurableExtensionDataRef; - routePath: ConfigurableExtensionDataRef; - routeRef: ConfigurableExtensionDataRef, {}>; + reactElement: ConfigurableExtensionDataRef< + 'core.reactElement', + JSX_2.Element, + {} + >; + routePath: ConfigurableExtensionDataRef<'core.routing.path', string, {}>; + routeRef: ConfigurableExtensionDataRef< + 'core.routing.ref', + RouteRef, + {} + >; }; // @public (undocumented) @@ -385,7 +396,11 @@ export function createApiExtension< // @public (undocumented) export namespace createApiExtension { const // (undocumented) - factoryDataRef: ConfigurableExtensionDataRef; + factoryDataRef: ConfigurableExtensionDataRef< + 'core.api.factory', + AnyApiFactory, + {} + >; } export { createApiFactory }; @@ -440,6 +455,7 @@ export function createAppRootWrapperExtension< export namespace createAppRootWrapperExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'app.root.wrapper', React_2.ComponentType<{ children?: React_2.ReactNode; }>, @@ -477,6 +493,7 @@ export function createComponentExtension< export namespace createComponentExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'core.component.component', { ref: ComponentRef; impl: ComponentType; @@ -544,10 +561,17 @@ export interface CreateExtensionBlueprintOptions< output: TOutput; } -// @public (undocumented) +// @public @deprecated (undocumented) export function createExtensionDataRef( id: string, -): ConfigurableExtensionDataRef; +): ConfigurableExtensionDataRef; + +// @public (undocumented) +export function createExtensionDataRef(): { + with(options: { + id: TId; + }): ConfigurableExtensionDataRef; +}; // @public (undocumented) export function createExtensionInput< @@ -646,6 +670,7 @@ export function createNavItemExtension(options: { export namespace createNavItemExtension { const // (undocumented) targetDataRef: ConfigurableExtensionDataRef< + 'core.nav-item.target', { title: string; icon: IconComponent_2; @@ -667,6 +692,7 @@ export function createNavLogoExtension(options: { export namespace createNavLogoExtension { const // (undocumented) logoElementsDataRef: ConfigurableExtensionDataRef< + 'core.nav-logo.logo-elements', { logoIcon?: JSX.Element | undefined; logoFull?: JSX.Element | undefined; @@ -760,6 +786,7 @@ export function createRouterExtension< export namespace createRouterExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'app.router.wrapper', React_2.ComponentType<{ children?: React_2.ReactNode; }>, @@ -796,6 +823,7 @@ export function createSignInPageExtension< export namespace createSignInPageExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< + 'core.sign-in-page.component', React_2.ComponentType, {} >; @@ -818,7 +846,11 @@ export function createThemeExtension( // @public (undocumented) export namespace createThemeExtension { const // (undocumented) - themeDataRef: ConfigurableExtensionDataRef; + themeDataRef: ConfigurableExtensionDataRef< + 'core.theme.theme', + AppTheme, + {} + >; } // @public (undocumented) @@ -831,6 +863,7 @@ export function createTranslationExtension(options: { export namespace createTranslationExtension { const // (undocumented) translationDataRef: ConfigurableExtensionDataRef< + 'core.translation.translation', | TranslationResource | TranslationMessages< string, @@ -934,11 +967,12 @@ export interface ExtensionBoundaryProps { // @public (undocumented) export type ExtensionDataRef< TData, + TId extends string = string, TConfig extends { optional?: true; } = {}, > = { - id: string; + id: TId; T: TData; config: TConfig; $$type: '@backstage/ExtensionDataRef'; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index 1680332960..93346f1ec9 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -72,6 +72,7 @@ export function createApiExtension< /** @public */ export namespace createApiExtension { - export const factoryDataRef = - createExtensionDataRef('core.api.factory'); + export const factoryDataRef = createExtensionDataRef().with({ + id: 'core.api.factory', + }); } diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx index bbc51806af..ce2eb89f25 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -77,8 +77,7 @@ export function createAppRootWrapperExtension< /** @public */ export namespace createAppRootWrapperExtension { - export const componentDataRef = - createExtensionDataRef>>( - 'app.root.wrapper', - ); + export const componentDataRef = createExtensionDataRef< + ComponentType> + >().with({ id: 'app.root.wrapper' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index a1231d8353..a40ee3b069 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -92,5 +92,5 @@ export namespace createComponentExtension { export const componentDataRef = createExtensionDataRef<{ ref: ComponentRef; impl: ComponentType; - }>('core.component.component'); + }>().with({ id: 'core.component.component' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx index 4b92d4dace..2a2c47835a 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -61,5 +61,5 @@ export namespace createNavItemExtension { title: string; icon: IconComponent; routeRef: RouteRef; - }>('core.nav-item.target'); + }>().with({ id: 'core.nav-item.target' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx index 3d66b26d6b..1f2419bd02 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx @@ -51,5 +51,5 @@ export namespace createNavLogoExtension { export const logoElementsDataRef = createExtensionDataRef<{ logoIcon?: JSX.Element; logoFull?: JSX.Element; - }>('core.nav-logo.logo-elements'); + }>().with({ id: 'core.nav-logo.logo-elements' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx index 32d61f7e60..12f946e8a7 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -77,8 +77,7 @@ export function createRouterExtension< /** @public */ export namespace createRouterExtension { - export const componentDataRef = - createExtensionDataRef>>( - 'app.router.wrapper', - ); + export const componentDataRef = createExtensionDataRef< + ComponentType> + >().with({ id: 'app.router.wrapper' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx index b525cc3a7b..312d30c78c 100644 --- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx @@ -79,5 +79,5 @@ export function createSignInPageExtension< export namespace createSignInPageExtension { export const componentDataRef = createExtensionDataRef< ComponentType - >('core.sign-in-page.component'); + >().with({ id: 'core.sign-in-page.component' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts index cc6dbdabe9..1740f05a5a 100644 --- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -33,6 +33,7 @@ export function createThemeExtension(theme: AppTheme) { /** @public */ export namespace createThemeExtension { - export const themeDataRef = - createExtensionDataRef('core.theme.theme'); + export const themeDataRef = createExtensionDataRef().with({ + id: 'core.theme.theme', + }); } diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts index 8497a64b88..9445e32e24 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts @@ -38,5 +38,5 @@ export function createTranslationExtension(options: { export namespace createTranslationExtension { export const translationDataRef = createExtensionDataRef< TranslationResource | TranslationMessages - >('core.translation.translation'); + >().with({ id: 'core.translation.translation' }); } diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index a6ff41f6b0..cb48cb8df3 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -20,7 +20,9 @@ import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ export const coreExtensionData = { - reactElement: createExtensionDataRef('core.reactElement'), - routePath: createExtensionDataRef('core.routing.path'), - routeRef: createExtensionDataRef('core.routing.ref'), + reactElement: createExtensionDataRef().with({ + id: 'core.reactElement', + }), + routePath: createExtensionDataRef().with({ id: 'core.routing.path' }), + routeRef: createExtensionDataRef().with({ id: 'core.routing.ref' }), }; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 42ab21db80..4323737540 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -18,7 +18,7 @@ import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; -const stringData = createExtensionDataRef('string'); +const stringData = createExtensionDataRef().with({ id: 'string' }); function unused(..._any: any[]) {} diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 28dc6a4e29..e87f3a638e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -22,7 +22,7 @@ import { ExtensionInput } from './createExtensionInput'; /** @public */ export type AnyExtensionDataMap = { - [name in string]: ExtensionDataRef; + [name in string]: ExtensionDataRef; }; /** @public */ diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts index f7a0dc82d0..f979a7def0 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts @@ -18,6 +18,15 @@ import { createExtensionDataRef } from './createExtensionDataRef'; describe('createExtensionDataRef', () => { it('can be created and read', () => { + const ref = createExtensionDataRef().with({ id: 'foo' }); + expect(ref.id).toBe('foo'); + expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}'); + const refOptional = ref.optional(); + expect(refOptional.id).toBe('foo'); + expect(String(refOptional)).toBe('ExtensionDataRef{id=foo,optional=true}'); + }); + + it('can be created and read in the deprecated way', () => { const ref = createExtensionDataRef('foo'); expect(ref.id).toBe('foo'); expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}'); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts index a7f4e4374e..a1c7b462f2 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -17,9 +17,10 @@ /** @public */ export type ExtensionDataRef< TData, + TId extends string = string, TConfig extends { optional?: true } = {}, > = { - id: string; + id: TId; T: TData; config: TConfig; $$type: '@backstage/ExtensionDataRef'; @@ -27,30 +28,59 @@ export type ExtensionDataRef< /** @public */ export interface ConfigurableExtensionDataRef< + TId extends string, TData, TConfig extends { optional?: true } = {}, -> extends ExtensionDataRef { - optional(): ConfigurableExtensionDataRef; +> extends ExtensionDataRef { + optional(): ConfigurableExtensionDataRef< + TId, + TData, + TData & { optional: true } + >; } -// TODO: change to options object with ID. -/** @public */ +/** + * @public + * @deprecated Use the following form instead: `createExtensionDataRef().with({ id: 'core.foo' })` + */ export function createExtensionDataRef( id: string, -): ConfigurableExtensionDataRef { +): ConfigurableExtensionDataRef; +/** @public */ +export function createExtensionDataRef(): { + with(options: { + id: TId; + }): ConfigurableExtensionDataRef; +}; +export function createExtensionDataRef(id?: string): + | ConfigurableExtensionDataRef + | { + with(options: { + id: TId; + }): ConfigurableExtensionDataRef; + } { + const createRef = (refId: TId) => + ({ + id: refId, + $$type: '@backstage/ExtensionDataRef', + config: {}, + optional() { + return { + ...this, + config: { ...this.config, optional: true }, + }; + }, + toString() { + const optional = Boolean(this.config.optional); + return `ExtensionDataRef{id=${refId},optional=${optional}}`; + }, + } as ConfigurableExtensionDataRef); + if (id) { + return createRef(id); + } return { - id, - $$type: '@backstage/ExtensionDataRef', - config: {}, - optional() { - return { - ...this, - config: { ...this.config, optional: true }, - }; + with(options: { id: TId }) { + return createRef(options.id); }, - toString() { - const optional = Boolean(this.config.optional); - return `ExtensionDataRef{id=${id},optional=${optional}}`; - }, - } as ConfigurableExtensionDataRef; + }; } diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index dab8a9778a..bb9bdc2107 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -27,7 +27,9 @@ import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import { createExtensionInput } from './createExtensionInput'; import { BackstagePlugin } from './types'; -const nameExtensionDataRef = createExtensionDataRef('name'); +const nameExtensionDataRef = createExtensionDataRef().with({ + id: 'name', +}); const Extension1 = createExtension({ name: '1', diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index b82c7ae3e2..d2bfb29458 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -17,12 +17,21 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) export const catalogExtensionData: { - entityContentTitle: ConfigurableExtensionDataRef; + entityContentTitle: ConfigurableExtensionDataRef< + 'catalog.entity-content-title', + string, + {} + >; entityFilterFunction: ConfigurableExtensionDataRef< + 'catalog.entity-filter-function', (entity: Entity) => boolean, {} >; - entityFilterExpression: ConfigurableExtensionDataRef; + entityFilterExpression: ConfigurableExtensionDataRef< + 'catalog.entity-filter-expression', + string, + {} + >; }; // @alpha (undocumented) diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index 85712b358a..e81b8e5ebf 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -36,15 +36,15 @@ export * from './translation'; /** @alpha */ export const catalogExtensionData = { - entityContentTitle: createExtensionDataRef( - 'catalog.entity-content-title', - ), - entityFilterFunction: createExtensionDataRef<(entity: Entity) => boolean>( - 'catalog.entity-filter-function', - ), - entityFilterExpression: createExtensionDataRef( - 'catalog.entity-filter-expression', - ), + entityContentTitle: createExtensionDataRef().with({ + id: 'catalog.entity-content-title', + }), + entityFilterFunction: createExtensionDataRef< + (entity: Entity) => boolean + >().with({ id: 'catalog.entity-filter-function' }), + entityFilterExpression: createExtensionDataRef().with({ + id: 'catalog.entity-filter-expression', + }), }; // TODO: Figure out how to merge with provided config schema diff --git a/plugins/home/api-report-alpha.md b/plugins/home/api-report-alpha.md index 35ce0bb86c..1dd3da96be 100644 --- a/plugins/home/api-report-alpha.md +++ b/plugins/home/api-report-alpha.md @@ -11,7 +11,11 @@ const _default: BackstagePlugin<{}, {}>; export default _default; // @alpha (undocumented) -export const titleExtensionDataRef: ConfigurableExtensionDataRef; +export const titleExtensionDataRef: ConfigurableExtensionDataRef< + 'title', + string, + {} +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index e9ff26c9e1..b13c2a2e0e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -31,7 +31,9 @@ const rootRouteRef = createRouteRef(); /** * @alpha */ -export const titleExtensionDataRef = createExtensionDataRef('title'); +export const titleExtensionDataRef = createExtensionDataRef().with({ + id: 'title', +}); const homePage = createPageExtension({ defaultPath: '/home', diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index 36f5f049c5..aea85d0f81 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -31,6 +31,7 @@ export function createSearchResultListItemExtension< export namespace createSearchResultListItemExtension { const // (undocumented) itemDataRef: ConfigurableExtensionDataRef< + 'search.search-result-list-item.item', { predicate?: SearchResultItemExtensionPredicate | undefined; component: SearchResultItemExtensionComponent; diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index db198fa99d..a22eaf4244 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -137,5 +137,5 @@ export namespace createSearchResultListItemExtension { export const itemDataRef = createExtensionDataRef<{ predicate?: SearchResultItemExtensionPredicate; component: SearchResultItemExtensionComponent; - }>('search.search-result-list-item.item'); + }>().with({ id: 'search.search-result-list-item.item' }); } From 0bfe7efa2284b31592c9966278f6d5ad5850c27a Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 18 Jul 2024 14:54:24 +0200 Subject: [PATCH 34/77] chore: wip Signed-off-by: blam Signed-off-by: blam --- .../frontend-app-api/src/wiring/createApp.tsx | 1 + .../src/extensions/IconBundleBlueprint.ts | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index cf6ea6dab4..ae7283f301 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -171,6 +171,7 @@ export interface CreateAppFeatureLoader { /** @public */ export function createApp(options?: { + /** @deprecated - TODO */ icons?: { [key in string]: IconComponent }; features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi }>; diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts new file mode 100644 index 0000000000..7008dba5a1 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts @@ -0,0 +1,31 @@ +/* + * 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 { IconComponent } from '../icons'; +import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; + +const iconsDataRef = + createExtensionDataRef<{ [key in string]: IconComponent }>('core.icons'); + +/** @public */ +export const IconBundleBlueprint = createExtensionBlueprint({ + kind: 'icon-bundle', + namespace: 'app', + attachTo: { id: 'app', input: 'icons' }, + output: { + icons: iconsDataRef, + }, + factory: (_, params: { icons: { [key in string]: IconComponent } }) => params, +}); From 39f36a944251d056362702622d82eacd6ff54bb6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 22 Jul 2024 14:06:27 +0200 Subject: [PATCH 35/77] auth-backend-module-microsoft-provider: update scope handling to define required scopes in backend Signed-off-by: Patrik Oldsberg --- .changeset/new-scissors-try.md | 5 ++ docs/auth/microsoft/provider.md | 4 +- .../src/authenticator.ts | 20 ++++-- .../src/module.test.ts | 65 ++++++++++++++++++- 4 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 .changeset/new-scissors-try.md diff --git a/.changeset/new-scissors-try.md b/.changeset/new-scissors-try.md new file mode 100644 index 0000000000..a9e9a45ce7 --- /dev/null +++ b/.changeset/new-scissors-try.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +--- + +Updated the Microsoft authenticator to accurately define required scopes, but to also omit the required and additional scopes when requesting resource scopes. diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index f799eeb627..60104b99b7 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -67,8 +67,6 @@ auth: clientSecret: ${AZURE_CLIENT_SECRET} tenantId: ${AZURE_TENANT_ID} domainHint: ${AZURE_TENANT_ID} - additionalScopes: - - Mail.Send signIn: resolvers: # typically you would pick one of these @@ -86,7 +84,7 @@ The Microsoft provider is a structure with three mandatory configuration keys: Leave blank if your app registration is multi tenant. When specified, this reduces login friction for users with accounts in multiple tenants by automatically filtering away accounts from other tenants. For more details, see [Home Realm Discovery](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/home-realm-discovery-policy) -- `additionalScopes` (optional): List of scopes for the App Registration. The default and mandatory value is ['user.read']. +- `additionalScopes` (optional): List of scopes for the App Registration, to be requested in addition to the required ones. ### Resolvers diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts index 1b6eb87fae..fe7dc8f7e1 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts @@ -21,21 +21,30 @@ import { PassportProfile, } from '@backstage/plugin-auth-node'; import { ExtendedMicrosoftStrategy } from './strategy'; -import { union } from 'lodash'; /** @public */ export const microsoftAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['email', 'openid', 'offline_access', 'user.read'], + transform({ requested, granted, required, additional }) { + // Resources scopes are of the form `/`, and are handled + // separately from the normal scopes in the client. When request a + // resource scope we should only include forward the request scope along + // with offline_access. + const hasResourceScope = Array.from(requested).some(s => s.includes('/')); + if (hasResourceScope) { + return [...requested, 'offline_access']; + } + return [...requested, ...granted, ...required, ...additional]; + }, + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); const tenantId = config.getString('tenantId'); const domainHint = config.getOptionalString('domainHint'); - const scope = union( - ['user.read'], - config.getOptionalStringArray('additionalScopes'), - ); const helper = PassportOAuthAuthenticatorHelper.from( new ExtendedMicrosoftStrategy( @@ -44,7 +53,6 @@ export const microsoftAuthenticator = createOAuthAuthenticator({ clientSecret: clientSecret, callbackURL: callbackUrl, tenant: tenantId, - scope: scope, }, ( accessToken: string, diff --git a/plugins/auth-backend-module-microsoft-provider/src/module.test.ts b/plugins/auth-backend-module-microsoft-provider/src/module.test.ts index b50956b9eb..a072b99bde 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/module.test.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/module.test.ts @@ -67,7 +67,7 @@ describe('authModuleMicrosoftProvider', () => { expect(startUrl.pathname).toBe('/my-tenant-id/oauth2/v2.0/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', - scope: 'User.Read.All', + scope: 'email openid offline_access user.read User.Read.All', client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, state: expect.any(String), @@ -97,6 +97,7 @@ describe('authModuleMicrosoftProvider', () => { clientSecret: 'another-client-secret', tenantId: 'another-tenant-id', domainHint: 'somedomain', + additionalScopes: ['some-extra-scope'], }, }, }, @@ -125,7 +126,7 @@ describe('authModuleMicrosoftProvider', () => { expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', - scope: 'user.read', + scope: 'email openid offline_access user.read some-extra-scope', client_id: 'another-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, state: expect.any(String), @@ -137,4 +138,64 @@ describe('authModuleMicrosoftProvider', () => { nonce: decodeURIComponent(nonceCookie.value), }); }); + + it('should not include required scopes for resources', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleMicrosoftProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + microsoft: { + development: { + clientId: 'another-client-id', + clientSecret: 'another-client-secret', + tenantId: 'another-tenant-id', + additionalScopes: ['some-extra-scope'], + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/microsoft/start?env=development&scope=some-resource/some-scope', + ); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('microsoft-nonce', { + domain: 'localhost', + path: '/api/auth/microsoft/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://login.microsoftonline.com'); + expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + scope: 'some-resource/some-scope offline_access', + client_id: 'another-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); }); From 09d1370799f92e7043ae0d1d5ebbd0798ab917d3 Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Mon, 22 Jul 2024 22:29:47 +0900 Subject: [PATCH 36/77] Update .changeset/witty-bears-behave.md Co-authored-by: Ben Lambert Signed-off-by: Calvin Lee --- .changeset/witty-bears-behave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/witty-bears-behave.md b/.changeset/witty-bears-behave.md index 7514759189..174467b9eb 100644 --- a/.changeset/witty-bears-behave.md +++ b/.changeset/witty-bears-behave.md @@ -2,4 +2,4 @@ '@backstage/integration': patch --- -Updated functions for getHarnessEditContentsUrl, getHarnessFileContentsUrl, getHarnessArchiveUrl, getHarnessLatestCommitUrl and parseHarnessUrl to handle account and org level urls +Updated functions for `getHarnessEditContentsUrl`, `getHarnessFileContentsUrl`, `getHarnessArchiveUrl`, `getHarnessLatestCommitUrl` and `parseHarnessUrl` to handle account and org level urls From dc37210a290aa28ed4e12d4d22a3f4696c693af1 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 22 Jul 2024 15:38:22 +0200 Subject: [PATCH 37/77] feat: add support for collecting icons using the new extension blueprint Signed-off-by: blam --- packages/frontend-app-api/src/extensions/App.tsx | 4 ++++ packages/frontend-app-api/src/wiring/createApp.tsx | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/extensions/App.tsx b/packages/frontend-app-api/src/extensions/App.tsx index 82e7600178..7794aceb79 100644 --- a/packages/frontend-app-api/src/extensions/App.tsx +++ b/packages/frontend-app-api/src/extensions/App.tsx @@ -24,6 +24,7 @@ import { createExtensionInput, createThemeExtension, createTranslationExtension, + IconBundleBlueprint, } from '@backstage/frontend-plugin-api'; export const App = createExtension({ @@ -42,6 +43,9 @@ export const App = createExtension({ translations: createExtensionInput({ translation: createTranslationExtension.translationDataRef, }), + icons: createExtensionInput({ + icon: IconBundleBlueprint.dataRefs.icons, + }), root: createExtensionInput( { element: coreExtensionData.reactElement, diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index ae7283f301..4d929b206f 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -25,6 +25,7 @@ import { createThemeExtension, createTranslationExtension, FrontendFeature, + IconBundleBlueprint, iconsApiRef, RouteResolutionApi, routeResolutionApiRef, @@ -247,6 +248,7 @@ export function createApp(options?: { * @public */ export function createSpecializedApp(options?: { + /** @deprecated - TODO */ icons?: { [key in string]: IconComponent }; features?: FrontendFeature[]; config?: ConfigApi; @@ -374,6 +376,11 @@ function createApiHolder( (x): x is typeof createTranslationExtension.translationDataRef.T => !!x, ) ?? []; + const extensionIcons = tree.root.edges.attachments + .get('icons') + ?.map(e => e.instance?.getData(IconBundleBlueprint.dataRefs.icons)) + .reduce((acc, bundle) => ({ ...acc, ...bundle }), {}); + for (const factory of pluginApis) { factoryRegistry.register('default', factory); } @@ -414,7 +421,8 @@ function createApiHolder( factoryRegistry.register('static', { api: iconsApiRef, deps: {}, - factory: () => new DefaultIconsApi({ ...defaultIcons, ...icons }), + factory: () => + new DefaultIconsApi({ ...defaultIcons, ...extensionIcons, ...icons }), }); factoryRegistry.register('static', { From 4a18c942be1d49700540005f9d6a1566d0268f98 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 22 Jul 2024 15:39:33 +0200 Subject: [PATCH 38/77] feat: support exporting of dataRefs through blueprints Signed-off-by: blam --- .../src/extensions/IconBundleBlueprint.ts | 10 ++++--- .../src/extensions/index.ts | 1 + .../src/wiring/createExtensionBlueprint.ts | 27 ++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts index 7008dba5a1..2ad1404102 100644 --- a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts @@ -16,8 +16,9 @@ import { IconComponent } from '../icons'; import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; -const iconsDataRef = - createExtensionDataRef<{ [key in string]: IconComponent }>('core.icons'); +const iconsDataRef = createExtensionDataRef<{ + [key in string]: IconComponent; +}>().with({ id: 'core.icons' }); /** @public */ export const IconBundleBlueprint = createExtensionBlueprint({ @@ -27,5 +28,8 @@ export const IconBundleBlueprint = createExtensionBlueprint({ output: { icons: iconsDataRef, }, - factory: (_, params: { icons: { [key in string]: IconComponent } }) => params, + factory: (params: { icons: { [key in string]: IconComponent } }) => params, + dataRefs: { + icons: iconsDataRef, + }, }); diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 562cb728cb..267201db81 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -25,3 +25,4 @@ export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; export { createComponentExtension } from './createComponentExtension'; export { createTranslationExtension } from './createTranslationExtension'; +export { IconBundleBlueprint } from './IconBundleBlueprint'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 6c79017a0a..b504db5f03 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -34,6 +34,7 @@ export interface CreateExtensionBlueprintOptions< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { kind: string; namespace?: string; @@ -50,6 +51,8 @@ export interface CreateExtensionBlueprintOptions< inputs: Expand>; }, ): Expand>; + + dataRefs?: TDataRefs; } /** @@ -60,7 +63,10 @@ export interface ExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { + dataRefs: TDataRefs; + make(args: { namespace?: string; name?: string; @@ -97,15 +103,21 @@ class ExtensionBlueprintImpl< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { constructor( private readonly options: CreateExtensionBlueprintOptions< TParams, TInputs, TOutput, - TConfig + TConfig, + TDataRefs >, - ) {} + ) { + this.dataRefs = options.dataRefs!; + } + + dataRefs: TDataRefs; public make(args: { namespace?: string; @@ -184,8 +196,15 @@ export function createExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap = never, >( - options: CreateExtensionBlueprintOptions, -): ExtensionBlueprint { + options: CreateExtensionBlueprintOptions< + TParams, + TInputs, + TOutput, + TConfig, + TDataRefs + >, +): ExtensionBlueprint { return new ExtensionBlueprintImpl(options); } From 7777b5fb40b96fcefe6a418c0d3d657b3ae6b6a1 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 22 Jul 2024 15:45:01 +0200 Subject: [PATCH 39/77] chore: added changeset Signed-off-by: blam --- .changeset/forty-ties-agree.md | 18 ++++++++++++++++++ .changeset/forty-ties-disagree.md | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 .changeset/forty-ties-agree.md create mode 100644 .changeset/forty-ties-disagree.md diff --git a/.changeset/forty-ties-agree.md b/.changeset/forty-ties-agree.md new file mode 100644 index 0000000000..caa45841a2 --- /dev/null +++ b/.changeset/forty-ties-agree.md @@ -0,0 +1,18 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added a new `IconBundleBlueprint` that lets you create icon bundle extensions that can be installed in an App in order to override or add new app icons. + +```tsx +import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; + +const exampleIconBundle = IconBundleBlueprint.make({ + name: 'example-bundle', + params: { + icons: { + user: MyOwnUserIcon, + }, + }, +}); +``` diff --git a/.changeset/forty-ties-disagree.md b/.changeset/forty-ties-disagree.md new file mode 100644 index 0000000000..214097b35d --- /dev/null +++ b/.changeset/forty-ties-disagree.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Support icon overriding with the new `IconBundleBlueprint` API. From be5cb5ea365f09044cc18788e7b5d9eaa6b3689e Mon Sep 17 00:00:00 2001 From: Devin Lauderdale Date: Mon, 22 Jul 2024 08:58:25 -0500 Subject: [PATCH 40/77] docs: remove unmaintained dora-metrics plugin Signed-off-by: Devin Lauderdale --- microsite/data/plugins/dora-metrics.yaml | 10 ---------- microsite/static/img/okay.png | Bin 46181 -> 0 bytes 2 files changed, 10 deletions(-) delete mode 100644 microsite/data/plugins/dora-metrics.yaml delete mode 100644 microsite/static/img/okay.png diff --git a/microsite/data/plugins/dora-metrics.yaml b/microsite/data/plugins/dora-metrics.yaml deleted file mode 100644 index 3f59902f46..0000000000 --- a/microsite/data/plugins/dora-metrics.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: DORA Metrics -author: Okay -authorUrl: https://www.okayhq.com -category: Metrics -description: Embed dashboards (like DORA metrics) in your team or service pages from any dev tools (including Github, Gitlab, Jira, Argo, CircleCI, Buildkite, Pagerduty, Rollbar, Sentry, etc). -documentation: https://github.com/OkayHQ/backstage-plugin -iconUrl: /img/okay.png -npmPackageName: '@okayhq/backstage-plugin' -addedDate: '2022-06-16' diff --git a/microsite/static/img/okay.png b/microsite/static/img/okay.png deleted file mode 100644 index 357219d9d2c51e1d9a16f3f7a0c4fc476936d685..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46181 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 From cf9da6dafbe6c2fd038d9b8176a2f28816b76e10 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 22 Jul 2024 16:10:05 +0200 Subject: [PATCH 41/77] chore: added test for dataRef exporting Signed-off-by: blam --- .../wiring/createExtensionBlueprint.test.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index c7b077bdbd..87b3cec152 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { coreExtensionData } from './coreExtensionData'; import { createExtensionBlueprint } from './createExtensionBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createExtensionDataRef } from './createExtensionDataRef'; describe('createExtensionBlueprint', () => { it('should allow creation of extension blueprints', () => { @@ -102,4 +103,28 @@ describe('createExtensionBlueprint', () => { const { container } = createExtensionTester(extension).render(); expect(container.querySelector('h2')).toHaveTextContent('Hello, world!'); }); + + it('should allow exporting the dataRefs from the extension blueprint', () => { + const dataRef = createExtensionDataRef().with({ id: 'test.data' }); + + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + dataRefs: { + data: dataRef, + }, + factory(params: { text: string }) { + return { + element:

{params.text}

, + }; + }, + }); + + expect(TestExtensionBlueprint.dataRefs).toEqual({ + data: dataRef, + }); + }); }); From 493feaca4d4af19c4174a531d268f4d6e63c1a9e Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Thu, 18 Jul 2024 11:10:18 +0100 Subject: [PATCH 42/77] cli: allow running with no configuration files Co-authored-by: Patrik Oldsberg Signed-off-by: MT Lewis --- .changeset/rich-mugs-dress.md | 7 ++++++ packages/cli/src/lib/bundler/config.ts | 6 ++++- packages/cli/src/lib/config.ts | 8 ++++--- packages/config-loader/api-report.md | 1 + .../src/sources/ConfigSources.test.ts | 13 +++++++++- .../src/sources/ConfigSources.ts | 24 +++++++++++++------ 6 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 .changeset/rich-mugs-dress.md diff --git a/.changeset/rich-mugs-dress.md b/.changeset/rich-mugs-dress.md new file mode 100644 index 0000000000..5008e33a70 --- /dev/null +++ b/.changeset/rich-mugs-dress.md @@ -0,0 +1,7 @@ +--- +'@backstage/config-loader': patch +--- + +Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and +`ConfigSources.defaultForTargets`, which results in omission of a ConfigSource +for the default app-config.yaml configuration file if it's not present. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 1f08f74a85..042f74d1f3 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -48,8 +48,12 @@ const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; export function resolveBaseUrl(config: Config): URL { const baseUrl = config.getOptionalString('app.baseUrl'); + try { - return new URL(baseUrl ?? '/', 'http://localhost:3000'); + return new URL( + baseUrl ?? '/', + `http://localhost:${process.env.PORT ?? '3000'}`, + ); } catch (error) { throw new Error(`Invalid app.baseUrl, ${error}`); } diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 0c657b640e..0be20b0185 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -96,11 +96,13 @@ export async function loadCliConfig(options: Options) { }, }); + const configurationLoadedMessage = appConfigs.length + ? `Loaded config from ${appConfigs.map(c => c.context).join(', ')}` + : `No configuration files found, running without config`; + // printing to stderr to not clobber stdout in case the cli command // outputs structured data (e.g. as config:schema does) - process.stderr.write( - `Loaded config from ${appConfigs.map(c => c.context).join(', ')}\n`, - ); + process.stderr.write(`${configurationLoadedMessage}\n`); try { const frontendAppConfigs = schema.process(appConfigs, { diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index bc8f217bbb..835e53351d 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -21,6 +21,7 @@ export type AsyncConfigSourceGenerator = AsyncGenerator< // @public export interface BaseConfigSourcesOptions { + allowMissingDefaultConfig?: boolean; // (undocumented) remote?: Pick; // (undocumented) diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index 4b14572666..e19ac1d51f 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -69,13 +69,18 @@ describe('ConfigSources', () => { }); it('should create default sources for targets', () => { + const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockImplementation(path => { + return path === `${root}app-config.yaml`; + }); + expect( mergeSources( ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), ), ).toEqual([{ name: 'FileConfigSource', path: `${root}app-config.yaml` }]); - const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockReturnValue(true); + fsSpy.mockReturnValue(true); + expect( mergeSources( ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), @@ -159,6 +164,10 @@ describe('ConfigSources', () => { }); it('should create a default source', () => { + const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockImplementation(path => { + return path === `${root}app-config.yaml`; + }); + expect( mergeSources( ConfigSources.default({ @@ -184,6 +193,8 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: resolvePath('b.yaml') }, { name: 'EnvConfigSource', env: { HOME: '/' } }, ]); + + fsSpy.mockRestore(); }); it('should merge sources', () => { diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index ecff39da2d..be8892f9a9 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -74,6 +74,11 @@ export interface BaseConfigSourcesOptions { watch?: boolean; rootDir?: string; remote?: Pick; + /** + * Allow the default app-config.yaml to be missing, in which case the source + * will not be created. + */ + allowMissingDefaultConfig?: boolean; /** * A custom substitution function that overrides the default one. @@ -177,14 +182,19 @@ export class ConfigSources { if (argSources.length === 0) { const defaultPath = resolvePath(rootDir, 'app-config.yaml'); const localPath = resolvePath(rootDir, 'app-config.local.yaml'); + const alwaysIncludeDefaultConfigSource = + !options.allowMissingDefaultConfig; + + if (alwaysIncludeDefaultConfigSource || fs.pathExistsSync(defaultPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: defaultPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } - argSources.push( - FileConfigSource.create({ - watch: options.watch, - path: defaultPath, - substitutionFunc: options.substitutionFunc, - }), - ); if (fs.pathExistsSync(localPath)) { argSources.push( FileConfigSource.create({ From 0eedec304dbe8e4f8b1e982a1e29b081cbed3ddd Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Thu, 18 Jul 2024 11:18:07 +0100 Subject: [PATCH 43/77] cli: add changeset for module federation local development changes Signed-off-by: MT Lewis --- .changeset/strong-otters-compete.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-otters-compete.md diff --git a/.changeset/strong-otters-compete.md b/.changeset/strong-otters-compete.md new file mode 100644 index 0000000000..fdcb982b88 --- /dev/null +++ b/.changeset/strong-otters-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add support for dynamic plugins via the EXPERIMENTAL_MODULE_FEDERATION environment variable when running `yarn start`. From 9d661bb3807a534cf023dc84d816cee5d3c81472 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Fri, 19 Jul 2024 14:28:13 +0100 Subject: [PATCH 44/77] cli: only consider PORT environment variable for module federation remotes Signed-off-by: MT Lewis --- packages/cli/src/lib/bundler/config.ts | 43 +++++++++++++++++++------- packages/cli/src/lib/bundler/server.ts | 7 +++-- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 042f74d1f3..42cdae24e3 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { BackendBundlingOptions, BundlingOptions } from './types'; +import { + BackendBundlingOptions, + BundlingOptions, + ModuleFederationOptions, +} from './types'; import { posix as posixPath, resolve as resolvePath, dirname } from 'path'; import chalk from 'chalk'; import webpack, { ProvidePlugin } from 'webpack'; @@ -46,24 +50,32 @@ import { hasReactDomClient } from './hasReactDomClient'; const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; -export function resolveBaseUrl(config: Config): URL { +export function resolveBaseUrl( + config: Config, + moduleFederation?: ModuleFederationOptions, +): URL { const baseUrl = config.getOptionalString('app.baseUrl'); + const defaultBaseUrl = + moduleFederation?.mode === 'remote' + ? `http://localhost:${process.env.PORT ?? '3000'}` + : 'http://localhost:3000'; + try { - return new URL( - baseUrl ?? '/', - `http://localhost:${process.env.PORT ?? '3000'}`, - ); + return new URL(baseUrl ?? '/', defaultBaseUrl); } catch (error) { throw new Error(`Invalid app.baseUrl, ${error}`); } } -export function resolveEndpoint(config: Config): { +export function resolveEndpoint( + config: Config, + moduleFederation?: ModuleFederationOptions, +): { host: string; port: number; } { - const url = resolveBaseUrl(config); + const url = resolveBaseUrl(config, moduleFederation); return { host: config.getOptionalString('app.listen.host') ?? url.hostname, @@ -114,7 +126,13 @@ export async function createConfig( paths: BundlingPaths, options: BundlingOptions, ): Promise { - const { checksEnabled, isDev, frontendConfig, publicSubPath = '' } = options; + const { + checksEnabled, + isDev, + frontendConfig, + moduleFederation, + publicSubPath = '', + } = options; const { plugins, loaders } = transforms(options); // Any package that is part of the monorepo but outside the monorepo root dir need @@ -122,14 +140,17 @@ export async function createConfig( const { packages } = await getPackages(cliPaths.targetDir); const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir)); - const validBaseUrl = resolveBaseUrl(frontendConfig); + const validBaseUrl = resolveBaseUrl(frontendConfig, moduleFederation); let publicPath = validBaseUrl.pathname.replace(/\/$/, ''); if (publicSubPath) { publicPath = `${publicPath}${publicSubPath}`.replace('//', '/'); } if (isDev) { - const { host, port } = resolveEndpoint(options.frontendConfig); + const { host, port } = resolveEndpoint( + options.frontendConfig, + options.moduleFederation, + ); plugins.push( new ReactRefreshPlugin({ diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 0421f4d019..6b89847fb6 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -130,8 +130,11 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be } const { frontendConfig, fullConfig } = cliConfig; - const url = resolveBaseUrl(frontendConfig); - const { host, port } = resolveEndpoint(frontendConfig); + const url = resolveBaseUrl(frontendConfig, options.moduleFederation); + const { host, port } = resolveEndpoint( + frontendConfig, + options.moduleFederation, + ); const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ config: fullConfig, From 239dffc970b4b040ffdf9aee24f6937c4994f5ba Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Mon, 22 Jul 2024 11:06:42 +0100 Subject: [PATCH 45/77] cli: remove usage of deprecated functionality from @backstage/config-loader Co-authored-by: Patrik Oldsberg Signed-off-by: MT Lewis --- .changeset/ninety-icons-smile.md | 5 +++ packages/cli/src/lib/config.ts | 76 +++++++++++++++++++------------- 2 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 .changeset/ninety-icons-smile.md diff --git a/.changeset/ninety-icons-smile.md b/.changeset/ninety-icons-smile.md new file mode 100644 index 0000000000..f137879706 --- /dev/null +++ b/.changeset/ninety-icons-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Remove usage of deprecated functionality from @backstage/config-loader diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 0be20b0185..49cce1f1ed 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -14,14 +14,9 @@ * limitations under the License. */ -import { - ConfigTarget, - loadConfig, - loadConfigSchema, -} from '@backstage/config-loader'; +import { ConfigSources, loadConfigSchema } from '@backstage/config-loader'; import { AppConfig, ConfigReader } from '@backstage/config'; import { paths } from './paths'; -import { isValidUrl } from './urls'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from '@backstage/cli-node'; @@ -37,13 +32,6 @@ type Options = { }; export async function loadCliConfig(options: Options) { - const configTargets: ConfigTarget[] = []; - options.args.forEach(arg => { - if (!isValidUrl(arg)) { - configTargets.push({ path: paths.resolveTarget(arg) }); - } - }); - // Consider all packages in the monorepo when loading in config const { packages } = await getPackages(paths.targetDir); @@ -75,25 +63,53 @@ export async function loadCliConfig(options: Options) { noUndeclaredProperties: options.strict, }); - const { appConfigs } = await loadConfig({ - experimentalEnvFunc: options.mockEnv + const source = ConfigSources.default({ + allowMissingDefaultConfig: true, + substitutionFunc: options.mockEnv ? async name => process.env[name] || 'x' : undefined, - configRoot: paths.targetRoot, - configTargets: configTargets, - watch: options.watch && { - onChange(newAppConfigs) { - const newFrontendAppConfigs = schema.process(newAppConfigs, { - visibility: options.fullVisibility - ? ['frontend', 'backend', 'secret'] - : ['frontend'], - withFilteredKeys: options.withFilteredKeys, - withDeprecatedKeys: options.withDeprecatedKeys, - ignoreSchemaErrors: !options.strict, - }); - options.watch?.(newFrontendAppConfigs); - }, - }, + watch: Boolean(options.watch), + rootDir: paths.targetRoot, + argv: options.args, + }); + + const appConfigs = await new Promise((resolve, reject) => { + async function loadConfigReaderLoop() { + let loaded = false; + + try { + const abortController = new AbortController(); + for await (const { configs } of source.readConfigData({ + signal: abortController.signal, + })) { + if (loaded) { + const newFrontendAppConfigs = schema.process(configs, { + visibility: options.fullVisibility + ? ['frontend', 'backend', 'secret'] + : ['frontend'], + withFilteredKeys: options.withFilteredKeys, + withDeprecatedKeys: options.withDeprecatedKeys, + ignoreSchemaErrors: !options.strict, + }); + options.watch?.(newFrontendAppConfigs); + } else { + resolve(configs); + loaded = true; + + if (!options.watch) { + abortController.abort(); + } + } + } + } catch (error) { + if (loaded) { + console.error(`Failed to reload configuration, ${error}`); + } else { + reject(error); + } + } + } + loadConfigReaderLoop(); }); const configurationLoadedMessage = appConfigs.length From 348087a09636d79e3a2894325b16afef79275ec9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 22 Jul 2024 16:22:29 +0200 Subject: [PATCH 46/77] auth-backend-module-microsoft-provider: cleanup deps Signed-off-by: Patrik Oldsberg --- plugins/auth-backend-module-microsoft-provider/package.json | 2 -- yarn.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 67a898cf80..b67fb8c447 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -38,9 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "lodash": "^4.17.21", "node-fetch": "^2.6.7", - "passport": "^0.7.0", "passport-microsoft": "^1.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index a8ed83a61a..ff5eac7b1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4861,10 +4861,8 @@ __metadata: "@types/passport-microsoft": ^1.0.0 express: ^4.18.2 jose: ^5.0.0 - lodash: ^4.17.21 msw: ^1.0.0 node-fetch: ^2.6.7 - passport: ^0.7.0 passport-microsoft: ^1.0.0 supertest: ^6.3.3 languageName: unknown From b1f3c44e42a8f880698e2b4e5c59be133c000738 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 22 Jul 2024 16:31:09 +0200 Subject: [PATCH 47/77] chore: api-reports Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 46 +++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 65445b5c4d..3c082a7694 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -522,9 +522,16 @@ export function createExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap = never, >( - options: CreateExtensionBlueprintOptions, -): ExtensionBlueprint; + options: CreateExtensionBlueprintOptions< + TParams, + TInputs, + TOutput, + TConfig, + TDataRefs + >, +): ExtensionBlueprint; // @public (undocumented) export interface CreateExtensionBlueprintOptions< @@ -532,6 +539,7 @@ export interface CreateExtensionBlueprintOptions< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { // (undocumented) attachTo: { @@ -541,6 +549,8 @@ export interface CreateExtensionBlueprintOptions< // (undocumented) configSchema?: PortableSchema; // (undocumented) + dataRefs?: TDataRefs; + // (undocumented) disabled?: boolean; // (undocumented) factory( @@ -917,7 +927,10 @@ export interface ExtensionBlueprint< TInputs extends AnyExtensionInputMap, TOutput extends AnyExtensionDataMap, TConfig, + TDataRefs extends AnyExtensionDataMap, > { + // (undocumented) + dataRefs: TDataRefs; // (undocumented) make(args: { namespace?: string; @@ -1085,6 +1098,35 @@ export { gitlabAuthApiRef }; export { googleAuthApiRef }; +// @public (undocumented) +export const IconBundleBlueprint: ExtensionBlueprint< + { + icons: { + [x: string]: IconComponent; + }; + }, + AnyExtensionInputMap, + { + icons: ConfigurableExtensionDataRef< + 'core.icons', + { + [x: string]: IconComponent; + }, + {} + >; + }, + unknown, + { + icons: ConfigurableExtensionDataRef< + 'core.icons', + { + [x: string]: IconComponent; + }, + {} + >; + } +>; + // @public export type IconComponent = ComponentType< | { From 98ea3e2ca9329f6b131e3d3f038ba5101b039beb Mon Sep 17 00:00:00 2001 From: Adam Letizia <43392371+koalaty-code@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:40:18 -0500 Subject: [PATCH 48/77] chore(changeset): new change description Co-authored-by: Ben Lambert Signed-off-by: Adam Letizia <43392371+koalaty-code@users.noreply.github.com> --- .changeset/tough-lies-repair.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tough-lies-repair.md b/.changeset/tough-lies-repair.md index 25b1884bbc..b2f848e292 100644 --- a/.changeset/tough-lies-repair.md +++ b/.changeset/tough-lies-repair.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': patch --- -RepoUrlPicker: refetch user credentials when a new host is selected +Fix issue with `RepoUrlPicker` not refreshing the credentials for a different host From d0e95a70369cb81bf2c74850a5c3b80250388110 Mon Sep 17 00:00:00 2001 From: Rich Barton-Cooper <58304039+richcooper95@users.noreply.github.com> Date: Fri, 3 May 2024 16:01:05 +0100 Subject: [PATCH 49/77] feat: expose uiSchema and formContext in FormProps This commit exposes two extra fields in the FormProps: `uiSchema` and `formContext`. Signed-off-by: Rich Barton-Cooper Co-authored-by: Chunyi Lyu --- .changeset/flat-plums-grow.md | 6 ++++++ plugins/scaffolder-react/api-report.md | 2 +- plugins/scaffolder-react/src/components/types.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/flat-plums-grow.md diff --git a/.changeset/flat-plums-grow.md b/.changeset/flat-plums-grow.md new file mode 100644 index 0000000000..b62f3e65a9 --- /dev/null +++ b/.changeset/flat-plums-grow.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Add ability to customise form fields in the UI by exposing `uiSchema` and `formContext` in +`FormProps` diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index a14490b80c..a19a656d17 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -128,7 +128,7 @@ export interface FieldExtensionUiSchema // @public export type FormProps = Pick< FormProps_2, - 'transformErrors' | 'noHtml5Validate' + 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' >; // @public diff --git a/plugins/scaffolder-react/src/components/types.ts b/plugins/scaffolder-react/src/components/types.ts index 597998bac4..d5d317015d 100644 --- a/plugins/scaffolder-react/src/components/types.ts +++ b/plugins/scaffolder-react/src/components/types.ts @@ -32,7 +32,7 @@ export type TemplateGroupFilter = { */ export type FormProps = Pick< SchemaFormProps, - 'transformErrors' | 'noHtml5Validate' + 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' >; /** From 8cdd846abc49841100cdaf5273d4793a821b475e Mon Sep 17 00:00:00 2001 From: Rich Barton-Cooper Date: Mon, 22 Jul 2024 15:55:23 +0100 Subject: [PATCH 50/77] fix: merge uiSchema and formContext in Stepper component This commit ensures that the uiSchema and formContext are merged in the Stepper component form, with the non-props values taking precedence over the props values. The uiSchemas are deep-merged using `lodash.merge` to ensure that nested values are correctly merged. Signed-off-by: Rich Barton-Cooper --- .../next/components/Stepper/Stepper.test.tsx | 60 +++++++++++++++++++ .../src/next/components/Stepper/Stepper.tsx | 15 ++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index df58d971fb..78aef3428f 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -549,6 +549,66 @@ describe('Stepper', () => { }); }); + it('should allow overrides to the uiSchema and formContext correctly', async () => { + const manifest: TemplateParameterSchema = { + title: 'Custom Fields', + steps: [ + { + title: 'Test', + schema: { + properties: { + name: { + type: 'string', + 'ui:placeholder': 'Enter your name', + }, + age: { + type: 'number', + 'ui:placeholder': 'Enter your age', + }, + }, + }, + }, + ], + }; + + const uiSchema = { + name: { + 'ui:readonly': true, + 'ui:placeholder': 'Should be overwritten', + }, + }; + + const formContext = { + readOnlyAsDisabled: true, + }; + + const { getByRole } = await renderInTestApp( + + + , + ); + + expect(getByRole('textbox', { name: 'name' })).toHaveValue('Some Name'); + expect(getByRole('textbox', { name: 'name' })).toBeDisabled(); + expect(getByRole('textbox', { name: 'name' })).toHaveAttribute( + 'placeholder', + 'Enter your name', + ); + + expect(getByRole('spinbutton', { name: 'age' })).toHaveValue(40); + expect(getByRole('spinbutton', { name: 'age' })).toBeEnabled(); + expect(getByRole('spinbutton', { name: 'age' })).toHaveAttribute( + 'placeholder', + 'Enter your age', + ); + }); + describe('Scaffolder Layouts', () => { it('should render the step in the scaffolder layout', async () => { const ScaffolderLayout: LayoutTemplate = ({ properties }) => ( diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 788c7cd715..3a8858144b 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -51,6 +51,7 @@ import { ErrorListTemplate } from './ErrorListTemplate'; import { makeStyles } from '@material-ui/core/styles'; import { PasswordWidget } from '../PasswordWidget/PasswordWidget'; import ajvErrors from 'ajv-errors'; +import { merge } from 'lodash'; const validator = customizeValidator(); ajvErrors(validator.ajv); @@ -193,6 +194,14 @@ export const Stepper = (stepperProps: StepperProps) => { setFormState(current => ({ ...current, ...formData })); }; + const { + formContext: propFormContext, + uiSchema: propUiSchema, + ...restFormProps + } = props.formProps ?? {}; + + const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema); + return ( <> {isValidating && } @@ -229,9 +238,9 @@ export const Stepper = (stepperProps: StepperProps) => { validator={validator} extraErrors={errors as unknown as ErrorSchema} formData={formState} - formContext={{ formData: formState }} + formContext={{ ...propFormContext, formData: formState }} schema={currentStep.schema} - uiSchema={currentStep.uiSchema} + uiSchema={mergedUiSchema} onSubmit={handleNext} fields={fields} showErrorList="top" @@ -241,7 +250,7 @@ export const Stepper = (stepperProps: StepperProps) => { experimental_defaultFormStateBehavior={{ allOf: 'populateDefaults', }} - {...(props.formProps ?? {})} + {...restFormProps} >

yjD;9z)BowS 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 literal 0 HcmV?d00001 diff --git a/docs/getting-started/logging-in.md b/docs/getting-started/logging-in.md index fef8907e07..5386e98798 100644 --- a/docs/getting-started/logging-in.md +++ b/docs/getting-started/logging-in.md @@ -20,6 +20,8 @@ Run your Backstage app with `yarn dev`. Navigate to `http://localhost:3000`. If you're not already logged in, you should see a login screen like this, +![Screenshot of the the login screen](../assets/getting-started/login-screen.png) + To login, you should choose the "Github" provider and click the "Sign in" button. This will redirect you to a Github OAuth page. Verify that the scopes mentioned on that page match the setup you did in [the authentication tutorial](./config/authentication.md). Once you click "Confirm", you will be brought back to the Backstage interface and signed in! If you are already logged in, you will be automatically brought to your Backstage instance. From 708294012180f435dd61c87a827b752b6f3fb974 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Wed, 17 Jul 2024 18:01:01 +0100 Subject: [PATCH 20/77] cli: add support for frontend-dynamic-container role in yarn start Signed-off-by: MT Lewis --- .../cli/src/commands/build/buildFrontend.ts | 27 +----------- packages/cli/src/commands/start/command.ts | 8 ++++ .../cli/src/commands/start/startFrontend.ts | 11 ++++- packages/cli/src/lib/bundler/index.ts | 1 + .../cli/src/lib/bundler/moduleFederation.ts | 44 +++++++++++++++++++ packages/cli/src/lib/bundler/types.ts | 1 + 6 files changed, 65 insertions(+), 27 deletions(-) create mode 100644 packages/cli/src/lib/bundler/moduleFederation.ts diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 62da74e6d3..f29e0bdd38 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -16,11 +16,9 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { buildBundle } from '../../lib/bundler'; +import { buildBundle, getModuleFederationOptions } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; -import chalk from 'chalk'; -import { BuildOptions } from '../../lib/bundler/types'; interface BuildAppOptions { targetDir: string; @@ -29,29 +27,6 @@ interface BuildAppOptions { isModuleFederationRemote?: true; } -function getModuleFederationOptions( - name: string, - isRemote?: boolean, -): BuildOptions['moduleFederation'] { - if (!isRemote && !process.env.EXPERIMENTAL_MODULE_FEDERATION) { - return undefined; - } - - console.log( - chalk.yellow( - `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, - ), - ); - - return { - mode: isRemote ? 'remote' : 'host', - // The default output mode requires the name to be a usable as a code - // symbol, there might be better options here but for now we need to - // sanitize the name. - name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), - }; -} - export async function buildFrontend(options: BuildAppOptions) { const { targetDir, writeStats, configPaths } = options; const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index 2f2af95d85..aa46b717bb 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -15,6 +15,7 @@ */ import { OptionValues } from 'commander'; +import { PackageRole } from '@backstage/cli-node'; import { findRoleFromCommand } from '../../lib/role'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; @@ -47,6 +48,13 @@ export async function command(opts: OptionValues): Promise { case 'frontend-plugin': case 'frontend-plugin-module': return startFrontend({ entry: 'dev/index', ...options }); + case 'frontend-dynamic-container' as PackageRole: // experimental + return startFrontend({ + entry: 'src/index', + ...options, + skipOpenBrowser: true, + isModuleFederationRemote: true, + }); default: throw new Error( `Start command is not supported for package role '${role}'`, diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index a809473788..627da1bd1a 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { serveBundle } from '../../lib/bundler'; +import { readJson } from 'fs-extra'; +import { getModuleFederationOptions, serveBundle } from '../../lib/bundler'; +import { paths } from '../../lib/paths'; interface StartAppOptions { verifyVersions?: boolean; @@ -22,14 +24,21 @@ interface StartAppOptions { checksEnabled: boolean; configPaths: string[]; + isModuleFederationRemote?: boolean; } export async function startFrontend(options: StartAppOptions) { + const { name } = await readJson(paths.resolveTarget('package.json')); + const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, configPaths: options.configPaths, verifyVersions: options.verifyVersions, + moduleFederation: getModuleFederationOptions( + name, + options.isModuleFederationRemote, + ), }); await waitForExit(); diff --git a/packages/cli/src/lib/bundler/index.ts b/packages/cli/src/lib/bundler/index.ts index a3b584efc4..2219784dc9 100644 --- a/packages/cli/src/lib/bundler/index.ts +++ b/packages/cli/src/lib/bundler/index.ts @@ -16,4 +16,5 @@ export { serveBackend } from './backend'; export { buildBundle } from './bundle'; +export { getModuleFederationOptions } from './moduleFederation'; export { serveBundle } from './server'; diff --git a/packages/cli/src/lib/bundler/moduleFederation.ts b/packages/cli/src/lib/bundler/moduleFederation.ts new file mode 100644 index 0000000000..ae3f9ab048 --- /dev/null +++ b/packages/cli/src/lib/bundler/moduleFederation.ts @@ -0,0 +1,44 @@ +/* + * 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 chalk from 'chalk'; +import { ModuleFederationOptions } from './types'; + +export function getModuleFederationOptions( + name: string, + isModuleFederationRemote?: boolean, +): ModuleFederationOptions | undefined { + if ( + !isModuleFederationRemote && + !process.env.EXPERIMENTAL_MODULE_FEDERATION + ) { + return undefined; + } + + console.log( + chalk.yellow( + `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, + ), + ); + + return { + mode: isModuleFederationRemote ? 'remote' : 'host', + // The default output mode requires the name to be a usable as a code + // symbol, there might be better options here but for now we need to + // sanitize the name. + name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), + }; +} diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 7291ac0818..60c59f757b 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -43,6 +43,7 @@ export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; configPaths: string[]; verifyVersions?: boolean; + moduleFederation?: ModuleFederationOptions; }; export type BuildOptions = BundlingPathsOptions & { From 94ab20ebfa48ae8197be8180dfa86b7ee4ea6bfa Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Wed, 17 Jul 2024 18:04:36 +0100 Subject: [PATCH 21/77] cli: skip opening browser when serving bundle for packages with frontend-dynamic-container role Signed-off-by: MT Lewis --- packages/cli/src/commands/start/startFrontend.ts | 2 ++ packages/cli/src/lib/bundler/server.ts | 5 ++++- packages/cli/src/lib/bundler/types.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 627da1bd1a..20faf4a9ff 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -24,6 +24,7 @@ interface StartAppOptions { checksEnabled: boolean; configPaths: string[]; + skipOpenBrowser?: boolean; isModuleFederationRemote?: boolean; } @@ -35,6 +36,7 @@ export async function startFrontend(options: StartAppOptions) { checksEnabled: options.checksEnabled, configPaths: options.configPaths, verifyVersions: options.verifyVersions, + skipOpenBrowser: options.skipOpenBrowser, moduleFederation: getModuleFederationOptions( name, options.isModuleFederationRemote, diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 1da15cd37e..cf35f65518 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -162,6 +162,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be const config = await createConfig(paths, { ...commonConfigOptions, additionalEntryPoints: detectedModulesEntryPoint, + moduleFederation: options.moduleFederation, }); if (process.env.EXPERIMENTAL_VITE) { @@ -278,7 +279,9 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be } }); - openBrowser(url.href); + if (!options.skipOpenBrowser) { + openBrowser(url.href); + } const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 60c59f757b..f8e228c5c4 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -43,6 +43,7 @@ export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; configPaths: string[]; verifyVersions?: boolean; + skipOpenBrowser?: boolean; moduleFederation?: ModuleFederationOptions; }; From b7709255e8b3540176b7db675e9ad77e658920cc Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Wed, 17 Jul 2024 18:12:16 +0100 Subject: [PATCH 22/77] cli: pass websocket host and port explicitly in configuration When configuring the ReactRefreshPlugin and the WebpackDevServer, we previously inferred the host and port from the browser location. This is no longer sufficient, since dynamic plugin containers may be served from separate dev servers. https://webpack.js.org/configuration/dev-server/#websocketurl https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#sockhost Signed-off-by: MT Lewis --- packages/cli/src/lib/bundler/config.ts | 30 ++++++++++++++++++++++ packages/cli/src/lib/bundler/server.ts | 12 +++------ packages/cli/src/lib/bundler/transforms.ts | 11 +------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 9a611a5954..1f08f74a85 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -29,6 +29,7 @@ import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack'; import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin'; +import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import { paths as cliPaths } from '../../lib/paths'; import fs from 'fs-extra'; import { getPackages } from '@manypkg/get-packages'; @@ -54,6 +55,21 @@ export function resolveBaseUrl(config: Config): URL { } } +export function resolveEndpoint(config: Config): { + host: string; + port: number; +} { + const url = resolveBaseUrl(config); + + return { + host: config.getOptionalString('app.listen.host') ?? url.hostname, + port: + config.getOptionalNumber('app.listen.port') ?? + Number(url.port) ?? + (url.protocol === 'https:' ? 443 : 80), + }; +} + async function readBuildInfo() { const timestamp = Date.now(); @@ -108,6 +124,20 @@ export async function createConfig( publicPath = `${publicPath}${publicSubPath}`.replace('//', '/'); } + if (isDev) { + const { host, port } = resolveEndpoint(options.frontendConfig); + + plugins.push( + new ReactRefreshPlugin({ + overlay: { + sockProtocol: 'ws', + sockHost: host, + sockPort: port, + }, + }), + ); + } + if (checksEnabled) { plugins.push( new ForkTsCheckerWebpackPlugin({ diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index cf35f65518..cbd76b4ead 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -30,7 +30,7 @@ import { import { paths as libPaths } from '../../lib/paths'; import { loadCliConfig } from '../config'; import { Lockfile } from '../versioning'; -import { createConfig, resolveBaseUrl } from './config'; +import { createConfig, resolveBaseUrl, resolveEndpoint } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths'; import { ServeOptions } from './types'; @@ -131,13 +131,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be const { frontendConfig, fullConfig } = cliConfig; const url = resolveBaseUrl(frontendConfig); - - const host = - frontendConfig.getOptionalString('app.listen.host') || url.hostname; - const port = - frontendConfig.getOptionalNumber('app.listen.port') || - Number(url.port) || - (url.protocol === 'https:' ? 443 : 80); + const { host, port } = resolveEndpoint(frontendConfig); const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ config: fullConfig, @@ -257,7 +251,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be // When the dev server is behind a proxy, the host and public hostname differ allowedHosts: [url.hostname], client: { - webSocketURL: 'auto://0.0.0.0:0/ws', + webSocketURL: { hostname: host, port }, }, }, compiler, diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index da1eec4e11..0900b8e48e 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -17,7 +17,6 @@ import { ModuleOptions, WebpackPluginInstance } from 'webpack'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import { svgrTemplate } from '../svgrTemplate'; -import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; type Transforms = { loaders: ModuleOptions['rules']; @@ -193,15 +192,7 @@ export const transforms = (options: TransformOptions): Transforms => { const plugins = new Array(); - if (isDev) { - if (!isBackend) { - plugins.push( - new ReactRefreshPlugin({ - overlay: { sockProtocol: 'ws' }, - }), - ); - } - } else { + if (!isDev) { plugins.push( new MiniCssExtractPlugin({ filename: 'static/[name].[contenthash:8].css', From be1ee471cf2b09d455256ad6edfa84031ca16489 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Wed, 17 Jul 2024 18:18:20 +0100 Subject: [PATCH 23/77] cli: disable historyApiFallback for module federation remotes Module federation remotes only serve static assets, and aren't intended to be loaded directly in a browser. As such, the historyApiFallback isn't useful and should be disabled in this context. Signed-off-by: MT Lewis --- packages/cli/src/lib/bundler/server.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index cbd76b4ead..a3cbaeacc7 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -227,14 +227,17 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be directory: paths.targetPublic, } : undefined, - historyApiFallback: { - // Paths with dots should still use the history fallback. - // See https://github.com/facebookincubator/create-react-app/issues/387. - disableDotRule: true, + 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`, - }, + // The index needs to be rewritten relative to the new public path, including subroutes. + index: `${config.output?.publicPath}index.html`, + }, server: url.protocol === 'https:' ? { From f93ae496ce956598c56254d5420438be92faf0c0 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Wed, 17 Jul 2024 18:19:32 +0100 Subject: [PATCH 24/77] cli: add CORS headers for webpack-dev-server content This allows loading static assets from dev-servers for dynamic plugin containers. Signed-off-by: MT Lewis --- packages/cli/src/lib/bundler/server.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index a3cbaeacc7..0421f4d019 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -256,6 +256,12 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be 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, ); From e20fe546952f78e8f82faed76c03daf930d821ec Mon Sep 17 00:00:00 2001 From: Ramin G Date: Fri, 19 Jul 2024 15:56:09 -0400 Subject: [PATCH 25/77] Corrected typo in logging-in.md Corrected typo in the description of the image Signed-off-by: Ramin G --- docs/getting-started/logging-in.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/logging-in.md b/docs/getting-started/logging-in.md index 5386e98798..7d6fb5d55b 100644 --- a/docs/getting-started/logging-in.md +++ b/docs/getting-started/logging-in.md @@ -20,7 +20,7 @@ Run your Backstage app with `yarn dev`. Navigate to `http://localhost:3000`. If you're not already logged in, you should see a login screen like this, -![Screenshot of the the login screen](../assets/getting-started/login-screen.png) +![Screenshot of the login screen](../assets/getting-started/login-screen.png) To login, you should choose the "Github" provider and click the "Sign in" button. This will redirect you to a Github OAuth page. Verify that the scopes mentioned on that page match the setup you did in [the authentication tutorial](./config/authentication.md). Once you click "Confirm", you will be brought back to the Backstage interface and signed in! From c761199ce774a272e303f4493ba4a9c505ee5131 Mon Sep 17 00:00:00 2001 From: rghara Date: Fri, 19 Jul 2024 18:51:44 -0400 Subject: [PATCH 26/77] Replaced login screen image Signed-off-by: rghara --- docs/assets/getting-started/login-screen.png | Bin 108521 -> 53116 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/getting-started/login-screen.png b/docs/assets/getting-started/login-screen.png index 70b56f399a22d19ad863f77db22739f67a352501..1af52cb2c9b0b95cac2b6e715077bf9095ec038c 100755 GIT binary patch literal 53116 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�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