From 60d0a1a2edbcb298a0d12d843ad896ae71b27f47 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 22 Mar 2021 18:24:15 +0000 Subject: [PATCH 01/36] github collaborators field Signed-off-by: Andrew Johnson --- .changeset/dry-elephants-doubt.md | 5 +++ plugins/github-deployments/src/api/index.ts | 8 +++++ .../components/GithubDeploymentsCard.test.tsx | 36 +++++++++++++++---- .../src/components/GithubDeploymentsCard.tsx | 15 ++++++-- .../GithubDeploymentsTable.tsx | 4 ++- plugins/github-deployments/src/mocks/mocks.ts | 16 +++++++++ 6 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 .changeset/dry-elephants-doubt.md diff --git a/.changeset/dry-elephants-doubt.md b/.changeset/dry-elephants-doubt.md new file mode 100644 index 0000000000..7a9ccc9892 --- /dev/null +++ b/.changeset/dry-elephants-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-deployments': patch +--- + +Adds extraColumns field to GitHub Deployments card diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index ca69ac3853..7496370bc9 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -24,6 +24,10 @@ export type GithubDeployment = { abbreviatedOid: string; commitUrl: string; }; + creator: { + login: string; + }; + payload: string; }; export interface GithubDeploymentsApi { @@ -55,6 +59,10 @@ query deployments($owner: String!, $repo: String!, $last: Int) { abbreviatedOid commitUrl } + creator { + login + } + payload } } } diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index e20183f1d6..0eca749788 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -22,11 +22,16 @@ import { ConfigReader, ConfigApi, OAuthApi, + TableColumn, } from '@backstage/core'; import { fireEvent } from '@testing-library/react'; import { msw, renderInTestApp } from '@backstage/test-utils'; -import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; +import { + GithubDeployment, + GithubDeploymentsApiClient, + githubDeploymentsApiRef, +} from '../api'; import { githubDeploymentsPlugin } from '../plugin'; import { GithubDeploymentsCard } from './GithubDeploymentsCard'; @@ -127,12 +132,6 @@ describe('github-deployments', () => { }); it('should shows new data on reload', async () => { - worker.use( - graphql.query('deployments', (_, res, ctx) => - res(ctx.data(responseStub)), - ), - ); - const rendered = await renderInTestApp( @@ -160,4 +159,27 @@ describe('github-deployments', () => { expect(await rendered.findByText('failure')).toBeInTheDocument(); }); }); + + it('should display extra columns', async () => { + worker.use( + graphql.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + const extraColumns: TableColumn[] = [ + { + title: 'Creator', + field: 'creator.login', + }, + ]; + + const rendered = await renderInTestApp( + + + , + ); + + expect(await rendered.findByText('robot-user-001')).toBeInTheDocument(); + }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 99dfba560a..2a887047a1 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -17,10 +17,11 @@ import React from 'react'; import { MissingAnnotationEmptyState, ResponseErrorPanel, + TableColumn, useApi, } from '@backstage/core'; import { useAsyncRetry } from 'react-use'; -import { githubDeploymentsApiRef } from '../api'; +import { GithubDeployment, githubDeploymentsApiRef } from '../api'; import { useEntity } from '@backstage/plugin-catalog-react'; import { GITHUB_PROJECT_SLUG_ANNOTATION, @@ -31,9 +32,11 @@ import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTa const GithubDeploymentsComponent = ({ projectSlug, last, + extraColumns, }: { projectSlug: string; last: number; + extraColumns: TableColumn[]; }) => { const api = useApi(githubDeploymentsApiRef); const [owner, repo] = projectSlug.split('/'); @@ -51,11 +54,18 @@ const GithubDeploymentsComponent = ({ deployments={value || []} isLoading={loading} reload={reload} + extraColumns={extraColumns} /> ); }; -export const GithubDeploymentsCard = ({ last }: { last?: number }) => { +export const GithubDeploymentsCard = ({ + last, + extraColumns, +}: { + last?: number; + extraColumns?: TableColumn[]; +}) => { const { entity } = useEntity(); return !isGithubDeploymentsAvailable(entity) ? ( @@ -66,6 +76,7 @@ export const GithubDeploymentsCard = ({ last }: { last?: number }) => { entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] || '' } last={last || 10} + extraColumns={extraColumns || []} /> ); }; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 91f91b919e..44ed1fbcdd 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -86,18 +86,20 @@ type GithubDeploymentsTableProps = { deployments: GithubDeployment[]; isLoading: boolean; reload: () => void; + extraColumns: TableColumn[]; }; const GithubDeploymentsTable = ({ deployments, isLoading, reload, + extraColumns, }: GithubDeploymentsTableProps) => { const classes = useStyles(); return ( Date: Tue, 6 Apr 2021 14:29:59 +0100 Subject: [PATCH 02/36] fix test Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsCard.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 0eca749788..a1c2230d0d 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -132,6 +132,12 @@ describe('github-deployments', () => { }); it('should shows new data on reload', async () => { + worker.use( + graphql.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + const rendered = await renderInTestApp( From 7b30d0cdcf33429bc49654b8ca0f64cc33c8563b Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Apr 2021 14:38:56 +0100 Subject: [PATCH 03/36] prettier Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsCard.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index a1c2230d0d..a3bdd2cb9c 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -137,7 +137,7 @@ describe('github-deployments', () => { res(ctx.data(responseStub)), ), ); - + const rendered = await renderInTestApp( From a1c46265e9396e411a01efeebe40d6368ed41183 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Apr 2021 15:34:31 +0100 Subject: [PATCH 04/36] fix test Signed-off-by: Andrew Johnson --- plugins/github-deployments/src/mocks/mocks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts index 1edbbc9b00..a84d684661 100644 --- a/plugins/github-deployments/src/mocks/mocks.ts +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -63,7 +63,7 @@ export const responseStub: QueryResponse = { abbreviatedOid: '54321', }, creator: { - login: 'robot-user-001', + login: 'robot-user-002', }, payload: '', }, @@ -98,7 +98,7 @@ export const refreshedResponseStub: QueryResponse = { abbreviatedOid: '54321', }, creator: { - login: 'robot-user-001', + login: 'robot-user-002', }, payload: '', }, From cc9132f8c3dccf47b8111a11cd9ce2021168da40 Mon Sep 17 00:00:00 2001 From: Victor Morfin Date: Wed, 7 Apr 2021 13:16:36 -0600 Subject: [PATCH 05/36] Adding close button to Support menu Signed-off-by: Victor Morfin --- .../core/src/components/SupportButton/SupportButton.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/components/SupportButton/SupportButton.tsx b/packages/core/src/components/SupportButton/SupportButton.tsx index eea2b000ca..5bbde953b0 100644 --- a/packages/core/src/components/SupportButton/SupportButton.tsx +++ b/packages/core/src/components/SupportButton/SupportButton.tsx @@ -16,6 +16,7 @@ import { HelpIcon, useApp } from '@backstage/core-api'; import { + Box, Button, List, ListItem, @@ -127,6 +128,11 @@ export const SupportButton = ({ children }: PropsWithChildren) => { {items && items.map((item, i) => )} + + + ); From 9a9e7a42f440742121207d3dd7107df3367396f5 Mon Sep 17 00:00:00 2001 From: Victor Morfin Date: Wed, 7 Apr 2021 14:30:22 -0600 Subject: [PATCH 06/36] Adding changeset Signed-off-by: Victor Morfin --- .changeset/stale-chefs-retire.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stale-chefs-retire.md diff --git a/.changeset/stale-chefs-retire.md b/.changeset/stale-chefs-retire.md new file mode 100644 index 0000000000..579f5c3042 --- /dev/null +++ b/.changeset/stale-chefs-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': minor +--- + +Adding close button on support menu From 1229d8377fbbc40a1e54297b0d4c3599e1a28a1a Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 8 Apr 2021 14:22:10 +0100 Subject: [PATCH 07/36] use custom columns Signed-off-by: Andrew Johnson --- .../components/GithubDeploymentsCard.test.tsx | 53 +++++----- .../src/components/GithubDeploymentsCard.tsx | 14 +-- .../GithubDeploymentsTable.tsx | 77 +++------------ .../GithubDeploymentsTable/columns.tsx | 99 +++++++++++++++++++ .../GithubDeploymentsTable/index.ts | 16 +++ .../GithubDeploymentsTable/presets.ts | 32 ++++++ plugins/github-deployments/src/mocks/mocks.ts | 4 +- 7 files changed, 197 insertions(+), 98 deletions(-) create mode 100644 plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx create mode 100644 plugins/github-deployments/src/components/GithubDeploymentsTable/index.ts create mode 100644 plugins/github-deployments/src/components/GithubDeploymentsTable/presets.ts diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index a3bdd2cb9c..52c84d657b 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -22,16 +22,11 @@ import { ConfigReader, ConfigApi, OAuthApi, - TableColumn, } from '@backstage/core'; import { fireEvent } from '@testing-library/react'; import { msw, renderInTestApp } from '@backstage/test-utils'; -import { - GithubDeployment, - GithubDeploymentsApiClient, - githubDeploymentsApiRef, -} from '../api'; +import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; import { githubDeploymentsPlugin } from '../plugin'; import { GithubDeploymentsCard } from './GithubDeploymentsCard'; @@ -44,6 +39,7 @@ import { import { setupServer } from 'msw/node'; import { graphql } from 'msw'; +import { GithubDeploymentsTable } from './GithubDeploymentsTable'; jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { @@ -164,28 +160,35 @@ describe('github-deployments', () => { ).toBeInTheDocument(); expect(await rendered.findByText('failure')).toBeInTheDocument(); }); - }); - it('should display extra columns', async () => { - worker.use( - graphql.query('deployments', (_, res, ctx) => - res(ctx.data(responseStub)), - ), - ); + it('should display extra columns', async () => { + worker.use( + graphql.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); - const extraColumns: TableColumn[] = [ - { - title: 'Creator', - field: 'creator.login', - }, - ]; + const renderTargetFromPayload = (payload: string) => { + const parsedPayload = JSON.parse(payload); + return parsedPayload?.target || 'unknown'; + }; - const rendered = await renderInTestApp( - - - , - ); + const columns = [ + ...GithubDeploymentsTable.defaultDeploymentColumns, + GithubDeploymentsTable.columns.createPayloadColumn( + 'Target', + renderTargetFromPayload, + ), + ]; - expect(await rendered.findByText('robot-user-001')).toBeInTheDocument(); + const rendered = await renderInTestApp( + + + , + ); + + expect(await rendered.findByText('moon')).toBeInTheDocument(); + expect(await rendered.findByText('sun')).toBeInTheDocument(); + }); }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 2a887047a1..073f78af2e 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -27,16 +27,16 @@ import { GITHUB_PROJECT_SLUG_ANNOTATION, isGithubDeploymentsAvailable, } from '../Router'; -import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; +import { GithubDeploymentsTable } from './GithubDeploymentsTable/GithubDeploymentsTable'; const GithubDeploymentsComponent = ({ projectSlug, last, - extraColumns, + columns, }: { projectSlug: string; last: number; - extraColumns: TableColumn[]; + columns: TableColumn[]; }) => { const api = useApi(githubDeploymentsApiRef); const [owner, repo] = projectSlug.split('/'); @@ -54,17 +54,17 @@ const GithubDeploymentsComponent = ({ deployments={value || []} isLoading={loading} reload={reload} - extraColumns={extraColumns} + columns={columns} /> ); }; export const GithubDeploymentsCard = ({ last, - extraColumns, + columns, }: { last?: number; - extraColumns?: TableColumn[]; + columns?: TableColumn[]; }) => { const { entity } = useEntity(); @@ -76,7 +76,7 @@ export const GithubDeploymentsCard = ({ entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] || '' } last={last || 10} - extraColumns={extraColumns || []} + columns={columns || GithubDeploymentsTable.defaultDeploymentColumns} /> ); }; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 44ed1fbcdd..e13b8aeb18 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -14,19 +14,12 @@ * limitations under the License. */ import React from 'react'; -import { - StatusPending, - StatusRunning, - StatusOK, - Table, - TableColumn, - StatusAborted, - StatusError, -} from '@backstage/core'; +import { Table, TableColumn } from '@backstage/core'; import { GithubDeployment } from '../../api'; -import { DateTime } from 'luxon'; -import { Box, Typography, Link, makeStyles } from '@material-ui/core'; +import { Typography, makeStyles } from '@material-ui/core'; import SyncIcon from '@material-ui/icons/Sync'; +import * as columnFactories from './columns'; +import { defaultDeploymentColumns } from './presets'; const useStyles = makeStyles(theme => ({ empty: { @@ -36,70 +29,24 @@ const useStyles = makeStyles(theme => ({ }, })); -const statusIndicator = (value: string): React.ReactNode => { - switch (value) { - case 'PENDING': - return ; - case 'IN_PROGRESS': - return ; - case 'ACTIVE': - return ; - case 'ERROR': - case 'FAILURE': - return ; - default: - return ; - } -}; - -const columns: TableColumn[] = [ - { - title: 'Environment', - field: 'environment', - highlight: true, - }, - { - title: 'Status', - render: (row: GithubDeployment): React.ReactNode => ( - - {statusIndicator(row.state)} - {row.state} - - ), - }, - { - title: 'Commit', - render: (row: GithubDeployment): React.ReactNode => ( - - {row.commit.abbreviatedOid} - - ), - }, - { - title: 'Last Updated', - render: (row: GithubDeployment): React.ReactNode => - DateTime.fromISO(row.updatedAt).toRelative({ locale: 'en' }), - }, -]; - type GithubDeploymentsTableProps = { deployments: GithubDeployment[]; isLoading: boolean; reload: () => void; - extraColumns: TableColumn[]; + columns: TableColumn[]; }; -const GithubDeploymentsTable = ({ +export function GithubDeploymentsTable({ deployments, isLoading, reload, - extraColumns, -}: GithubDeploymentsTableProps) => { + columns, +}: GithubDeploymentsTableProps) { const classes = useStyles(); return (
); -}; +} -export default GithubDeploymentsTable; +GithubDeploymentsTable.columns = columnFactories; + +GithubDeploymentsTable.defaultDeploymentColumns = defaultDeploymentColumns; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx new file mode 100644 index 0000000000..fe166be6e0 --- /dev/null +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + StatusPending, + StatusRunning, + StatusOK, + TableColumn, + StatusAborted, + StatusError, +} from '@backstage/core'; +import { GithubDeployment } from '../../api'; +import { DateTime } from 'luxon'; +import { Box, Typography, Link } from '@material-ui/core'; + +const statusIndicator = (value: string): React.ReactNode => { + switch (value) { + case 'PENDING': + return ; + case 'IN_PROGRESS': + return ; + case 'ACTIVE': + return ; + case 'ERROR': + case 'FAILURE': + return ; + default: + return ; + } +}; + +export function createEnvironmentColumn(): TableColumn { + return { + title: 'Environment', + field: 'environment', + highlight: true, + }; +} + +export function createStatusColumn(): TableColumn { + return { + title: 'Status', + render: (row: GithubDeployment): React.ReactNode => ( + + {statusIndicator(row.state)} + {row.state} + + ), + }; +} + +export function createCommitColumn(): TableColumn { + return { + title: 'Commit', + render: (row: GithubDeployment): React.ReactNode => ( + + {row.commit.abbreviatedOid} + + ), + }; +} + +export function createCreatorColumn(): TableColumn { + return { + title: 'Creator', + field: 'creator.login', + }; +} + +export function createLastUpdatedColumn(): TableColumn { + return { + title: 'Last Updated', + render: (row: GithubDeployment): React.ReactNode => + DateTime.fromISO(row.updatedAt).toRelative({ locale: 'en' }), + }; +} + +export function createPayloadColumn( + title: string, + render: (payload: string) => React.ReactNode, +): TableColumn { + return { + title: title, + render: (deployment: GithubDeployment) => render(deployment.payload), + }; +} diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/index.ts b/plugins/github-deployments/src/components/GithubDeploymentsTable/index.ts new file mode 100644 index 0000000000..e622d559cb --- /dev/null +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { GithubDeploymentsTable } from './GithubDeploymentsTable'; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/presets.ts b/plugins/github-deployments/src/components/GithubDeploymentsTable/presets.ts new file mode 100644 index 0000000000..b50e11dcb6 --- /dev/null +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/presets.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { TableColumn } from '@backstage/core'; +import { GithubDeployment } from '../../api'; +import { + createEnvironmentColumn, + createStatusColumn, + createCommitColumn, + createLastUpdatedColumn, + createCreatorColumn, +} from './columns'; + +export const defaultDeploymentColumns: TableColumn[] = [ + createEnvironmentColumn(), + createStatusColumn(), + createCommitColumn(), + createCreatorColumn(), + createLastUpdatedColumn(), +]; diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts index a84d684661..c0afb325db 100644 --- a/plugins/github-deployments/src/mocks/mocks.ts +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -52,7 +52,7 @@ export const responseStub: QueryResponse = { creator: { login: 'robot-user-001', }, - payload: '', + payload: '{"target":"moon"}', }, { state: 'pending', @@ -65,7 +65,7 @@ export const responseStub: QueryResponse = { creator: { login: 'robot-user-002', }, - payload: '', + payload: '{"target":"sun"}', }, ], }, From 932293a0785e52c542967ac70a55c99d5b84b16a Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 8 Apr 2021 14:26:37 +0100 Subject: [PATCH 08/36] export Signed-off-by: Andrew Johnson --- plugins/github-deployments/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/github-deployments/src/index.ts b/plugins/github-deployments/src/index.ts index 2ee681332a..06eed2a2a2 100644 --- a/plugins/github-deployments/src/index.ts +++ b/plugins/github-deployments/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ export { githubDeploymentsPlugin, EntityGithubDeploymentsCard } from './plugin'; +export { GithubDeploymentsTable } from './components/GithubDeploymentsTable'; export { isGithubDeploymentsAvailable } from './Router'; From 7c5784e3f4745ea83eb5ff795d638460ecc807fe Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 8 Apr 2021 14:38:58 +0100 Subject: [PATCH 09/36] add @types/react Signed-off-by: Andrew Johnson --- plugins/github-deployments/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index b8761c1385..2cbc63255a 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -42,6 +42,7 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", + "@types/react": "^16.9", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, From 8ced77687892c761eab45eef86d84e6acb7f7ee6 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 8 Apr 2021 14:52:17 +0100 Subject: [PATCH 10/36] move Signed-off-by: Andrew Johnson --- plugins/github-deployments/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 2cbc63255a..6d4e61d4eb 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -31,7 +31,8 @@ "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^15.3.3", + "@types/react": "^16.9" }, "devDependencies": { "@backstage/cli": "^0.6.6", @@ -42,7 +43,6 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "^16.9", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, From aec9ca24b11bb2a6cb01aa0bcea8e2d0ce24230b Mon Sep 17 00:00:00 2001 From: Victor Morfin Date: Thu, 8 Apr 2021 10:31:24 -0600 Subject: [PATCH 11/36] Changing changeset minor to patch Signed-off-by: Victor Morfin --- .changeset/stale-chefs-retire.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/stale-chefs-retire.md b/.changeset/stale-chefs-retire.md index 579f5c3042..e43c20062a 100644 --- a/.changeset/stale-chefs-retire.md +++ b/.changeset/stale-chefs-retire.md @@ -1,5 +1,5 @@ --- -'@backstage/core': minor +'@backstage/core': patch --- Adding close button on support menu From 72de2a1428535d7a74ed7ef9e38eb4d966515eb7 Mon Sep 17 00:00:00 2001 From: Victor Morfin Date: Thu, 8 Apr 2021 11:28:24 -0600 Subject: [PATCH 12/36] Changing box component to DialogActions component Signed-off-by: Victor Morfin --- .../core/src/components/SupportButton/SupportButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/SupportButton/SupportButton.tsx b/packages/core/src/components/SupportButton/SupportButton.tsx index 5bbde953b0..d0c2412dad 100644 --- a/packages/core/src/components/SupportButton/SupportButton.tsx +++ b/packages/core/src/components/SupportButton/SupportButton.tsx @@ -16,8 +16,8 @@ import { HelpIcon, useApp } from '@backstage/core-api'; import { - Box, Button, + DialogActions, List, ListItem, ListItemIcon, @@ -128,11 +128,11 @@ export const SupportButton = ({ children }: PropsWithChildren) => { {items && items.map((item, i) => )} - + - + ); From 695d5534f5a2f787c44f0b51ea49cbdb6331d44c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Apr 2021 04:18:41 +0000 Subject: [PATCH 13/36] chore(deps): bump @types/webpack-node-externals from 2.5.0 to 2.5.1 Bumps [@types/webpack-node-externals](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/webpack-node-externals) from 2.5.0 to 2.5.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/webpack-node-externals) Signed-off-by: dependabot[bot] --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7d4eda4f1c..8c0258711f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6852,11 +6852,11 @@ integrity sha512-Fx+NpfOO0CpeYX2g9bkvX8O5qh9wrU1sOF4g8sft4Mu7z+qfe387YlyY8w8daDyDsKY5vUxM0yxkAYnbkRbZEw== "@types/webpack-node-externals@^2.5.0": - version "2.5.0" - resolved "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-2.5.0.tgz#bcd161af84a4960416e5850e06931b35321c6654" - integrity sha512-KaWfhUQlpWknM/CMBKhV7i0vxX/N2xEy3WeaE500s4ZNxC4nLnKB+0F3gD3Fg+5octPq0nn8ZlfFR/P3dSkXpw== + version "2.5.1" + resolved "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-2.5.1.tgz#0f00036bce0f405ceabc092e415b734059fe5505" + integrity sha512-Cwg6+FQogkImRMF5nu5bKsLoZlwNCzpEyvxIzJM0ZgkkuKP7TrmQ3suOvNKKG1O4luxXZroKGo0mMC5EN5gPBA== dependencies: - "@types/webpack" "*" + "@types/webpack" "^4" "@types/webpack-sources@*": version "0.1.6" From c2306f898d2634b2e791e55cb9667ea9318ca3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20A=CC=8Ahsberg?= Date: Mon, 12 Apr 2021 15:04:22 +0000 Subject: [PATCH 14/36] Externalize repository processing for BitbucketDiscoveryProcessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mathias Åhsberg --- .changeset/stale-carpets-poke.md | 30 ++ docs/integrations/bitbucket/discovery.md | 26 ++ .../BitbucketDiscoveryProcessor.test.ts | 350 +++++++++++------- .../processors/BitbucketDiscoveryProcessor.ts | 45 ++- .../BitbucketRepositoryParser.test.ts | 55 +++ .../bitbucket/BitbucketRepositoryParser.ts | 41 ++ .../ingestion/processors/bitbucket/client.ts | 9 + .../ingestion/processors/bitbucket/index.ts | 3 + .../ingestion/processors/bitbucket/types.ts | 28 ++ 9 files changed, 441 insertions(+), 146 deletions(-) create mode 100644 .changeset/stale-carpets-poke.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts diff --git a/.changeset/stale-carpets-poke.md b/.changeset/stale-carpets-poke.md new file mode 100644 index 0000000000..4f39e57b71 --- /dev/null +++ b/.changeset/stale-carpets-poke.md @@ -0,0 +1,30 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Externalize repository processing for BitbucketDiscoveryProcessor. + +Add an extension point where you can customize how a matched Bitbucket repository should +be processed. This can for example be used if you want to generate the catalog-info.yaml +automatically based on other files in a repository, while taking advantage of the +build-in repository crawling functionality. + +`BitbucketDiscoveryProcessor.fromConfig` now takes an optional parameter `options.parser` where +you can customize the logic for each repository found. The default parser has the same +behaviour as before, where it emits an optional location for the matched repository +and lets the other processors take care of further processing. + +```typescript +const customRepositoryParser: BitbucketRepositoryParser = async function* customRepositoryParser({ + client, + repository, +}) { + // Custom logic for interpret the matching repository. + // See defaultRepositoryParser for an example +}; + +const processor = BitbucketDiscoveryProcessor.fromConfig(env.config, { + parser: customRepositoryParser, + logger: env.logger, +}); +``` diff --git a/docs/integrations/bitbucket/discovery.md b/docs/integrations/bitbucket/discovery.md index b24734abde..fadb7c9f06 100644 --- a/docs/integrations/bitbucket/discovery.md +++ b/docs/integrations/bitbucket/discovery.md @@ -39,3 +39,29 @@ The target is composed of four parts: - The path within each repository to find the catalog YAML file. This will usually be `/catalog-info.yaml` or a similar variation for catalog files stored in the root directory of each repository. + +## Custom repository processing + +The Bitbucket Discovery Processor will by default emit a location for each +matching repository for further processing by other processors. However, it is +possible to override this functionality and take full control of how each +matching repository is processed. + +`BitbucketDiscoveryProcessor.fromConfig` takes an optional parameter +`options.parser` where you can set your own parser to be used for each matched +repository. + +```typescript +const customRepositoryParser: BitbucketRepositoryParser = async function* customRepositoryParser({ + client, + repository, +}) { + // Custom logic for interpret the matching repository. + // See defaultRepositoryParser for an example +}; + +const processor = BitbucketDiscoveryProcessor.fromConfig(env.config, { + parser: customRepositoryParser, + logger: env.logger, +}); +``` diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts index 5197549d85..73f4281a51 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts @@ -14,13 +14,15 @@ * limitations under the License. */ import { getVoidLogger } from '@backstage/backend-common'; -import { - BitbucketDiscoveryProcessor, - readBitbucketOrg, -} from './BitbucketDiscoveryProcessor'; +import { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; import { ConfigReader } from '@backstage/config'; import { LocationSpec } from '@backstage/catalog-model'; -import { BitbucketClient, PagedResponse } from './bitbucket'; +import { + BitbucketClient, + BitbucketRepositoryParser, + PagedResponse, +} from './bitbucket'; +import { results } from './index'; function pagedResponse(values: any): PagedResponse { return { @@ -30,11 +32,6 @@ function pagedResponse(values: any): PagedResponse { } describe('BitbucketDiscoveryProcessor', () => { - const client: jest.Mocked = { - listProjects: jest.fn(), - listRepositories: jest.fn(), - } as any; - afterEach(() => jest.resetAllMocks()); describe('reject unrelated entries', () => { @@ -81,137 +78,236 @@ describe('BitbucketDiscoveryProcessor', () => { }); describe('handles repositories', () => { + const processor = BitbucketDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + bitbucket: [{ host: 'bitbucket.mycompany.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + it('output all repositories', async () => { - const target = - 'https://bitbucket.mycompany.com/projects/*/repos/*/catalog.yaml'; - - client.listProjects.mockResolvedValue( - pagedResponse([{ key: 'backstage' }, { key: 'demo' }]), - ); - client.listRepositories.mockResolvedValueOnce( - pagedResponse([ - { - slug: 'backstage', - links: { - self: [ - { - href: - 'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse', - }, - ], - }, - }, - ]), - ); - client.listRepositories.mockResolvedValueOnce( - pagedResponse([ - { - slug: 'demo', - links: { - self: [ - { - href: - 'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse', - }, - ], - }, - }, - ]), - ); - - const actual = await readBitbucketOrg(client, target); - expect(actual.scanned).toBe(2); - expect(actual.matches).toContainEqual({ - type: 'url', + const location: LocationSpec = { + type: 'bitbucket-discovery', target: - 'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse/catalog.yaml', + 'https://bitbucket.mycompany.com/projects/*/repos/*/catalog.yaml', + }; + + jest + .spyOn(BitbucketClient.prototype, 'listProjects') + .mockResolvedValue( + pagedResponse([{ key: 'backstage' }, { key: 'demo' }]), + ); + jest + .spyOn(BitbucketClient.prototype, 'listRepositories') + .mockResolvedValueOnce( + pagedResponse([ + { + slug: 'backstage', + links: { + self: [ + { + href: + 'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse', + }, + ], + }, + }, + ]), + ); + jest + .spyOn(BitbucketClient.prototype, 'listRepositories') + .mockResolvedValueOnce( + pagedResponse([ + { + slug: 'demo', + links: { + self: [ + { + href: + 'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse', + }, + ], + }, + }, + ]), + ); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse/catalog.yaml', + }, + optional: true, }); - expect(actual.matches).toContainEqual({ - type: 'url', - target: - 'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse/catalog.yaml', + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse/catalog.yaml', + }, + optional: true, }); }); it('output repositories with wildcards', async () => { - const target = - 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-*/catalog.yaml'; - - client.listProjects.mockResolvedValue( - pagedResponse([{ key: 'backstage' }]), - ); - client.listRepositories.mockResolvedValueOnce( - pagedResponse([ - { slug: 'backstage' }, - { - slug: 'techdocs-cli', - links: { - self: [ - { - href: - 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse', - }, - ], - }, - }, - { - slug: 'techdocs-container', - links: { - self: [ - { - href: - 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse', - }, - ], - }, - }, - ]), - ); - - const actual = await readBitbucketOrg(client, target); - expect(actual.scanned).toBe(3); - expect(actual.matches).toContainEqual({ - type: 'url', + const location: LocationSpec = { + type: 'bitbucket-discovery', target: - 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse/catalog.yaml', + 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-*/catalog.yaml', + }; + + jest + .spyOn(BitbucketClient.prototype, 'listProjects') + .mockResolvedValue(pagedResponse([{ key: 'backstage' }])); + jest + .spyOn(BitbucketClient.prototype, 'listRepositories') + .mockResolvedValueOnce( + pagedResponse([ + { slug: 'backstage' }, + { + slug: 'techdocs-cli', + links: { + self: [ + { + href: + 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse', + }, + ], + }, + }, + { + slug: 'techdocs-container', + links: { + self: [ + { + href: + 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse', + }, + ], + }, + }, + ]), + ); + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse/catalog.yaml', + }, + optional: true, }); - expect(actual.matches).toContainEqual({ - type: 'url', - target: - 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse/catalog.yaml', + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse/catalog.yaml', + }, + optional: true, }); }); it('filter unrelated repositories', async () => { - const target = - 'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml'; - - client.listProjects.mockResolvedValue( - pagedResponse([{ key: 'backstage' }]), - ); - client.listRepositories.mockResolvedValue( - pagedResponse([ - { slug: 'abstest' }, - { slug: 'testxyz' }, - { - slug: 'test', - links: { - self: [ - { - href: - 'https://bitbucket.mycompany.com/projects/backstage/repos/test', - }, - ], - }, - }, - ]), - ); - - const actual = await readBitbucketOrg(client, target); - expect(actual.scanned).toBe(3); - expect(actual.matches).toContainEqual({ - type: 'url', + const location: LocationSpec = { + type: 'bitbucket-discovery', target: 'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml', + }; + + jest + .spyOn(BitbucketClient.prototype, 'listProjects') + .mockResolvedValue(pagedResponse([{ key: 'backstage' }])); + jest + .spyOn(BitbucketClient.prototype, 'listRepositories') + .mockResolvedValue( + pagedResponse([ + { slug: 'abstest' }, + { slug: 'testxyz' }, + { + slug: 'test', + links: { + self: [ + { + href: + 'https://bitbucket.mycompany.com/projects/backstage/repos/test', + }, + ], + }, + }, + ]), + ); + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml', + }, + optional: true, + }); + }); + }); + + describe('Custom repository parser', () => { + const customRepositoryParser: BitbucketRepositoryParser = async function* customRepositoryParser({}) { + yield results.location( + { + type: 'custom-location-type', + target: 'custom-target', + }, + true, + ); + }; + + const processor = BitbucketDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + bitbucket: [{ host: 'bitbucket.mycompany.com', token: 'blob' }], + }, + }), + { parser: customRepositoryParser, logger: getVoidLogger() }, + ); + + it('use custom repository parser', async () => { + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml', + }; + + jest + .spyOn(BitbucketClient.prototype, 'listProjects') + .mockResolvedValue(pagedResponse([{ key: 'backstage' }])); + jest + .spyOn(BitbucketClient.prototype, 'listRepositories') + .mockResolvedValue(pagedResponse([{ slug: 'test' }])); + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'custom-location-type', + target: 'custom-target', + }, + optional: true, }); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts index f3dae9c112..f92a5c6f7c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts @@ -21,15 +21,24 @@ import { ScmIntegrations, } from '@backstage/integration'; import { LocationSpec } from '@backstage/catalog-model'; -import { BitbucketClient, paginated } from './bitbucket'; +import { + Repository, + BitbucketRepositoryParser, + BitbucketClient, + defaultRepositoryParser, + paginated, +} from './bitbucket'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; -import { results } from './index'; export class BitbucketDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; + private readonly parser: BitbucketRepositoryParser; private readonly logger: Logger; - static fromConfig(config: Config, options: { logger: Logger }) { + static fromConfig( + config: Config, + options: { parser?: BitbucketRepositoryParser; logger: Logger }, + ) { const integrations = ScmIntegrations.fromConfig(config); return new BitbucketDiscoveryProcessor({ @@ -40,9 +49,11 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; + parser?: BitbucketRepositoryParser; logger: Logger; }) { this.integrations = options.integrations; + this.parser = options.parser || defaultRepositoryParser; this.logger = options.logger; } @@ -73,18 +84,18 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { const startTimestamp = Date.now(); this.logger.info(`Reading Bitbucket repositories from ${location.target}`); + const { catalogPath } = parseUrl(location.target); + const result = await readBitbucketOrg(client, location.target); for (const repository of result.matches) { - emit( - results.location( - repository, - // Not all locations may actually exist, since the user defined them as a wildcard pattern. - // Thus, we emit them as optional and let the downstream processor find them while not outputting - // an error if it couldn't. - true, - ), - ); + for await (const entity of this.parser({ + client: client, + repository: repository, + path: catalogPath, + })) { + emit(entity); + } } const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); @@ -100,7 +111,7 @@ export async function readBitbucketOrg( client: BitbucketClient, target: string, ): Promise { - const { projectSearchPath, repoSearchPath, catalogPath } = parseUrl(target); + const { projectSearchPath, repoSearchPath } = parseUrl(target); const projects = paginated(options => client.listProjects(options)); const result: Result = { scanned: 0, @@ -116,12 +127,8 @@ export async function readBitbucketOrg( ); for await (const repository of repositories) { result.scanned++; - if (repoSearchPath.test(repository.slug)) { - result.matches.push({ - type: 'url', - target: `${repository.links.self[0].href}${catalogPath}`, - }); + result.matches.push(repository); } } } @@ -152,5 +159,5 @@ function escapeRegExp(str: string): RegExp { type Result = { scanned: number; - matches: LocationSpec[]; + matches: Repository[]; }; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts new file mode 100644 index 0000000000..ab5080f446 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { defaultRepositoryParser } from './BitbucketRepositoryParser'; +import { Project, Repository } from './types'; +import { BitbucketClient } from './client'; +import { results } from '../index'; + +describe('BitbucketRepositoryParser', () => { + describe('defaultRepositoryParser', () => { + it('emits location', async () => { + const browseUrl = + 'https://bitbucket.mycompany.com/projects/project-key/repos/repo-slug/browse'; + const path = '/catalog-info.yaml'; + const expected = [ + results.location( + { + type: 'url', + target: `${browseUrl}${path}`, + }, + true, + ), + ]; + const actual = await defaultRepositoryParser({ + client: {} as BitbucketClient, + repository: { + project: {} as Project, + slug: 'repo-slug', + links: { + self: [{ href: browseUrl }], + }, + } as Repository, + path: path, + }); + + let i = 0; + for await (const entity of actual) { + expect(entity).toStrictEqual(expected[i]); + i++; + } + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts new file mode 100644 index 0000000000..f786b6dac8 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Repository } from './types'; +import { CatalogProcessorResult } from '../types'; +import { results } from '../index'; +import { BitbucketClient } from './client'; + +export type BitbucketRepositoryParser = (options: { + client: BitbucketClient; + repository: Repository; + path: string; +}) => AsyncIterable; + +export const defaultRepositoryParser: BitbucketRepositoryParser = async function* defaultRepositoryParser({ + repository, + path, +}) { + yield results.location( + { + type: 'url', + target: `${repository.links.self[0].href}${path}`, + }, + // Not all locations may actually exist, since the user defined them as a wildcard pattern. + // Thus, we emit them as optional and let the downstream processor find them while not outputting + // an error if it couldn't. + true, + ); +}; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts index c3c27aedfc..601461dcf5 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts @@ -41,6 +41,15 @@ export class BitbucketClient { ); } + async getRaw( + projectKey: string, + repo: string, + path: string, + ): Promise { + const request = `${this.config.apiBaseUrl}/projects/${projectKey}/repos/${repo}/raw/${path}`; + return fetch(request, getBitbucketRequestOptions(this.config)); + } + private async pagedRequest( endpoint: string, options?: ListOptions, diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts index 7e70bcfe7a..ba2a2b3afe 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts @@ -15,3 +15,6 @@ */ export { BitbucketClient, paginated } from './client'; export type { PagedResponse } from './client'; +export * from './types'; +export type { BitbucketRepositoryParser } from './BitbucketRepositoryParser'; +export { defaultRepositoryParser } from './BitbucketRepositoryParser'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts new file mode 100644 index 0000000000..75dd372faa --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type Project = { + key: string; +}; + +export type Repository = { + project: Project; + slug: string; + links: Record; +}; + +export type Link = { + href: string; +}; From b42531cfedc94bbdc4fa8a445f4a2b2a341bd395 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 13 Apr 2021 13:53:43 +0200 Subject: [PATCH 15/36] Support configuration of file storage for SQLite databases Signed-off-by: Oliver Sand --- .changeset/six-turtles-sip.md | 6 +++ packages/backend-common/config.d.ts | 2 +- .../backend-common/src/database/connection.ts | 2 +- .../src/database/sqlite3.test.ts | 36 +++++++++++++-- .../backend-common/src/database/sqlite3.ts | 45 +++++++++++++++++-- 5 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 .changeset/six-turtles-sip.md diff --git a/.changeset/six-turtles-sip.md b/.changeset/six-turtles-sip.md new file mode 100644 index 0000000000..e71b04e64d --- /dev/null +++ b/.changeset/six-turtles-sip.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +--- + +Support configuration of file storage for SQLite databases. Every plugin has its +own database file at the specified path. diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 74c199e737..41845f952e 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -57,7 +57,7 @@ export interface Config { database: | { client: 'sqlite3'; - connection: ':memory:' | string; + connection: ':memory:' | string | { filename: string }; } | { client: 'pg'; diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 17ef2c461d..3502e21674 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -37,7 +37,7 @@ export function createDatabaseClient( if (client === 'pg') { return createPgDatabaseClient(dbConfig, overrides); } else if (client === 'sqlite3') { - return createSqliteDatabaseClient(dbConfig); + return createSqliteDatabaseClient(dbConfig, overrides); } return knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides)); diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/sqlite3.test.ts index a6b8e5d84d..3066cb6c9d 100644 --- a/packages/backend-common/src/database/sqlite3.test.ts +++ b/packages/backend-common/src/database/sqlite3.test.ts @@ -25,15 +25,23 @@ describe('sqlite3', () => { new ConfigReader({ client: 'sqlite3', connection }); describe('buildSqliteDatabaseConfig', () => { - it('buidls a string connection', () => { + it('builds an in memory connection', () => { expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({ client: 'sqlite3', - connection: ':memory:', + connection: { filename: ':memory:' }, useNullAsDefault: true, }); }); - it('builds a filename connection', () => { + it('builds a persistent connection, normalize config with filename', () => { + expect(buildSqliteDatabaseConfig(createConfig('/path/to/foo'))).toEqual({ + client: 'sqlite3', + connection: { filename: '/path/to/foo' }, + useNullAsDefault: true, + }); + }); + + it('builds a persistent connection', () => { expect( buildSqliteDatabaseConfig( createConfig({ @@ -49,6 +57,28 @@ describe('sqlite3', () => { }); }); + it('builds a persistent connection per database', () => { + expect( + buildSqliteDatabaseConfig( + createConfig({ + filename: '/path/to/foo', + }), + { + connection: { + database: 'my-database', + }, + }, + ), + ).toEqual({ + client: 'sqlite3', + connection: { + filename: '/path/to/foo/my-database.sqlite', + database: 'my-database', + }, + useNullAsDefault: true, + }); + }); + it('replaces the connection with an override', () => { expect( buildSqliteDatabaseConfig(createConfig(':memory:'), { diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index f5742c68a0..9250315413 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -14,8 +14,10 @@ * limitations under the License. */ -import knexFactory, { Knex } from 'knex'; import { Config } from '@backstage/config'; +import fs from 'fs'; +import knexFactory, { Knex } from 'knex'; +import path from 'path'; import { mergeDatabaseConfig } from './config'; /** @@ -29,6 +31,20 @@ export function createSqliteDatabaseClient( overrides?: Knex.Config, ) { const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); + + // If storage on disk is used, ensure that the directory exists + if ( + typeof knexConfig.connection === 'object' && + (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename + ) { + const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; + const directory = path.dirname(filename); + + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } + } + const database = knexFactory(knexConfig); database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { @@ -47,12 +63,33 @@ export function createSqliteDatabaseClient( export function buildSqliteDatabaseConfig( dbConfig: Config, overrides?: Knex.Config, -) { - return mergeDatabaseConfig( - dbConfig.get(), +): Knex.Config { + const baseConfig = dbConfig.get(); + + // Normalize config to always contain a connection object + if (typeof baseConfig.connection === 'string') { + baseConfig.connection = { filename: baseConfig.connection }; + } + + const config: Knex.Config = mergeDatabaseConfig( + baseConfig, { useNullAsDefault: true, }, overrides, ); + + // If we don't create an in-memory database, interpret the connection string + // as a directory that contains multiple sqlite files based on the database + // name. + if (config.connection && typeof config.connection === 'object') { + const database = (config.connection as Knex.ConnectionConfig).database; + const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig; + + if (database && sqliteConnection.filename !== ':memory:') { + sqliteConnection.filename = `${sqliteConnection.filename}/${database}.sqlite`; + } + } + + return config; } From c42cd1daaf45b6fd90e6a8cc06847897b3b2f112 Mon Sep 17 00:00:00 2001 From: Travis Truman Date: Tue, 13 Apr 2021 12:55:28 -0400 Subject: [PATCH 16/36] Kubernetes client TLS verification is now configurable Verification now defaults to true, where previously it defaulted to false Signed-off-by: Travis Truman --- .changeset/rude-items-bow.md | 5 +++++ docs/features/kubernetes/configuration.md | 6 ++++++ .../src/cluster-locator/ConfigClusterLocator.test.ts | 5 +++++ .../src/cluster-locator/ConfigClusterLocator.ts | 4 ++++ .../kubernetes-backend/src/cluster-locator/index.test.ts | 2 ++ .../src/service/KubernetesClientProvider.test.ts | 3 +++ .../src/service/KubernetesClientProvider.ts | 3 +-- plugins/kubernetes-backend/src/types/types.ts | 1 + 8 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 .changeset/rude-items-bow.md diff --git a/.changeset/rude-items-bow.md b/.changeset/rude-items-bow.md new file mode 100644 index 0000000000..69b76d821e --- /dev/null +++ b/.changeset/rude-items-bow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Kubernetes client TLS verification is now configurable and defaults to true diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index c21ac7e18a..4ac324a430 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -25,6 +25,7 @@ kubernetes: - url: http://127.0.0.1:9999 name: minikube authProvider: 'serviceAccount' + skipTLSVerify: false serviceAccountToken: $env: K8S_MINIKUBE_TOKEN - url: http://127.0.0.2:9999 @@ -79,6 +80,11 @@ cluster. Valid values are: | `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | | `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. | +##### `clusters.\*.skipTLSVerify` + +This determines whether or not the Kubernetes client verifies the TLS +certificate presented by the API server. + ##### `clusters.\*.serviceAccountToken` (optional) The service account token to be used when using the `serviceAccount` auth diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index cb79a3020c..6ad8bdd9a1 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -52,6 +52,7 @@ describe('ConfigClusterLocator', () => { serviceAccountToken: undefined, url: 'http://localhost:8080', authProvider: 'serviceAccount', + skipTLSVerify: false, }, ]); }); @@ -64,11 +65,13 @@ describe('ConfigClusterLocator', () => { serviceAccountToken: 'token', url: 'http://localhost:8080', authProvider: 'serviceAccount', + skipTLSVerify: false, }, { name: 'cluster2', url: 'http://localhost:8081', authProvider: 'google', + skipTLSVerify: true, }, ], }); @@ -83,12 +86,14 @@ describe('ConfigClusterLocator', () => { serviceAccountToken: 'token', url: 'http://localhost:8080', authProvider: 'serviceAccount', + skipTLSVerify: false, }, { name: 'cluster2', serviceAccountToken: undefined, url: 'http://localhost:8081', authProvider: 'google', + skipTLSVerify: true, }, ]); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index e1016789af..62b127cd60 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -33,6 +33,10 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { name: c.getString('name'), url: c.getString('url'), serviceAccountToken: c.getOptionalString('serviceAccountToken'), + skipTLSVerify: + c.getOptionalBoolean('skipTLSVerify') === undefined + ? false + : c.getOptionalBoolean('skipTLSVerify'), authProvider: c.getString('authProvider'), }; }), diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index d586f90151..d7eb98719f 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -53,12 +53,14 @@ describe('getCombinedClusterDetails', () => { serviceAccountToken: 'token', url: 'http://localhost:8080', authProvider: 'serviceAccount', + skipTLSVerify: false, }, { name: 'cluster2', serviceAccountToken: undefined, url: 'http://localhost:8081', authProvider: 'google', + skipTLSVerify: false, }, ]); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts index 4655e5f552..be6fc9c47c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts @@ -34,6 +34,7 @@ describe('KubernetesClientProvider', () => { url: 'http://localhost:9999', serviceAccountToken: 'TOKEN', authProvider: 'serviceAccount', + skipTLSVerify: false, }); expect(result.basePath).toBe('http://localhost:9999'); @@ -41,6 +42,7 @@ describe('KubernetesClientProvider', () => { const auth = (result as any).authentications.default; expect(auth.users[0].token).toBe('TOKEN'); expect(auth.clusters[0].name).toBe('cluster-name'); + expect(auth.clusters[0].skipTLSVerify).toBe(false); expect(mockGetKubeConfig.mock.calls.length).toBe(1); }); @@ -57,6 +59,7 @@ describe('KubernetesClientProvider', () => { url: 'http://localhost:9999', serviceAccountToken: 'TOKEN', authProvider: 'serviceAccount', + skipTLSVerify: false, }); expect(result.basePath).toBe('http://localhost:9999'); diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts index cd1c6afedf..25ed40322a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts @@ -30,8 +30,7 @@ export class KubernetesClientProvider { const cluster = { name: clusterDetails.name, server: clusterDetails.url, - // TODO configure this - skipTLSVerify: true, + skipTLSVerify: clusterDetails.skipTLSVerify, }; // TODO configure diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 84ca08585b..c597c718c5 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -30,6 +30,7 @@ export interface ClusterDetails { url: string; authProvider: string; serviceAccountToken?: string | undefined; + skipTLSVerify?: boolean; } export interface KubernetesRequestBody { From 09b5fcf2e4357fba580798e2dfc8f3c1002e7dc6 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 13 Apr 2021 19:44:21 +0300 Subject: [PATCH 17/36] Filtered archived repositories when discovering repos Signed-off-by: Nir Gazit --- .changeset/real-apples-visit.md | 5 +++ .../GithubDiscoveryProcessor.test.ts | 35 ++++++++++++++++--- .../processors/GithubDiscoveryProcessor.ts | 4 ++- .../processors/github/github.test.ts | 9 ++++- .../src/ingestion/processors/github/github.ts | 2 ++ 5 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 .changeset/real-apples-visit.md diff --git a/.changeset/real-apples-visit.md b/.changeset/real-apples-visit.md new file mode 100644 index 0000000000..b5165f633b --- /dev/null +++ b/.changeset/real-apples-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +GithubDiscoveryProcessor now excludes archived repositories so they won't be added to Backstage. diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts index 9c9aa41740..30778fe7fd 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts @@ -118,8 +118,16 @@ describe('GithubDiscoveryProcessor', () => { }; mockGetOrganizationRepositories.mockResolvedValueOnce({ repositories: [ - { name: 'backstage', url: 'https://github.com/backstage/backstage' }, - { name: 'demo', url: 'https://github.com/backstage/demo' }, + { + name: 'backstage', + url: 'https://github.com/backstage/backstage', + isArchived: false, + }, + { + name: 'demo', + url: 'https://github.com/backstage/demo', + isArchived: false, + }, ], }); const emitter = jest.fn(); @@ -153,14 +161,20 @@ describe('GithubDiscoveryProcessor', () => { }; mockGetOrganizationRepositories.mockResolvedValueOnce({ repositories: [ - { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'backstage', + url: 'https://github.com/backstage/backstage', + isArchived: false, + }, { name: 'techdocs-cli', url: 'https://github.com/backstage/techdocs-cli', + isArchived: false, }, { name: 'techdocs-container', url: 'https://github.com/backstage/techdocs-container', + isArchived: false, }, ], }); @@ -187,21 +201,32 @@ describe('GithubDiscoveryProcessor', () => { optional: true, }); }); - it('filter unrelated repositories', async () => { + it('filter unrelated and archived repositories', async () => { const location: LocationSpec = { type: 'github-discovery', target: 'https://github.com/backstage/test/blob/master/catalog.yaml', }; mockGetOrganizationRepositories.mockResolvedValueOnce({ repositories: [ - { name: 'abstest', url: 'https://github.com/backstage/abctest' }, + { + name: 'abstest', + url: 'https://github.com/backstage/abctest', + isArchived: false, + }, { name: 'test', url: 'https://github.com/backstage/test', + isArchived: false, + }, + { + name: 'test-archived', + url: 'https://github.com/backstage/test', + isArchived: true, }, { name: 'testxyz', url: 'https://github.com/backstage/testxyz', + isArchived: false, }, ], }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts index f1818a3ab8..e187c49196 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -78,7 +78,9 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { this.logger.info(`Reading GitHub repositories from ${location.target}`); const { repositories } = await getOrganizationRepositories(client, org); - const matching = repositories.filter(r => repoSearchPath.test(r.name)); + const matching = repositories.filter( + r => !r.isArchived && repoSearchPath.test(r.name), + ); const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); this.logger.debug( diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts index 81b44c706d..15280b96b8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts @@ -162,10 +162,12 @@ describe('github', () => { { name: 'backstage', url: 'https://github.com/backstage/backstage', + isArchived: false, }, { name: 'demo', url: 'https://github.com/backstage/demo', + isArchived: true, }, ], pageInfo: { @@ -177,10 +179,15 @@ describe('github', () => { const output = { repositories: [ - { name: 'backstage', url: 'https://github.com/backstage/backstage' }, + { + name: 'backstage', + url: 'https://github.com/backstage/backstage', + isArchived: false, + }, { name: 'demo', url: 'https://github.com/backstage/demo', + isArchived: true, }, ], }; diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.ts index d50887c592..e07ea8917b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.ts @@ -56,6 +56,7 @@ export type Team = { export type Repository = { name: string; url: string; + isArchived: boolean; }; export type Connection = { @@ -234,6 +235,7 @@ export async function getOrganizationRepositories( nodes { name url + isArchived } pageInfo { hasNextPage From 2fc0c9b1c0d731f45a6b301c3848123161dff244 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 14 Apr 2021 15:41:27 +0200 Subject: [PATCH 18/36] Make tests path separator independent and work on Windows Signed-off-by: Oliver Sand --- .../src/database/sqlite3.test.ts | 19 +++++++++++-------- .../backend-common/src/database/sqlite3.ts | 5 ++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/sqlite3.test.ts index 3066cb6c9d..cf22fa28ee 100644 --- a/packages/backend-common/src/database/sqlite3.test.ts +++ b/packages/backend-common/src/database/sqlite3.test.ts @@ -15,6 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; +import path from 'path'; import { buildSqliteDatabaseConfig, createSqliteDatabaseClient, @@ -34,9 +35,11 @@ describe('sqlite3', () => { }); it('builds a persistent connection, normalize config with filename', () => { - expect(buildSqliteDatabaseConfig(createConfig('/path/to/foo'))).toEqual({ + expect( + buildSqliteDatabaseConfig(createConfig(path.join('path', 'to', 'foo'))), + ).toEqual({ client: 'sqlite3', - connection: { filename: '/path/to/foo' }, + connection: { filename: path.join('path', 'to', 'foo') }, useNullAsDefault: true, }); }); @@ -45,13 +48,13 @@ describe('sqlite3', () => { expect( buildSqliteDatabaseConfig( createConfig({ - filename: '/path/to/foo', + filename: path.join('path', 'to', 'foo'), }), ), ).toEqual({ client: 'sqlite3', connection: { - filename: '/path/to/foo', + filename: path.join('path', 'to', 'foo'), }, useNullAsDefault: true, }); @@ -61,7 +64,7 @@ describe('sqlite3', () => { expect( buildSqliteDatabaseConfig( createConfig({ - filename: '/path/to/foo', + filename: path.join('path', 'to', 'foo'), }), { connection: { @@ -72,7 +75,7 @@ describe('sqlite3', () => { ).toEqual({ client: 'sqlite3', connection: { - filename: '/path/to/foo/my-database.sqlite', + filename: path.join('path', 'to', 'foo', 'my-database.sqlite'), database: 'my-database', }, useNullAsDefault: true, @@ -82,12 +85,12 @@ describe('sqlite3', () => { it('replaces the connection with an override', () => { expect( buildSqliteDatabaseConfig(createConfig(':memory:'), { - connection: { filename: '/path/to/foo' }, + connection: { filename: path.join('path', 'to', 'foo') }, }), ).toEqual({ client: 'sqlite3', connection: { - filename: '/path/to/foo', + filename: path.join('path', 'to', 'foo'), }, useNullAsDefault: true, }); diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index 9250315413..c73f3f7029 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -87,7 +87,10 @@ export function buildSqliteDatabaseConfig( const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig; if (database && sqliteConnection.filename !== ':memory:') { - sqliteConnection.filename = `${sqliteConnection.filename}/${database}.sqlite`; + sqliteConnection.filename = path.join( + sqliteConnection.filename, + `${database}.sqlite`, + ); } } From 4b6e31ece2ad0233f010176fdade5e85d605665f Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 14 Apr 2021 15:46:35 +0200 Subject: [PATCH 19/36] Also normalize the config override Signed-off-by: Oliver Sand --- .../backend-common/src/database/sqlite3.test.ts | 15 ++++++++++++++- packages/backend-common/src/database/sqlite3.ts | 7 +++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/sqlite3.test.ts index cf22fa28ee..86f3a6968b 100644 --- a/packages/backend-common/src/database/sqlite3.test.ts +++ b/packages/backend-common/src/database/sqlite3.test.ts @@ -26,7 +26,7 @@ describe('sqlite3', () => { new ConfigReader({ client: 'sqlite3', connection }); describe('buildSqliteDatabaseConfig', () => { - it('builds an in memory connection', () => { + it('builds an in-memory connection', () => { expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({ client: 'sqlite3', connection: { filename: ':memory:' }, @@ -34,6 +34,19 @@ describe('sqlite3', () => { }); }); + it('builds an in-memory connection by override with filename', () => { + expect( + buildSqliteDatabaseConfig( + createConfig(path.join('path', 'to', 'foo')), + { connection: ':memory:' }, + ), + ).toEqual({ + client: 'sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }); + }); + it('builds a persistent connection, normalize config with filename', () => { expect( buildSqliteDatabaseConfig(createConfig(path.join('path', 'to', 'foo'))), diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index c73f3f7029..170ea5215d 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -34,7 +34,7 @@ export function createSqliteDatabaseClient( // If storage on disk is used, ensure that the directory exists if ( - typeof knexConfig.connection === 'object' && + knexConfig.connection && (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename ) { const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; @@ -70,6 +70,9 @@ export function buildSqliteDatabaseConfig( if (typeof baseConfig.connection === 'string') { baseConfig.connection = { filename: baseConfig.connection }; } + if (overrides && typeof overrides.connection === 'string') { + overrides.connection = { filename: overrides.connection }; + } const config: Knex.Config = mergeDatabaseConfig( baseConfig, @@ -82,7 +85,7 @@ export function buildSqliteDatabaseConfig( // If we don't create an in-memory database, interpret the connection string // as a directory that contains multiple sqlite files based on the database // name. - if (config.connection && typeof config.connection === 'object') { + if (config.connection) { const database = (config.connection as Knex.ConnectionConfig).database; const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig; From 0c49f4461ef0cec425e4282e6666ecd2327a5f9b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 14 Apr 2021 15:56:49 +0200 Subject: [PATCH 20/36] Ensure that the connection object is always initialized Signed-off-by: Oliver Sand --- .../backend-common/src/database/sqlite3.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index 170ea5215d..d0d36df51d 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -33,10 +33,7 @@ export function createSqliteDatabaseClient( const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); // If storage on disk is used, ensure that the directory exists - if ( - knexConfig.connection && - (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename - ) { + if ((knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename) { const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; const directory = path.dirname(filename); @@ -75,6 +72,9 @@ export function buildSqliteDatabaseConfig( } const config: Knex.Config = mergeDatabaseConfig( + { + connection: {}, + }, baseConfig, { useNullAsDefault: true, @@ -85,16 +85,14 @@ export function buildSqliteDatabaseConfig( // If we don't create an in-memory database, interpret the connection string // as a directory that contains multiple sqlite files based on the database // name. - if (config.connection) { - const database = (config.connection as Knex.ConnectionConfig).database; - const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig; + const database = (config.connection as Knex.ConnectionConfig).database; + const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig; - if (database && sqliteConnection.filename !== ':memory:') { - sqliteConnection.filename = path.join( - sqliteConnection.filename, - `${database}.sqlite`, - ); - } + if (database && sqliteConnection.filename !== ':memory:') { + sqliteConnection.filename = path.join( + sqliteConnection.filename, + `${database}.sqlite`, + ); } return config; From 4e5c942491dd1f52d8e0deff5f1877ff8192b87f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 14 Apr 2021 17:42:11 +0200 Subject: [PATCH 21/36] cli: added config:docs command Signed-off-by: Patrik Oldsberg --- .changeset/fair-carrots-tell.md | 5 +++ packages/cli/src/commands/config/docs.ts | 40 ++++++++++++++++++++++++ packages/cli/src/commands/index.ts | 9 ++++++ 3 files changed, 54 insertions(+) create mode 100644 .changeset/fair-carrots-tell.md create mode 100644 packages/cli/src/commands/config/docs.ts diff --git a/.changeset/fair-carrots-tell.md b/.changeset/fair-carrots-tell.md new file mode 100644 index 0000000000..62dce5c3ce --- /dev/null +++ b/.changeset/fair-carrots-tell.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add `config:docs` command that opens up reference documentation for the local configuration schema in a browser. diff --git a/packages/cli/src/commands/config/docs.ts b/packages/cli/src/commands/config/docs.ts new file mode 100644 index 0000000000..e06bc42c27 --- /dev/null +++ b/packages/cli/src/commands/config/docs.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { JsonObject } from '@backstage/config'; +import { mergeConfigSchemas } from '@backstage/config-loader'; +import { Command } from 'commander'; +import { JSONSchema7 as JSONSchema } from 'json-schema'; +import openBrowser from 'react-dev-utils/openBrowser'; +import { loadCliConfig } from '../../lib/config'; + +const DOCS_URL = 'https://config.backstage.io'; + +export default async (cmd: Command) => { + const { schema: appSchemas } = await loadCliConfig({ + args: [], + fromPackage: cmd.package, + mockEnv: true, + }); + + const schema = mergeConfigSchemas( + (appSchemas.serialize().schemas as JsonObject[]).map( + _ => _.value as JSONSchema, + ), + ); + + openBrowser(`${DOCS_URL}#schema=${JSON.stringify(schema)}`); +}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 46b6deeb21..c770239464 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -140,6 +140,15 @@ export function registerCommands(program: CommanderStatic) { .description('Run tests, forwarding args to Jest, defaulting to watch mode') .action(lazy(() => import('./testCommand').then(m => m.default))); + program + .command('config:docs') + .option( + '--package ', + 'Only include the schema that applies to the given package', + ) + .description('Browse the configuration reference documentation') + .action(lazy(() => import('./config/docs').then(m => m.default))); + program .command('config:print') .option( From ff42512d3a31f2a51814bcf35a2ca7ef53b36590 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 14 Apr 2021 17:47:27 +0200 Subject: [PATCH 22/36] docs/cli: add config:docs command Signed-off-by: Patrik Oldsberg --- docs/cli/commands.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 33693b010f..324d17463c 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -44,6 +44,7 @@ clean Delete cache directories create-plugin Creates a new plugin in the current repository remove-plugin Removes plugin in the current repository +config:docs Browse the configuration reference documentation config:print Print the app configuration for the current package config:check Validate that the given configuration loads and matches schema config:schema Dump the app configuration schema @@ -447,6 +448,25 @@ Options: --backstage-cli-help display help for command ``` +## config:docs + +Scope: `root` + +This commands opens up the reference documentation of your apps local +configuration schema in the browser. This is useful to get an overview of what +configuration values are available to use, a description of what they do and +their format, and where they get sent. + +```text +Usage: backstage-cli config:docs [options] + +Browse the configuration reference documentation + +Options: + --package Only include the schema that applies to the given package + -h, --help display help for command +``` + ## config:print Scope: `root` From 00755782242b024ef29e200216db0fd477d9c7d3 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 15 Apr 2021 10:55:43 +0200 Subject: [PATCH 23/36] Use ensureDirSync Signed-off-by: Oliver Sand --- packages/backend-common/src/database/sqlite3.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index d0d36df51d..90328f9527 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import fs from 'fs'; +import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; import path from 'path'; import { mergeDatabaseConfig } from './config'; @@ -37,9 +37,7 @@ export function createSqliteDatabaseClient( const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; const directory = path.dirname(filename); - if (!fs.existsSync(directory)) { - fs.mkdirSync(directory, { recursive: true }); - } + ensureDirSync(directory); } const database = knexFactory(knexConfig); From 3f048fbd250aca3cb68ae489f87ee4c714659d76 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 15 Apr 2021 11:30:12 +0200 Subject: [PATCH 24/36] Don't ensure directory exists if :memory: storage is used Signed-off-by: Oliver Sand --- packages/backend-common/src/database/sqlite3.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/sqlite3.ts index 90328f9527..d4169e3899 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/sqlite3.ts @@ -33,7 +33,11 @@ export function createSqliteDatabaseClient( const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); // If storage on disk is used, ensure that the directory exists - if ((knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename) { + if ( + (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename && + (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename !== + ':memory:' + ) { const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; const directory = path.dirname(filename); From ab07d77f6884bd7067b511486b8d5d4df3a755e9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Apr 2021 17:48:38 +0200 Subject: [PATCH 25/36] core-api: discover plugins in app element tree Signed-off-by: Patrik Oldsberg --- .changeset/good-glasses-build.md | 6 +++++ packages/core-api/src/app/App.test.tsx | 3 +++ packages/core-api/src/app/App.tsx | 33 ++++++++++++++++++++------ 3 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 .changeset/good-glasses-build.md diff --git a/.changeset/good-glasses-build.md b/.changeset/good-glasses-build.md new file mode 100644 index 0000000000..3d506d3e43 --- /dev/null +++ b/.changeset/good-glasses-build.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-api': patch +'@backstage/core': patch +--- + +Add support for discovering plugins through the app element tree, removing the need to register them explicitly. diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx index c91eef1fb0..3cade002de 100644 --- a/packages/core-api/src/app/App.test.tsx +++ b/packages/core-api/src/app/App.test.tsx @@ -212,6 +212,9 @@ describe('Integration Test', () => { expect(screen.getByText('extLink2: /foo/a')).toBeInTheDocument(); expect(screen.getByText('extLink3: /sub1')).toBeInTheDocument(); expect(screen.getByText('extLink4: /foo/b')).toBeInTheDocument(); + + // Plugins should be discovered through element tree + expect(app.getPlugins()).toEqual([plugin1, plugin2]); }); it('runs happy paths without optional routes', async () => { diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index d2346e478c..fdbee075fc 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -52,6 +52,7 @@ import { } from '../extensions/traversal'; import { IconComponent, IconComponentMap, IconKey } from '../icons'; import { BackstagePlugin } from '../plugin'; +import { pluginCollector } from '../plugin/collectors'; import { AnyRoutes } from '../plugin/types'; import { RouteRef, ExternalRouteRef, SubRouteRef } from '../routing'; import { @@ -189,7 +190,7 @@ export class PrivateAppImpl implements BackstageApp { private readonly apis: Iterable; private readonly icons: IconComponentMap; - private readonly plugins: BackstagePlugin[]; + private readonly plugins: Set>; private readonly components: AppComponents; private readonly themes: AppTheme[]; private readonly configLoader?: AppConfigLoader; @@ -201,7 +202,7 @@ export class PrivateAppImpl implements BackstageApp { constructor(options: FullAppOptions) { this.apis = options.apis; this.icons = options.icons; - this.plugins = options.plugins; + this.plugins = new Set(options.plugins); this.components = options.components; this.themes = options.themes; this.configLoader = options.configLoader; @@ -210,7 +211,7 @@ export class PrivateAppImpl implements BackstageApp { } getPlugins(): BackstagePlugin[] { - return this.plugins; + return Array.from(this.plugins); } getSystemIcon(key: IconKey): IconComponent | undefined { @@ -276,7 +277,6 @@ export class PrivateAppImpl implements BackstageApp { getProvider(): ComponentType<{}> { const appContext = new AppContextImpl(this); - const apiHolder = this.getApiHolder(); const Provider = ({ children }: PropsWithChildren<{}>) => { const appThemeApi = useMemo( @@ -292,11 +292,25 @@ export class PrivateAppImpl implements BackstageApp { routePaths: routePathCollector, routeParents: routeParentCollector, routeObjects: routeObjectCollector, + collectedPlugins: pluginCollector, }, }); validateRoutes(result.routePaths, result.routeParents); + // TODO(Rugvip): Restructure the public API so that we can get an immediate view of + // the app, rather than having to wait for the provider to render. + // For now we need to push the additional plugins we find during + // collection and then make sure we initialize things afterwards. + result.collectedPlugins.forEach(plugin => this.plugins.add(plugin)); + this.verifyPlugins(this.plugins); + + // Initialize APIs once all plugins are available + if (this.apiHolder) { + throw new Error('Plugin holder was initialized too soon'); + } + this.getApiHolder(); + return result; }, [children]); @@ -340,7 +354,7 @@ export class PrivateAppImpl implements BackstageApp { } return ( - + ) { const pluginIds = new Set(); - for (const plugin of this.plugins) { + for (const plugin of plugins) { const id = plugin.getId(); if (pluginIds.has(id)) { throw new Error(`Duplicate plugin found '${id}'`); From 46b7f84c89be6057858058069d5303fd356e2ed5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Apr 2021 17:49:09 +0200 Subject: [PATCH 26/36] example-app: remove explicit plugin imports and some unused plugins Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 3 --- packages/app/src/plugins.ts | 35 +++-------------------------------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 6f58bdb82a..4d2dbe660a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,8 +19,6 @@ "@backstage/plugin-explore": "^0.3.2", "@backstage/plugin-gcp-projects": "^0.2.5", "@backstage/plugin-github-actions": "^0.4.2", - "@backstage/plugin-github-deployments": "^0.1.2", - "@backstage/plugin-gitops-profiles": "^0.2.6", "@backstage/plugin-graphiql": "^0.2.9", "@backstage/plugin-jenkins": "^0.4.1", "@backstage/plugin-kafka": "^0.2.6", @@ -29,7 +27,6 @@ "@backstage/plugin-newrelic": "^0.2.6", "@backstage/plugin-org": "^0.3.12", "@backstage/plugin-pagerduty": "0.3.2", - "@backstage/plugin-register-component": "^0.2.12", "@backstage/plugin-rollbar": "^0.3.3", "@backstage/plugin-scaffolder": "^0.9.0", "@backstage/plugin-search": "^0.3.4", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index e08bfd6abe..7dbea5b995 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -13,36 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse'; -export { catalogPlugin } from '@backstage/plugin-catalog'; -export { scaffolderPlugin } from '@backstage/plugin-scaffolder'; -export { plugin as TechRadar } from '@backstage/plugin-tech-radar'; -export { explorePlugin } from '@backstage/plugin-explore'; -export { plugin as Circleci } from '@backstage/plugin-circleci'; -export { plugin as RegisterComponent } from '@backstage/plugin-register-component'; -export { plugin as Sentry } from '@backstage/plugin-sentry'; -export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles'; -export { plugin as TechDocs } from '@backstage/plugin-techdocs'; -export { plugin as GraphiQL } from '@backstage/plugin-graphiql'; -export { plugin as GithubActions } from '@backstage/plugin-github-actions'; -export { plugin as Rollbar } from '@backstage/plugin-rollbar'; -export { plugin as Newrelic } from '@backstage/plugin-newrelic'; -export { travisciPlugin } from '@roadiehq/backstage-plugin-travis-ci'; -export { plugin as Jenkins } from '@backstage/plugin-jenkins'; -export { plugin as ApiDocs } from '@backstage/plugin-api-docs'; -export { githubPullRequestsPlugin } from '@roadiehq/backstage-plugin-github-pull-requests'; -export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects'; -export { plugin as Kubernetes } from '@backstage/plugin-kubernetes'; -export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; -export { plugin as CostInsights } from '@backstage/plugin-cost-insights'; -export { githubInsightsPlugin } from '@roadiehq/backstage-plugin-github-insights'; -export { plugin as CatalogImport } from '@backstage/plugin-catalog-import'; -export { plugin as UserSettings } from '@backstage/plugin-user-settings'; -export { plugin as PagerDuty } from '@backstage/plugin-pagerduty'; -export { buildkitePlugin } from '@roadiehq/backstage-plugin-buildkite'; -export { plugin as Search } from '@backstage/plugin-search'; -export { plugin as Org } from '@backstage/plugin-org'; -export { plugin as Kafka } from '@backstage/plugin-kafka'; -export { todoPlugin } from '@backstage/plugin-todo'; + +// TODO(Rugvip): This plugin is currently not part of the app element tree, +// ideally we have an API for the context menu that permits that. export { badgesPlugin } from '@backstage/plugin-badges'; -export { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; From ee22773e90f4ffaff7b9b93ebe32fcca83d47da1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Apr 2021 19:23:00 +0200 Subject: [PATCH 27/36] create-app: removed plugins.ts Signed-off-by: Patrik Oldsberg --- .changeset/quiet-badgers-cheer.md | 11 +++++++++++ .../templates/default-app/packages/app/src/App.tsx | 2 -- .../templates/default-app/packages/app/src/plugins.ts | 9 --------- 3 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 .changeset/quiet-badgers-cheer.md delete mode 100644 packages/create-app/templates/default-app/packages/app/src/plugins.ts diff --git a/.changeset/quiet-badgers-cheer.md b/.changeset/quiet-badgers-cheer.md new file mode 100644 index 0000000000..e22bf9492c --- /dev/null +++ b/.changeset/quiet-badgers-cheer.md @@ -0,0 +1,11 @@ +--- +'@backstage/create-app': patch +--- + +Removed `plugins.ts` from the app, as plugins are now discovered through the react tree. + +To apply this change to an existing app, simply delete `packages/app/src/plugins.ts` along with the import and usage in `packages/app/src/App.tsx`. + +Note that there are a few plugins that require explicit registration, in which case you would need to keep them in `plugins.ts`. The set of plugins that need explicit registration is any plugin that doesn't have a component extension that gets rendered as part of the app element tree. An example of such a plugin in the main Backstage repo is `@backstage/plugin-badges`. In the case of the badges plugin this is because there is not yet a component-based API for adding context menu items to the entity layout. + +If you have plugins that still rely on route registration through the `register` method of `createPlugin`, these need to be kept in `plugins.ts` as well. However, it is recommended to migrate these to export an extensions component instead. diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 1ed7aa030a..026771ce4f 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -21,11 +21,9 @@ import { UserSettingsPage } from '@backstage/plugin-user-settings'; import { apis } from './apis'; import { entityPage } from './components/catalog/EntityPage'; import { Root } from './components/Root'; -import * as plugins from './plugins'; const app = createApp({ apis, - plugins: Object.values(plugins), bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, diff --git a/packages/create-app/templates/default-app/packages/app/src/plugins.ts b/packages/create-app/templates/default-app/packages/app/src/plugins.ts deleted file mode 100644 index df53885723..0000000000 --- a/packages/create-app/templates/default-app/packages/app/src/plugins.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { plugin as ApiDocs } from '@backstage/plugin-api-docs'; -export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; -export { plugin as CatalogImport } from '@backstage/plugin-catalog-import'; -export { plugin as GithubActions } from '@backstage/plugin-github-actions'; -export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder'; -export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs'; -export { plugin as TechRadar } from '@backstage/plugin-tech-radar'; -export { plugin as UserSettings } from '@backstage/plugin-user-settings'; - From 1373f4f1229bfe8f9422d4a5301726ed1df524fe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Apr 2021 19:30:44 +0200 Subject: [PATCH 28/36] cli: remove plugin.ts addition from create-plugin Signed-off-by: Patrik Oldsberg --- .changeset/fresh-cheetahs-rush.md | 5 +++++ .../commands/create-plugin/createPlugin.ts | 19 ------------------- 2 files changed, 5 insertions(+), 19 deletions(-) create mode 100644 .changeset/fresh-cheetahs-rush.md diff --git a/.changeset/fresh-cheetahs-rush.md b/.changeset/fresh-cheetahs-rush.md new file mode 100644 index 0000000000..fe8a9caafb --- /dev/null +++ b/.changeset/fresh-cheetahs-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +No longer add newly created plugins to `plugins.ts` in the app, as it is no longer needed. diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index affbf64922..9fc5531dfa 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -106,24 +106,6 @@ export async function addPluginDependencyToApp( }); } -export async function addPluginImportToApp( - rootDir: string, - pluginVar: string, - pluginPackage: string, -) { - const pluginExport = `export { ${pluginVar} } from '${pluginPackage}';`; - const pluginsFilePath = 'packages/app/src/plugins.ts'; - const pluginsFile = resolvePath(rootDir, pluginsFilePath); - - await Task.forItem('processing', pluginsFilePath, async () => { - await addExportStatement(pluginsFile, pluginExport).catch(error => { - throw new Error( - `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, - ); - }); - }); -} - export async function addPluginExtensionToApp( pluginId: string, extensionName: string, @@ -320,7 +302,6 @@ export default async (cmd: Command) => { await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion); Task.section('Import plugin in app'); - await addPluginImportToApp(paths.targetRoot, pluginVar, name); await addPluginExtensionToApp(pluginId, extensionName, name); } From df3afcc896efb41008f9a19af5252bc0e6faa0b9 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 15 Apr 2021 14:13:17 -0600 Subject: [PATCH 29/36] Update contrib k8s docs to reference k8s microsite docs Signed-off-by: Tim Hansen --- contrib/docs/tutorials/aws-deployment.md | 72 +++--------- .../plain_single_backend_deployment/README.md | 44 ------- .../deployment.yaml | 107 ------------------ 3 files changed, 18 insertions(+), 205 deletions(-) delete mode 100644 contrib/kubernetes/plain_single_backend_deployment/README.md delete mode 100644 contrib/kubernetes/plain_single_backend_deployment/deployment.yaml diff --git a/contrib/docs/tutorials/aws-deployment.md b/contrib/docs/tutorials/aws-deployment.md index 5f94672b82..47170658a3 100644 --- a/contrib/docs/tutorials/aws-deployment.md +++ b/contrib/docs/tutorials/aws-deployment.md @@ -41,12 +41,9 @@ documentation to build a new Backstage Docker image: ```shell $ yarn build -$ docker image build . -f packages/backend/Dockerfile --tag backstage +$ yarn build-image --tag backstage ``` -This command builds a backend-only image, but you can similarly build a frontend -or combined Docker image. - Next, configure the [AWS CLI](https://aws.amazon.com/cli/) to use the `ecr-publisher` user you created: @@ -90,65 +87,37 @@ document, but it can be as easy as `eksctl create cluster` documented in the guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html), which uses a Cloudformation template to create the necessary resources. -To deploy the Docker image to EKS, create a `kubernetes` folder in your -Backstage source folder and add a Kubernetes `deployment.yaml`: +To deploy the Docker image to EKS, follow the [Kubernetes +guide](https://backstage.io/docs/deployment/k8s#creating-the-backstage-instance) +but set the Backstage deployment `image` to the ECR repository URL: ```yaml apiVersion: apps/v1 kind: Deployment metadata: - name: backstage-backend - labels: - app: backstage-backend - namespace: default + name: backstage + namespace: backstage spec: - replicas: 1 - selector: - matchLabels: - app: backstage-backend - strategy: - rollingUpdate: - maxSurge: 25% - maxUnavailable: 25% - type: RollingUpdate + ... template: metadata: labels: - app: backstage-backend + app: backstage spec: containers: - image: /backstage:1.0.0 imagePullPolicy: Always - name: backstage-backend - ports: - - containerPort: 7000 - protocol: TCP + ... ``` -Note the `image` key in the container spec referencing the ECR repository. - -Now create a simple `service.yaml` to map the container ports: - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: backstage-backend -spec: - selector: - app: backstage-backend - ports: - - protocol: TCP - port: 80 - targetPort: 7000 -``` - -Apply these Kubernetes definitions to the EKS cluster to complete the Backstage -deployment: +Create the [Service +descriptor](https://backstage.io/docs/deployment/k8s#creating-a-backstage-service) +as well, and apply these Kubernetes definitions to the EKS cluster to complete +the Backstage deployment: ```shell -$ kubectl apply -f deployment.yaml -$ kubectl apply -f service.yaml +$ kubectl apply -f kubernetes/backstage.yaml +$ kubectl apply -f kubernetes/backstage-service.yaml ``` Now you can see your Backstage workload running from the [EKS @@ -158,14 +127,15 @@ console](https://console.aws.amazon.com/eks/home). ### Exposing Backstage with a load balancer -Backstage users need to query the backend, which means we need to expose -the workload with a load balancer. Follow the [Application load balancing on +To make the service useful, we need to expose the workload with a load balancer. +Follow the [Application load balancing on EKS](https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html) guide to set up a Load Balancer controller and Kubernetes ingress to your application. This is ultimately a `kubectl apply` with an ingress definition: ```yaml +# kubernetes/backstage-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -187,9 +157,3 @@ spec: port: number: 80 ``` - -### Updating the deployment - -To update the Kubernetes deployment to a newly published version of your -Backstage Docker image, update the image tag reference in `deployment.yaml` and -then apply the changes to EKS with `kubectl apply -f deployment.yaml`. diff --git a/contrib/kubernetes/plain_single_backend_deployment/README.md b/contrib/kubernetes/plain_single_backend_deployment/README.md deleted file mode 100644 index e4709615ce..0000000000 --- a/contrib/kubernetes/plain_single_backend_deployment/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Plain Kubernetes Deployment - -This directory contains an example of a simple Kubernetes deployment of Backstage. It is not intended to serve as a complete production deployment, but as a starting point for setting one up. - -## Usage - -You can try the deployment out as is. The easiest way is to use [Docker Desktop](https://www.docker.com/products/docker-desktop) with [Kubernetes](https://docs.docker.com/get-started/kube-deploy/). - -You can now follow the documentation here to build the Backend Container [Docker Build](https://backstage.io/docs/getting-started/deployment-docker) - -From a fresh clone of this repo, run the following in the root: - -```bash -yarn install - -yarn docker-build - -kubectl apply -f contrib/kubernetes/plain_single_backend_deployment/deployment.yaml -``` - -You can use the following commands to monitor the deployment: - -```bash -# List all resources in the backstage namespace -kubectl -n backstage get all - -# Inspect the status of the deployment resource -kubectl -n backstage describe deployment backstage-backend - -# Inspect the status of the pod running the backstage backend -kubectl -n backstage describe pod -l app=backstage,component=backend -``` - -Once the deployment is up and running, you can use the following to set up a proxy to reach the backend locally: - -```bash -kubectl proxy -``` - -With the proxy up and running, you should be able to navigate to [http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy](http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy) and see Backstage. Note that you'll end up on a 404 page, but hitting the home icon in the sidebar should take you to the catalog page where you can see a few example services. - -## Caveats - -This deployment is for demonstration purposes only, for a production deployment you will need to set up at least a persistent database and some form of ingress. If your organization doesn't already have established patterns for these, you could look at options of managed PostgreSQL instances from cloud providers, or something like Zalando's [postgres-operator](https://github.com/zalando/postgres-operator). For ingress there are also [plenty of options](https://ramitsurana.gitbook.io/awesome-kubernetes/docs/projects/projects#load-balancing), where `nginx` is a popular choice to get started. diff --git a/contrib/kubernetes/plain_single_backend_deployment/deployment.yaml b/contrib/kubernetes/plain_single_backend_deployment/deployment.yaml deleted file mode 100644 index 822f5e42dc..0000000000 --- a/contrib/kubernetes/plain_single_backend_deployment/deployment.yaml +++ /dev/null @@ -1,107 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: backstage ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: backstage-backend - namespace: backstage -spec: - replicas: 1 - selector: - matchLabels: - app: backstage - component: backend - template: - metadata: - labels: - app: backstage - component: backend - spec: - containers: - - name: backend - # This image is built with `yarn docker-build` in the repo root. - # Replace this with your own image to deploy your own Backstage app. - image: example-backend:latest - imagePullPolicy: Never - - command: [node, packages/backend] - args: [--config, app-config.yaml, --config, k8s-config.yaml] - - env: - # We set this to development to make the backend start with incomplete configuration. In a production - # deployment you will want to make sure that you have a full configuration, and remove any plugins that - # you are not using. - - name: NODE_ENV - value: development - - # This makes it possible for the app to reach the backend when serving through `kubectl proxy` - # If you expose the service using for example an ingress controller, you should - # switch this out or remove it. - # - # Note that we're not setting app.baseUrl here, as setting the base path is not working at the moment. - # Further work is needed around the routing in the frontend or react-router before we can support that. - - name: APP_CONFIG_backend_baseUrl - value: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy - - ports: - - name: http - containerPort: 7000 - - volumeMounts: - - name: config-volume - mountPath: /app/k8s-config.yaml - subPath: k8s-config.yaml - - resources: - limits: - cpu: 1 - memory: 0.5Gi - - readinessProbe: - httpGet: - port: 7000 - path: /healthcheck - livenessProbe: - httpGet: - port: 7000 - path: /healthcheck - - volumes: - - name: config-volume - configMap: - name: backstage-config - items: - - key: app-config - path: k8s-config.yaml ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: backstage-config - namespace: backstage -data: - # Note that the config here is only applied to the backend. The frontend config is applied at build time. - # To override frontend config in this deployment, use `APP_CONFIG_` env vars. - app-config: | - app: - baseUrl: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy - backend: - baseUrl: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy ---- -apiVersion: v1 -kind: Service -metadata: - name: backstage-backend - namespace: backstage -spec: - selector: - app: backstage - component: backend - ports: - - name: http - port: 80 - targetPort: http From af95852d2edc440adfc633cf41b73c3b6e74a51d Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 15 Apr 2021 22:18:59 +0100 Subject: [PATCH 30/36] suggestions Signed-off-by: Andrew Johnson --- .../app/src/components/catalog/EntityPage.tsx | 4 ++++ plugins/github-deployments/package.json | 3 +-- .../GithubDeploymentsTable/columns.tsx | 17 +++++++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 5dc4f4c570..e161022059 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -108,6 +108,7 @@ import { } from '@roadiehq/backstage-plugin-travis-ci'; import React, { ReactNode, useMemo, useState } from 'react'; import BadgeIcon from '@material-ui/icons/CallToAction'; +import { EntityGithubDeploymentsCard } from '@backstage/plugin-github-deployments'; export const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -247,6 +248,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( + + + ); diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 6d4e61d4eb..b8761c1385 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -31,8 +31,7 @@ "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3", - "@types/react": "^16.9" + "react-use": "^15.3.3" }, "devDependencies": { "@backstage/cli": "^0.6.6", diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx index fe166be6e0..e1a9fc180b 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx @@ -26,7 +26,7 @@ import { GithubDeployment } from '../../api'; import { DateTime } from 'luxon'; import { Box, Typography, Link } from '@material-ui/core'; -const statusIndicator = (value: string): React.ReactNode => { +const statusIndicator = (value: string): JSX.Element => { switch (value) { case 'PENDING': return ; @@ -53,7 +53,7 @@ export function createEnvironmentColumn(): TableColumn { export function createStatusColumn(): TableColumn { return { title: 'Status', - render: (row: GithubDeployment): React.ReactNode => ( + render: (row: GithubDeployment): JSX.Element => ( {statusIndicator(row.state)} {row.state} @@ -65,7 +65,7 @@ export function createStatusColumn(): TableColumn { export function createCommitColumn(): TableColumn { return { title: 'Commit', - render: (row: GithubDeployment): React.ReactNode => ( + render: (row: GithubDeployment): JSX.Element => ( {row.commit.abbreviatedOid} @@ -83,17 +83,18 @@ export function createCreatorColumn(): TableColumn { export function createLastUpdatedColumn(): TableColumn { return { title: 'Last Updated', - render: (row: GithubDeployment): React.ReactNode => - DateTime.fromISO(row.updatedAt).toRelative({ locale: 'en' }), + render: (row: GithubDeployment): JSX.Element => ( + {DateTime.fromISO(row.updatedAt).toRelative({ locale: 'en' })} + ), }; } -export function createPayloadColumn( +export function createCustomColumn( title: string, - render: (payload: string) => React.ReactNode, + render: (payload: GithubDeployment) => JSX.Element, ): TableColumn { return { title: title, - render: (deployment: GithubDeployment) => render(deployment.payload), + render: (deployment: GithubDeployment) => render(deployment), }; } From 523371d8a91cc5a570a96bcfd330c768792c10ec Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 16 Apr 2021 01:02:41 +0100 Subject: [PATCH 31/36] remove Signed-off-by: Andrew Johnson --- .../components/GithubDeploymentsCard.test.tsx | 19 ++++++++++++++----- .../GithubDeploymentsTable/columns.tsx | 10 ---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 52c84d657b..f128e6274b 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -26,7 +26,11 @@ import { import { fireEvent } from '@testing-library/react'; import { msw, renderInTestApp } from '@backstage/test-utils'; -import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; +import { + GithubDeployment, + GithubDeploymentsApiClient, + githubDeploymentsApiRef, +} from '../api'; import { githubDeploymentsPlugin } from '../plugin'; import { GithubDeploymentsCard } from './GithubDeploymentsCard'; @@ -40,6 +44,7 @@ import { import { setupServer } from 'msw/node'; import { graphql } from 'msw'; import { GithubDeploymentsTable } from './GithubDeploymentsTable'; +import { Box } from '@material-ui/core'; jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { @@ -173,12 +178,16 @@ describe('github-deployments', () => { return parsedPayload?.target || 'unknown'; }; + const extraColumn = { + title: 'Target', + render: (row: GithubDeployment): JSX.Element => ( + {renderTargetFromPayload(row.payload)} + ), + }; + const columns = [ ...GithubDeploymentsTable.defaultDeploymentColumns, - GithubDeploymentsTable.columns.createPayloadColumn( - 'Target', - renderTargetFromPayload, - ), + extraColumn, ]; const rendered = await renderInTestApp( diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx index e1a9fc180b..94d60e9b5f 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx @@ -88,13 +88,3 @@ export function createLastUpdatedColumn(): TableColumn { ), }; } - -export function createCustomColumn( - title: string, - render: (payload: GithubDeployment) => JSX.Element, -): TableColumn { - return { - title: title, - render: (deployment: GithubDeployment) => render(deployment), - }; -} From 670acd88ee482090dae447d5182df470912506ca Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 15 Apr 2021 21:05:26 -0400 Subject: [PATCH 32/36] Move diagram to system page Signed-off-by: Adam Harvey --- .changeset/sour-plums-enjoy.md | 7 +++++++ packages/app/src/components/catalog/EntityPage.tsx | 7 +++---- .../packages/app/src/components/catalog/EntityPage.tsx | 7 +++---- 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/sour-plums-enjoy.md diff --git a/.changeset/sour-plums-enjoy.md b/.changeset/sour-plums-enjoy.md new file mode 100644 index 0000000000..ef68166255 --- /dev/null +++ b/.changeset/sour-plums-enjoy.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': patch +--- + +Fix system diagram card to be on the system page + +To apply the same fix to an existing application, in `EntityPage.tsx` simply move the `` for the `/diagram` path from the `groupPage` down into the `systemPage` element. diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d6f91957a9..d4db2b0e63 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -441,10 +441,6 @@ const groupPage = ( - - - - ); @@ -463,6 +459,9 @@ const systemPage = ( + + + ); diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index f1aba46c52..a302bb6dd7 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -208,10 +208,6 @@ const groupPage = ( - - - - ); @@ -230,6 +226,9 @@ const systemPage = ( + + + ); From 25fb68e488e6e41b39559e49e5f9d2c3a3082dae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Apr 2021 04:19:34 +0000 Subject: [PATCH 33/36] chore(deps): bump diff from 4.0.2 to 5.0.0 Bumps [diff](https://github.com/kpdecker/jsdiff) from 4.0.2 to 5.0.0. - [Release notes](https://github.com/kpdecker/jsdiff/releases) - [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md) - [Commits](https://github.com/kpdecker/jsdiff/compare/v4.0.2...v5.0.0) Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0a8561fa16..548e659e76 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,7 +62,7 @@ "commander": "^6.1.0", "css-loader": "^3.5.3", "dashify": "^2.0.0", - "diff": "^4.0.2", + "diff": "^5.0.0", "esbuild": "^0.8.56", "eslint": "^7.1.0", "eslint-config-prettier": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 1f76456d00..2fe64c785d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11527,11 +11527,16 @@ diff@1.4.0: resolved "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= -diff@^4.0.1, diff@^4.0.2: +diff@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" From 2346b070295866cd380fbe85c660a85ef74f1f79 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 16 Apr 2021 10:03:32 +0100 Subject: [PATCH 34/36] GithubStateIndicator component + link from backstage Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsTable/columns.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx index 94d60e9b5f..f050af836c 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx @@ -21,13 +21,14 @@ import { TableColumn, StatusAborted, StatusError, + Link, } from '@backstage/core'; import { GithubDeployment } from '../../api'; import { DateTime } from 'luxon'; -import { Box, Typography, Link } from '@material-ui/core'; +import { Box, Typography } from '@material-ui/core'; -const statusIndicator = (value: string): JSX.Element => { - switch (value) { +export const GithubStateIndicator = ({ state }: { state: string }) => { + switch (state) { case 'PENDING': return ; case 'IN_PROGRESS': @@ -55,7 +56,7 @@ export function createStatusColumn(): TableColumn { title: 'Status', render: (row: GithubDeployment): JSX.Element => ( - {statusIndicator(row.state)} + {row.state} ), @@ -66,7 +67,7 @@ export function createCommitColumn(): TableColumn { return { title: 'Commit', render: (row: GithubDeployment): JSX.Element => ( - + {row.commit.abbreviatedOid} ), From cb0206b2b47ff9473725e684961852a7e6266332 Mon Sep 17 00:00:00 2001 From: James Turley Date: Fri, 16 Apr 2021 12:01:54 +0100 Subject: [PATCH 35/36] Extract top-level UI schema keys in multistep form Signed-off-by: James Turley --- .changeset/fluffy-suns-repair.md | 5 +++ .../MultistepJsonForm/schema.test.ts | 2 + .../components/MultistepJsonForm/schema.ts | 44 +++++++++---------- 3 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 .changeset/fluffy-suns-repair.md diff --git a/.changeset/fluffy-suns-repair.md b/.changeset/fluffy-suns-repair.md new file mode 100644 index 0000000000..6f45905219 --- /dev/null +++ b/.changeset/fluffy-suns-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Respect top-level UI schema keys in scaffolder forms. Allows more advanced RJSF features such as explicit field ordering. diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts index e02e5be01c..b83725c2bb 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts @@ -20,6 +20,7 @@ describe('transformSchemaToProps', () => { it('transforms deep schema', () => { const inputSchema = { type: 'object', + 'ui:welp': 'warp', properties: { field1: { type: 'string', @@ -53,6 +54,7 @@ describe('transformSchemaToProps', () => { }, }; const expectedUiSchema = { + 'ui:welp': 'warp', field1: { 'ui:derp': 'herp', }, diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts index 0e1c0a4b50..e591589bd8 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts @@ -22,41 +22,39 @@ function isObject(value: unknown): value is JsonObject { } function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { + if (!isObject(schema)) { + return; + } + const { properties } = schema; + + for (const propName in schema) { + if (!schema.hasOwnProperty(propName)) { + continue; + } + + if (propName.startsWith('ui:')) { + uiSchema[propName] = schema[propName]; + delete schema[propName]; + } + } + if (!isObject(properties)) { return; } + for (const propName in properties) { if (!properties.hasOwnProperty(propName)) { continue; } + const schemaNode = properties[propName]; if (!isObject(schemaNode)) { continue; } - - if (schemaNode.type === 'object') { - const innerUiSchema = {}; - uiSchema[propName] = innerUiSchema; - extractUiSchema(schemaNode, innerUiSchema); - } else { - for (const innerKey in schemaNode) { - if (!schemaNode.hasOwnProperty(innerKey)) { - continue; - } - const innerValue = schemaNode[innerKey]; - if (innerKey.startsWith('ui:')) { - const innerUiSchema = uiSchema[propName] || {}; - if (!isObject(innerUiSchema)) { - throw new TypeError('Unexpected non-object in uiSchema'); - } - uiSchema[propName] = innerUiSchema; - - innerUiSchema[innerKey] = innerValue; - delete schemaNode[innerKey]; - } - } - } + const innerUiSchema = {}; + uiSchema[propName] = innerUiSchema; + extractUiSchema(schemaNode, innerUiSchema); } } From b58eb099f7d349fe7980e98b22c0bca347ba6c51 Mon Sep 17 00:00:00 2001 From: Travis Truman Date: Fri, 16 Apr 2021 08:44:04 -0400 Subject: [PATCH 36/36] Responding to review feedback Signed-off-by: Travis Truman --- docs/features/kubernetes/configuration.md | 2 +- .../src/cluster-locator/ConfigClusterLocator.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index b96b1461de..8222fbb400 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -82,7 +82,7 @@ cluster. Valid values are: ##### `clusters.\*.skipTLSVerify` This determines whether or not the Kubernetes client verifies the TLS -certificate presented by the API server. +certificate presented by the API server. Defaults to `false`. ##### `clusters.\*.serviceAccountToken` (optional) diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 62b127cd60..169e50534f 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -33,10 +33,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { name: c.getString('name'), url: c.getString('url'), serviceAccountToken: c.getOptionalString('serviceAccountToken'), - skipTLSVerify: - c.getOptionalBoolean('skipTLSVerify') === undefined - ? false - : c.getOptionalBoolean('skipTLSVerify'), + skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false, authProvider: c.getString('authProvider'), }; }),